├── .all-contributorsrc ├── .changeset ├── config.json └── eight-cows-ask.md ├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── changesets.yml │ ├── codeql.yml │ ├── i18n-static-gh-pages.yml │ ├── node.js.yml │ └── scorecard.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── eslint.config.mjs ├── package-lock.json ├── package.json ├── tsconfig.base.json ├── tsconfig.json └── workspaces ├── conformance ├── README.md ├── package.json ├── scripts │ └── fetchSpdxLicenses.js ├── src │ ├── class │ │ └── LicenseResult.class.ts │ ├── data │ │ └── spdx.ts │ ├── extract.ts │ ├── index.ts │ ├── licenses.ts │ ├── parse.ts │ └── utils │ │ ├── extractDirentLicenses.ts │ │ ├── index.ts │ │ └── spdx.ts ├── test │ ├── class │ │ └── LicenseResult.spec.ts │ ├── extract.spec.ts │ ├── fixtures │ │ ├── parseLicense.snap.js │ │ ├── project1 │ │ │ ├── LICENSE │ │ │ └── package.json │ │ └── project2 │ │ │ ├── LICENSE │ │ │ └── package.json │ ├── licenses.spec.ts │ ├── parse.spec.ts │ └── utils.spec.ts └── tsconfig.json ├── contact ├── README.md ├── package.json ├── src │ ├── ContactExtractor.class.ts │ ├── UnlitContact.class.ts │ ├── index.ts │ └── utils │ │ ├── compareContact.ts │ │ ├── index.ts │ │ └── parseRegexp.ts ├── test │ ├── ContactExtractor.spec.ts │ └── utils │ │ ├── compareContact.spec.ts │ │ └── parseRegexp.spec.ts └── tsconfig.json ├── i18n ├── README.md ├── index.html ├── package.json ├── public │ ├── css │ │ ├── main.css │ │ └── reset.css │ ├── fonts │ │ ├── mononoki-Bold.eot │ │ ├── mononoki-Bold.woff2 │ │ ├── mononoki-Regular.eot │ │ └── mononoki-Regular.woff2 │ ├── img │ │ ├── devto.png │ │ ├── flag-fr.png │ │ ├── flag-uk.png │ │ ├── github.png │ │ ├── linkedin.png │ │ └── nodesecure-logo.png │ └── js │ │ └── main.js ├── scripts │ └── buildDocumentation.ts ├── src │ ├── constants.ts │ ├── index.ts │ ├── languages │ │ ├── english.ts │ │ ├── french.ts │ │ └── index.ts │ └── utils.ts ├── test │ ├── i18n.spec.ts │ └── utils.spec.ts ├── tsconfig.json └── views │ └── index.html ├── mama ├── CHANGELOG.md ├── README.md ├── package.json ├── src │ ├── ManifestManager.class.ts │ ├── index.ts │ └── utils │ │ ├── index.ts │ │ ├── inspectModuleType.ts │ │ └── integrity-hash.ts ├── test │ ├── ManifestManager.spec.ts │ ├── inspectModuleType.spec.ts │ └── packageJSONIntegrityHash.spec.ts └── tsconfig.json ├── npm-types ├── CHANGELOG.md ├── README.md ├── package.json ├── src │ └── index.d.ts └── tsconfig.json ├── rc ├── README.md ├── package.json ├── src │ ├── constants.ts │ ├── functions │ │ ├── memoize.ts │ │ ├── read.ts │ │ └── write.ts │ ├── index.ts │ ├── projects │ │ ├── ci.ts │ │ ├── report.ts │ │ └── scanner.ts │ ├── rc.ts │ ├── schema │ │ ├── defs │ │ │ ├── ci.json │ │ │ ├── ciWarnings.json │ │ │ ├── contact.json │ │ │ ├── report.json │ │ │ ├── reportChart.json │ │ │ └── scanner.json │ │ ├── loader.ts │ │ └── nodesecurerc.json │ └── utils │ │ ├── index.ts │ │ └── readJSON.ts ├── test │ ├── fixtures │ │ ├── .nodesecurerc │ │ └── configuration │ │ │ ├── ci_v1.json │ │ │ └── ci_v2.json │ ├── index.spec.ts │ ├── memoize.spec.ts │ ├── rc.spec.ts │ ├── read.spec.ts │ ├── types │ │ ├── index.test-d.ts │ │ └── rc.test-d.ts │ └── write.spec.ts └── tsconfig.json ├── scanner ├── CHANGELOG.md ├── README.md ├── docs │ ├── extractors.md │ └── from.md ├── package.json ├── src │ ├── class │ │ └── logger.class.ts │ ├── comparePayloads.ts │ ├── depWalker.ts │ ├── extractors │ │ ├── index.ts │ │ ├── payload.ts │ │ └── probes │ │ │ ├── ContactExtractor.class.ts │ │ │ ├── FlagsExtractor.class.ts │ │ │ ├── LicensesExtractor.class.ts │ │ │ ├── SizeExtractor.class.ts │ │ │ ├── VulnerabilitiesExtractor.class.ts │ │ │ ├── WarningsExtractor.class.ts │ │ │ └── index.ts │ ├── i18n │ │ ├── english.js │ │ └── french.js │ ├── index.ts │ ├── npmRegistry.ts │ ├── types.ts │ └── utils │ │ ├── addMissingVersionFlags.ts │ │ ├── dirname.ts │ │ ├── getLinks.ts │ │ ├── getUsedDeps.ts │ │ ├── index.ts │ │ ├── isNodesecurePayload.ts │ │ ├── manifestAuthor.ts │ │ ├── urlToString.ts │ │ └── warnings.ts ├── test │ ├── comparePayloads.spec.ts │ ├── depWalker.spec.ts │ ├── extractors │ │ └── payload.spec.ts │ ├── fixtures │ │ ├── depWalker │ │ │ ├── non-npm-package │ │ │ │ └── package.json │ │ │ ├── pkg.gitdeps.json │ │ │ ├── slimio.config.json │ │ │ ├── slimio.is-result.json │ │ │ └── slimio.is.json │ │ ├── extractors │ │ │ ├── express.json │ │ │ └── strnum.json │ │ ├── scannerPayloads │ │ │ ├── deeplyUpdatedPayload.json │ │ │ ├── nullAuthor.json │ │ │ ├── otherRootDependency.json │ │ │ ├── payload.json │ │ │ ├── sameIdPayload.json │ │ │ ├── scannerVersionChanged.json │ │ │ ├── vulnerabilityStrategyChanged.json │ │ │ └── warningChangedPayload.json │ │ ├── verify │ │ │ └── express-result.json │ │ └── verifySemVer │ │ │ └── package.json │ ├── integrityWarning.spec.ts │ ├── logger.spec.ts │ ├── npmRegistry.spec.ts │ ├── utils │ │ ├── addMissingVersionFlags.spec.ts │ │ ├── getLinks.spec.ts │ │ ├── getUsedDeps.spec.ts │ │ ├── manifestAuthor.spec.ts │ │ └── warnings.spec.ts │ └── verify.spec.ts └── tsconfig.json ├── tarball ├── CHANGELOG.md ├── README.md ├── package.json ├── src │ ├── index.ts │ ├── sast │ │ ├── file.ts │ │ └── index.ts │ ├── tarball.ts │ ├── utils │ │ ├── analyzeDependencies.ts │ │ ├── booleanToFlags.ts │ │ ├── filterDependencyKind.ts │ │ ├── getPackageName.ts │ │ ├── getTarballComposition.ts │ │ ├── index.ts │ │ └── isSensitiveFile.ts │ └── warnings.ts ├── test │ ├── fixtures │ │ ├── getTarballComposition │ │ │ ├── one │ │ │ │ └── README │ │ │ └── two │ │ │ │ ├── empty.txt │ │ │ │ ├── package.json │ │ │ │ └── two-deep │ │ │ │ └── test.js │ │ ├── scanJavascriptFile │ │ │ ├── fetch.js │ │ │ ├── one.js │ │ │ ├── onelineStmt.min.js │ │ │ ├── parsingError.js │ │ │ └── two.min.js │ │ └── scanPackage │ │ │ └── caseone │ │ │ ├── .gitignore │ │ │ ├── foobar.txt │ │ │ ├── index.js │ │ │ ├── package.json │ │ │ └── src │ │ │ ├── deps.js │ │ │ └── other.min.js │ ├── sast │ │ └── scanFile.spec.ts │ ├── tarball │ │ └── scanPackage.spec.ts │ ├── utils │ │ ├── analyzeDependencies.spec.ts │ │ ├── booleanToFlags.spec.ts │ │ ├── filterDependencyKind.spec.ts │ │ ├── getPackageName.spec.ts │ │ ├── getTarballComposition.spec.ts │ │ └── isSensitiveFile.spec.ts │ └── warnings.spec.ts └── tsconfig.json └── tree-walker ├── CHANGELOG.md ├── README.md ├── package.json ├── src ├── Dependency.class.ts ├── git │ └── .gitkeep ├── index.ts ├── npm │ ├── LocalDependencyTreeLoader.ts │ └── walker.ts └── utils │ ├── index.ts │ ├── isGitDependency.ts │ ├── mergeDependencies.ts │ └── semver.ts ├── test ├── Dependency.spec.ts ├── fixtures │ └── mergeDependencies │ │ ├── one.json │ │ ├── three.json │ │ └── two.json ├── npm │ └── TreeWalker.spec.ts └── utils │ ├── cleanRange.spec.ts │ ├── isGitDependency.spec.ts │ └── mergeDependencies.spec.ts └── tsconfig.json /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { "repo": "nodesecure/scanner" } 6 | ], 7 | "commit": false, 8 | "fixed": [], 9 | "linked": [], 10 | "access": "public", 11 | "baseBranch": "master", 12 | "updateInternalDependencies": "patch", 13 | "ignore": [] 14 | } 15 | -------------------------------------------------------------------------------- /.changeset/eight-cows-ask.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@nodesecure/mama": patch 3 | --- 4 | 5 | Add optional ManifestManager dirname location 6 | -------------------------------------------------------------------------------- /.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 15 | -------------------------------------------------------------------------------- /.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/changesets.yml: -------------------------------------------------------------------------------- 1 | name: Changesets 2 | on: 3 | push: 4 | branches: 5 | - master 6 | env: 7 | CI: true 8 | 9 | jobs: 10 | version: 11 | timeout-minutes: 15 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout code repository 15 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Setup node.js 20 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 21 | with: 22 | node-version: 22 23 | 24 | - name: Install dependencies 25 | run: npm ci 26 | 27 | - name: Build monorepo 28 | run: npm run build 29 | 30 | - name: Update and publish versions 31 | uses: changesets/action@e0145edc7d9d8679003495b11f87bd8ef63c0cba # v1.5.3 32 | with: 33 | version: npm run ci:version 34 | commit: "chore: update versions" 35 | title: "chore: update versions" 36 | publish: npm run ci:publish 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 40 | -------------------------------------------------------------------------------- /.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 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 61 | # queries: security-extended,security-and-quality 62 | 63 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 64 | # If this step fails, then you should remove it and run the build manually (see below) 65 | - name: Autobuild 66 | uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 67 | 68 | # ℹ️ Command-line programs to run using the OS shell. 69 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 70 | 71 | # If the Autobuild fails above, remove it and uncomment the following three lines. 72 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 73 | 74 | # - run: | 75 | # echo "Run, Build Application using script" 76 | # ./location_of_script_within_repo/buildscript.sh 77 | 78 | - name: Perform CodeQL Analysis 79 | uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 80 | with: 81 | category: "/language:${{matrix.language}}" -------------------------------------------------------------------------------- /.github/workflows/i18n-static-gh-pages.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["master"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | # Single deploy job since we're just deploying 26 | deploy: 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | - name: Setup Pages 35 | uses: actions/configure-pages@v5 36 | - name: Upload artifact 37 | uses: actions/upload-pages-artifact@v3 38 | with: 39 | # Upload entire repository 40 | path: './workspaces/i18n' 41 | - name: Deploy to GitHub Pages 42 | id: deployment 43 | uses: actions/deploy-pages@v4 44 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | node-version: [22.x, 24.x] 17 | fail-fast: false 18 | steps: 19 | - name: Harden Runner 20 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 21 | with: 22 | egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs 23 | 24 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | - name: Install dependencies 30 | run: npm install 31 | - name: build dependencies 32 | run: npm run build 33 | - name: Run tests 34 | run: npm run test 35 | -------------------------------------------------------------------------------- /.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: '17 18 * * 4' 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 (https://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 | # next.js build output 61 | .next 62 | 63 | *.tsbuildinfo 64 | 65 | nsecure-result.json 66 | temp/ 67 | dist/ 68 | temp.js 69 | temp.mjs 70 | -------------------------------------------------------------------------------- /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) 2021-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 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Issues 2 | 3 | To report a security issue, please [publish a private security advisory](https://github.com/NodeSecure/scanner/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 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { typescriptConfig, globals } from "@openally/config.eslint"; 2 | 3 | export default [ 4 | ...typescriptConfig({ 5 | languageOptions: { 6 | globals: { 7 | ...globals.browser 8 | } 9 | } 10 | }), 11 | { 12 | ignores: [ 13 | "workspaces/**/coverage", 14 | "workspaces/**/test/fixtures", 15 | "workspaces/**/temp/**", 16 | "workspaces/i18n/src/languages" 17 | ], 18 | } 19 | ]; 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "workspaces": [ 3 | "workspaces/scanner", 4 | "workspaces/tarball", 5 | "workspaces/mama", 6 | "workspaces/tree-walker", 7 | "workspaces/conformance", 8 | "workspaces/contact", 9 | "workspaces/npm-types", 10 | "workspaces/i18n", 11 | "workspaces/rc" 12 | ], 13 | "scripts": { 14 | "build": "tsc --build", 15 | "test": "npm run test --ws --if-present", 16 | "lint": "npx eslint workspaces", 17 | "ci:publish": "changeset publish", 18 | "ci:version": "changeset version" 19 | }, 20 | "author": "NodeSecure", 21 | "license": "MIT", 22 | "devDependencies": { 23 | "@changesets/changelog-github": "^0.5.1", 24 | "@changesets/cli": "^2.29.4", 25 | "@openally/config.eslint": "^2.1.0", 26 | "@openally/config.typescript": "^1.0.3", 27 | "@slimio/is": "^2.0.0", 28 | "@types/node": "^22.15.17", 29 | "@types/pacote": "^11.1.8", 30 | "@types/semver": "^7.5.8", 31 | "c8": "^10.1.2", 32 | "pkg-ok": "^3.0.0", 33 | "tsd": "^0.32.0", 34 | "tsx": "^4.16.2", 35 | "typescript": "^5.5.3" 36 | }, 37 | "engines": { 38 | "node": ">=20" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@openally/config.typescript", 3 | "compilerOptions": { 4 | "composite": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./workspaces/npm-types" 6 | }, 7 | { 8 | "path": "./workspaces/conformance" 9 | }, 10 | { 11 | "path": "./workspaces/mama" 12 | }, 13 | { 14 | "path": "./workspaces/contact" 15 | }, 16 | { 17 | "path": "./workspaces/tarball" 18 | }, 19 | { 20 | "path": "./workspaces/tree-walker" 21 | }, 22 | { 23 | "path": "./workspaces/scanner" 24 | }, 25 | { 26 | "path": "./workspaces/rc" 27 | }, 28 | { 29 | "path": "./workspaces/i18n" 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /workspaces/conformance/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nodesecure/conformance", 3 | "version": "1.0.0", 4 | "description": "SPDX license conformance for NodeSecure", 5 | "type": "module", 6 | "exports": "./dist/index.js", 7 | "types": "./dist/index.d.ts", 8 | "engines": { 9 | "node": ">=20" 10 | }, 11 | "scripts": { 12 | "build": "tsc -b", 13 | "prepublishOnly": "npm run build", 14 | "test-only": "tsx --test ./test/**/*.spec.ts", 15 | "test": "c8 -r html npm run test-only", 16 | "spdx:refresh": "node ./scripts/fetchSpdxLicenses.js" 17 | }, 18 | "files": [ 19 | "dist" 20 | ], 21 | "keywords": [ 22 | "SPDX", 23 | "conformance", 24 | "license" 25 | ], 26 | "author": "GENTILHOMME Thomas ", 27 | "license": "MIT", 28 | "repository": { 29 | "type": "git", 30 | "url": "git+https://github.com/NodeSecure/scanner.git" 31 | }, 32 | "bugs": { 33 | "url": "https://github.com/NodeSecure/scanner/issues" 34 | }, 35 | "homepage": "https://github.com/NodeSecure/tree/master/workspaces/conformance#readme", 36 | "devDependencies": { 37 | "@myunisoft/httpie": "^5.0.1", 38 | "@types/spdx-expression-parse": "^3.0.5", 39 | "node-estree": "^4.0.0" 40 | }, 41 | "dependencies": { 42 | "@nodesecure/mama": "^1.0.0", 43 | "@openally/result": "^1.2.1", 44 | "astring": "^1.9.0", 45 | "fastest-levenshtein": "^1.0.16", 46 | "spdx-expression-parse": "^4.0.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /workspaces/conformance/scripts/fetchSpdxLicenses.js: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import fs from "node:fs"; 3 | 4 | // Import Third-party Dependencies 5 | import httpie from "@myunisoft/httpie"; 6 | import * as astring from "astring"; 7 | import { ESTree, Helpers, VarDeclaration } from "node-estree"; 8 | 9 | // CONSTANTS 10 | const kSrcDirectory = new URL("../src/data/", import.meta.url); 11 | 12 | const { data } = await httpie.get( 13 | "https://raw.githubusercontent.com/spdx/license-list-data/main/json/licenses.json" 14 | ); 15 | const response = JSON.parse(data); 16 | 17 | const spdxProperties = []; 18 | 19 | for (const license of response.licenses) { 20 | const { 21 | name, 22 | isDeprecatedLicenseId: deprecated, 23 | licenseId: id, 24 | isOsiApproved: osi, 25 | isFsfLibre: fsf = false 26 | } = license; 27 | 28 | spdxProperties.push( 29 | ESTree.Property( 30 | id.charAt(0) !== "0" && /^[a-zA-Z0-9]+$/.test(id) ? ESTree.Identifier(id) : ESTree.Literal(id), 31 | Helpers.PlainObject({ name, id, deprecated, osi, fsf }), 32 | { 33 | kind: "init", 34 | computed: false, 35 | method: false, 36 | shorthand: false 37 | } 38 | ) 39 | ); 40 | } 41 | 42 | const prog = ESTree.Program("module", [ 43 | ESTree.ExportNamedDeclaration( 44 | VarDeclaration.const("spdx", ESTree.ObjectExpression(spdxProperties)) 45 | ) 46 | ]); 47 | 48 | fs.writeFileSync( 49 | new URL("spdx.ts", kSrcDirectory), 50 | `/* eslint-disable max-lines */\n\n` + astring.generate(prog) 51 | ); 52 | -------------------------------------------------------------------------------- /workspaces/conformance/src/class/LicenseResult.class.ts: -------------------------------------------------------------------------------- 1 | // Import Internal Dependencies 2 | import { 3 | licenseIdConformance, 4 | searchSpdxLicenseId, 5 | type SpdxLicenseConformance 6 | } from "../parse.js"; 7 | 8 | export interface SpdxFileLicenseConformance extends SpdxLicenseConformance { 9 | fileName: string; 10 | } 11 | 12 | export interface SpdxUnidentifiedLicense { 13 | licenseId: string; 14 | reason: string; 15 | } 16 | 17 | export interface SpdxExtractedResult { 18 | /** 19 | * List of licenses, each with its SPDX conformance details. 20 | * This array includes all licenses found, conforming to the SPDX standards. 21 | */ 22 | licenses: SpdxFileLicenseConformance[]; 23 | 24 | /** 25 | * A unique list of license identifiers (e.g., 'MIT', 'ISC'). 26 | * This list does not contain any duplicate entries. 27 | * It represents the distinct licenses identified. 28 | */ 29 | uniqueLicenseIds: string[]; 30 | 31 | /** 32 | * List of licenses that do not conform to SPDX standards or have invalid/unidentified identifiers. 33 | * This array includes licenses that could not be matched to valid SPDX identifiers. 34 | */ 35 | unidentifiedLicenseIds?: SpdxUnidentifiedLicense[]; 36 | } 37 | 38 | export class LicenseResult { 39 | #uniqueLicenseIds: Set = new Set(); 40 | #invalidLicenseIds: Map = new Map(); 41 | #licenses: SpdxFileLicenseConformance[] = []; 42 | 43 | addLicenseIDFromSource(source: string, file: string) { 44 | const licenseID = searchSpdxLicenseId(source); 45 | if (licenseID !== null) { 46 | this.addLicenseID(licenseID, file); 47 | } 48 | 49 | return this; 50 | } 51 | 52 | addLicenseID(licenseID: string, source: string) { 53 | const conformanceResult = licenseIdConformance(licenseID); 54 | if (conformanceResult.err) { 55 | this.#invalidLicenseIds.set(licenseID, conformanceResult.val.message); 56 | 57 | return this; 58 | } 59 | 60 | const license: SpdxFileLicenseConformance = { 61 | ...conformanceResult.safeUnwrap(), 62 | fileName: source 63 | }; 64 | Object.keys(license.licenses) 65 | .forEach((id) => this.#uniqueLicenseIds.add(id)); 66 | 67 | this.#licenses.push(license); 68 | 69 | return this; 70 | } 71 | 72 | toJSON(): SpdxExtractedResult { 73 | const unidentifiedLicenseIds: SpdxUnidentifiedLicense[] = [...this.#invalidLicenseIds.entries()] 74 | .map(([licenseId, reason]) => { 75 | return { licenseId, reason }; 76 | }); 77 | 78 | return { 79 | licenses: this.#licenses, 80 | uniqueLicenseIds: [...this.#uniqueLicenseIds], 81 | ...(unidentifiedLicenseIds.length > 0 ? { unidentifiedLicenseIds } : {}) 82 | }; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /workspaces/conformance/src/extract.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import * as path from "node:path"; 3 | import * as fsSync from "node:fs"; 4 | import * as fs from "node:fs/promises"; 5 | 6 | // Import Third-party Dependencies 7 | import { ManifestManager } from "@nodesecure/mama"; 8 | 9 | // Import Internal Dependencies 10 | import * as utils from "./utils/index.js"; 11 | import { 12 | LicenseResult, 13 | type SpdxUnidentifiedLicense, 14 | type SpdxFileLicenseConformance, 15 | type SpdxExtractedResult 16 | } from "./class/LicenseResult.class.js"; 17 | 18 | // CONSTANTS 19 | const kManifestFileName = "package.json"; 20 | const kInvalidLicense = "invalid license"; 21 | 22 | export interface ExtractAsyncOptions { 23 | fsEngine?: typeof fs; 24 | } 25 | 26 | export async function extractLicenses( 27 | location: string, 28 | options: ExtractAsyncOptions = {} 29 | ): Promise { 30 | const { fsEngine = fs } = options; 31 | 32 | const mama = await ManifestManager.fromPackageJSON( 33 | location 34 | ); 35 | 36 | const licenseData = new LicenseResult() 37 | .addLicenseID( 38 | mama.license ?? kInvalidLicense, 39 | kManifestFileName 40 | ); 41 | 42 | const dirents = await fsEngine.readdir(location, { 43 | withFileTypes: true 44 | }); 45 | await Promise.allSettled( 46 | utils.extractDirentLicenses(dirents).map(async(file) => { 47 | const contentStr = await fsEngine.readFile( 48 | path.join(location, file), 49 | "utf-8" 50 | ); 51 | licenseData.addLicenseIDFromSource(contentStr, file); 52 | }) 53 | ); 54 | 55 | return licenseData.toJSON(); 56 | } 57 | 58 | export interface ExtractSyncOptions { 59 | fsEngine?: typeof fsSync; 60 | } 61 | 62 | export function extractLicensesSync( 63 | location: string, 64 | options: ExtractSyncOptions = {} 65 | ): SpdxExtractedResult { 66 | const { fsEngine = fsSync } = options; 67 | 68 | const packageStr = fsEngine.readFileSync( 69 | path.join(location, kManifestFileName), "utf-8" 70 | ); 71 | const packageJSON = JSON.parse(packageStr); 72 | const mama = new ManifestManager(packageJSON); 73 | 74 | const licenseData = new LicenseResult(); 75 | licenseData.addLicenseID( 76 | mama.license ?? kInvalidLicense, 77 | kManifestFileName 78 | ); 79 | 80 | const dirents = fsEngine.readdirSync(location, { 81 | withFileTypes: true 82 | }); 83 | for (const file of utils.extractDirentLicenses(dirents)) { 84 | const contentStr = fsEngine.readFileSync( 85 | path.join(location, file), 86 | "utf-8" 87 | ); 88 | licenseData.addLicenseIDFromSource(contentStr, file); 89 | } 90 | 91 | return licenseData.toJSON(); 92 | } 93 | 94 | export type { 95 | SpdxUnidentifiedLicense, 96 | SpdxFileLicenseConformance, 97 | SpdxExtractedResult 98 | }; 99 | -------------------------------------------------------------------------------- /workspaces/conformance/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./extract.js"; 2 | export * from "./parse.js"; 3 | -------------------------------------------------------------------------------- /workspaces/conformance/src/licenses.ts: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import * as levenshtein from "fastest-levenshtein"; 3 | 4 | // Import Internal Dependencies 5 | import { spdx } from "./data/spdx.js"; 6 | 7 | export interface SpdxConformance { 8 | name: string; 9 | id: string; 10 | deprecated: boolean; 11 | osi: boolean; 12 | fsf: boolean; 13 | } 14 | 15 | // CONSTANTS 16 | const kMaximumLicenseDistance = 1; 17 | const kLevenshteinCache = new Map(); 18 | 19 | const licenseNameToId = new Map(); 20 | const osi: string[] = []; 21 | const fsf: string[] = []; 22 | const deprecated: string[] = []; 23 | 24 | for (const [licenseId, license] of Object.entries(spdx)) { 25 | if (license.deprecated) { 26 | deprecated.push(licenseId); 27 | } 28 | if (license.osi) { 29 | osi.push(licenseId); 30 | } 31 | if (license.fsf) { 32 | fsf.push(licenseId); 33 | } 34 | licenseNameToId.set(license.name, license); 35 | } 36 | 37 | const spdxLicenseIds = new Set([ 38 | ...deprecated, 39 | ...fsf, 40 | ...osi 41 | ]); 42 | 43 | export function closestSpdxLicenseID( 44 | licenseID: string 45 | ): string { 46 | if (kLevenshteinCache.has(licenseID)) { 47 | return kLevenshteinCache.get(licenseID)!; 48 | } 49 | 50 | for (const iteratedLicenseId of spdxLicenseIds) { 51 | const distance = levenshtein.distance(licenseID, iteratedLicenseId); 52 | if (distance <= kMaximumLicenseDistance) { 53 | kLevenshteinCache.set(licenseID, iteratedLicenseId); 54 | 55 | return iteratedLicenseId; 56 | } 57 | } 58 | 59 | return licenseID; 60 | } 61 | 62 | export function checkSpdx( 63 | licenseToCheck: string 64 | ) { 65 | return { 66 | osi: osi.includes(licenseToCheck), 67 | fsf: fsf.includes(licenseToCheck), 68 | fsfAndOsi: osi.includes(licenseToCheck) && fsf.includes(licenseToCheck), 69 | includesDeprecated: deprecated.includes(licenseToCheck) 70 | }; 71 | } 72 | 73 | export { 74 | osi, 75 | fsf, 76 | deprecated, 77 | licenseNameToId, 78 | spdxLicenseIds 79 | }; 80 | -------------------------------------------------------------------------------- /workspaces/conformance/src/parse.ts: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import parseExpressions from "spdx-expression-parse"; 3 | import { Result, Ok, Err } from "@openally/result"; 4 | 5 | // Import Internal Dependencies 6 | import { 7 | spdxLicenseIds, 8 | licenseNameToId, 9 | closestSpdxLicenseID, 10 | checkSpdx 11 | } from "./licenses.js"; 12 | import * as utils from "./utils/index.js"; 13 | 14 | export interface SpdxLicenseConformance { 15 | licenses: Record; 16 | spdx: { 17 | osi: boolean; 18 | fsf: boolean; 19 | fsfAndOsi: boolean; 20 | includesDeprecated: boolean; 21 | }; 22 | } 23 | 24 | export function licenseIdConformance( 25 | licenseID: string 26 | ): Result { 27 | let closestLicenseID: string; 28 | if (/and|or/.exec(licenseID) === null) { 29 | closestLicenseID = spdxLicenseIds.has(licenseID) ? 30 | licenseID : 31 | closestSpdxLicenseID(licenseID); 32 | } 33 | else { 34 | closestLicenseID = licenseID; 35 | } 36 | 37 | try { 38 | const uniqueLicenseIds = [ 39 | ...extractLicenseIds(parseExpressions(closestLicenseID)) 40 | ]; 41 | 42 | return Ok( 43 | buildSpdxLicenseConformance(uniqueLicenseIds) 44 | ); 45 | } 46 | catch (cause) { 47 | return Err( 48 | new Error(`Passed license expression '${closestLicenseID}' was not a valid license expression.`, { 49 | cause 50 | }) 51 | ); 52 | } 53 | } 54 | 55 | export function searchSpdxLicenseId( 56 | contentStr: string 57 | ): string | null { 58 | for (const [licenseName, license] of licenseNameToId) { 59 | if (contentStr.indexOf(licenseName) > -1) { 60 | return license.id; 61 | } 62 | } 63 | 64 | return null; 65 | } 66 | 67 | function buildSpdxLicenseConformance( 68 | uniqueLicenseIds: string[] 69 | ): SpdxLicenseConformance { 70 | const conformance: SpdxLicenseConformance = { 71 | licenses: Object.fromEntries( 72 | uniqueLicenseIds.map((id) => [id, utils.createSpdxLink(id)]) 73 | ), 74 | spdx: { 75 | osi: false, 76 | fsf: false, 77 | fsfAndOsi: false, 78 | includesDeprecated: false 79 | } 80 | }; 81 | const licenseSpdx = uniqueLicenseIds.map((id) => checkSpdx(id)); 82 | 83 | conformance.spdx.osi = utils.checkEveryTruthy( 84 | ...licenseSpdx.map((spdx) => spdx.osi) 85 | ); 86 | conformance.spdx.fsf = utils.checkEveryTruthy( 87 | ...licenseSpdx.map((spdx) => spdx.fsf) 88 | ); 89 | conformance.spdx.fsfAndOsi = utils.checkEveryTruthy( 90 | ...licenseSpdx.map((spdx) => spdx.fsfAndOsi) 91 | ); 92 | conformance.spdx.includesDeprecated = utils.checkSomeTruthy( 93 | ...licenseSpdx.map((spdx) => spdx.includesDeprecated) 94 | ); 95 | 96 | return conformance; 97 | } 98 | 99 | function* extractLicenseIds( 100 | data: parseExpressions.Info 101 | ): IterableIterator { 102 | if ("conjunction" in data) { 103 | yield* extractLicenseIds(data.left); 104 | yield* extractLicenseIds(data.right); 105 | } 106 | else { 107 | yield data.license; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /workspaces/conformance/src/utils/extractDirentLicenses.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { Dirent } from "node:fs"; 3 | 4 | export function extractDirentLicenses( 5 | dirents: Dirent[] 6 | ): string[] { 7 | return dirents 8 | .flatMap((dirent) => (dirent.isFile() && dirent.name.toLowerCase().includes("license") ? [dirent.name] : [])); 9 | } 10 | -------------------------------------------------------------------------------- /workspaces/conformance/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./extractDirentLicenses.js"; 2 | export * from "./spdx.js"; 3 | -------------------------------------------------------------------------------- /workspaces/conformance/src/utils/spdx.ts: -------------------------------------------------------------------------------- 1 | export function checkEveryTruthy( 2 | ...arrayOfBooleans: boolean[] 3 | ): boolean { 4 | return arrayOfBooleans.every((check) => check); 5 | } 6 | 7 | export function checkSomeTruthy( 8 | ...arrayOfBooleans: boolean[] 9 | ): boolean { 10 | return arrayOfBooleans.some((check) => check); 11 | } 12 | 13 | export function createSpdxLink( 14 | license: string 15 | ): string { 16 | return `https://spdx.org/licenses/${license}.html#licenseText`; 17 | } 18 | -------------------------------------------------------------------------------- /workspaces/conformance/test/class/LicenseResult.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 { 7 | LicenseResult, 8 | type SpdxExtractedResult 9 | } from "../../src/class/LicenseResult.class.js"; 10 | 11 | const kMITSpdxConformance: SpdxExtractedResult = { 12 | uniqueLicenseIds: ["MIT"], 13 | licenses: [ 14 | { 15 | licenses: { 16 | MIT: "https://spdx.org/licenses/MIT.html#licenseText" 17 | }, 18 | spdx: { 19 | fsf: true, 20 | fsfAndOsi: true, 21 | includesDeprecated: false, 22 | osi: true 23 | }, 24 | fileName: "LICENSE" 25 | } 26 | ] 27 | }; 28 | 29 | describe("LicenseResult", () => { 30 | it("should add the license to the invalid list if not known", () => { 31 | const lr = new LicenseResult(); 32 | lr.addLicenseID("notalicense", "foobar"); 33 | 34 | const result = lr.toJSON(); 35 | assert.deepStrictEqual( 36 | result.unidentifiedLicenseIds, 37 | [ 38 | { 39 | licenseId: "notalicense", 40 | reason: "Passed license expression 'notalicense' was not a valid license expression." 41 | } 42 | ] 43 | ); 44 | assert.strictEqual(result.licenses.length, 0); 45 | }); 46 | 47 | it("should add MIT using a source", () => { 48 | const lr = new LicenseResult(); 49 | lr.addLicenseIDFromSource("blabla MIT License yooyo", "LICENSE"); 50 | 51 | const result = lr.toJSON(); 52 | assert.deepEqual(result, kMITSpdxConformance); 53 | }); 54 | 55 | it("should add MIT license and hasMultipleLicenses should be false", () => { 56 | const lr = new LicenseResult(); 57 | lr.addLicenseID("MIT", "LICENSE"); 58 | 59 | const result = lr.toJSON(); 60 | assert.deepEqual(result, kMITSpdxConformance); 61 | }); 62 | 63 | it("should add MIT and ISC licenses and hasMultipleLicenses should be true", () => { 64 | const licenseSource = "LICENSE"; 65 | const lr = new LicenseResult(); 66 | lr.addLicenseID("ISC", "package.json"); 67 | lr.addLicenseID("MIT", licenseSource); 68 | 69 | const result = lr.toJSON(); 70 | assert.deepStrictEqual(result.uniqueLicenseIds, ["ISC", "MIT"]); 71 | assert.strictEqual(result.licenses.length, 2); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /workspaces/conformance/test/extract.spec.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import path from "node:path"; 3 | import assert from "node:assert"; 4 | import { fileURLToPath } from "node:url"; 5 | import { describe, it } from "node:test"; 6 | 7 | // Import Internal Dependencies 8 | import { 9 | extractLicenses, 10 | extractLicensesSync 11 | } from "../src/index.js"; 12 | import expectedParsedLicense from "./fixtures/parseLicense.snap.js"; 13 | 14 | // CONSTANTS 15 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 16 | const kFixturePath = path.join(__dirname, "fixtures"); 17 | 18 | describe("extractLicenses", () => { 19 | it("should detect two licenses (ISC, MIT) in project1", async() => { 20 | const result = await extractLicenses(path.join(kFixturePath, "project1")); 21 | 22 | assert.deepStrictEqual(result.uniqueLicenseIds, ["ISC", "MIT"]); 23 | assert.deepStrictEqual(result, expectedParsedLicense); 24 | }); 25 | 26 | it("should detect one license (Artistic-2.0) in project2", async() => { 27 | const result = await extractLicenses(path.join(kFixturePath, "project2")); 28 | 29 | assert.deepStrictEqual(result.uniqueLicenseIds, ["Artistic-2.0"]); 30 | }); 31 | }); 32 | 33 | describe("extractLicensesSync", () => { 34 | it("should detect two licenses (ISC, MIT) in project1", async() => { 35 | const result = extractLicensesSync(path.join(kFixturePath, "project1")); 36 | 37 | assert.deepStrictEqual(result.uniqueLicenseIds, ["ISC", "MIT"]); 38 | assert.deepStrictEqual(result, expectedParsedLicense); 39 | }); 40 | 41 | it("should detect one license (Artistic-2.0) in project2", async() => { 42 | const result = extractLicensesSync(path.join(kFixturePath, "project2")); 43 | 44 | assert.deepStrictEqual(result.uniqueLicenseIds, ["Artistic-2.0"]); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /workspaces/conformance/test/fixtures/parseLicense.snap.js: -------------------------------------------------------------------------------- 1 | export default { 2 | licenses: [ 3 | { 4 | licenses: { 5 | ISC: "https://spdx.org/licenses/ISC.html#licenseText" 6 | }, 7 | spdx: { 8 | fsf: true, 9 | fsfAndOsi: true, 10 | includesDeprecated: false, 11 | osi: true 12 | }, 13 | fileName: "package.json" 14 | }, 15 | { 16 | licenses: { 17 | MIT: "https://spdx.org/licenses/MIT.html#licenseText" 18 | }, 19 | spdx: { 20 | fsf: true, 21 | fsfAndOsi: true, 22 | includesDeprecated: false, 23 | osi: true 24 | }, 25 | fileName: "LICENSE" 26 | } 27 | ], 28 | uniqueLicenseIds: [ 29 | "ISC", 30 | "MIT" 31 | ] 32 | }; 33 | -------------------------------------------------------------------------------- /workspaces/conformance/test/fixtures/project1/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 SlimIO 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 | -------------------------------------------------------------------------------- /workspaces/conformance/test/fixtures/project1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "ISC" 3 | } 4 | -------------------------------------------------------------------------------- /workspaces/conformance/test/fixtures/project2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "Artistic-2.0" 3 | } 4 | -------------------------------------------------------------------------------- /workspaces/conformance/test/licenses.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 { closestSpdxLicenseID, checkSpdx } from "../src/licenses.js"; 7 | 8 | describe("closestSpdxLicenseID", () => { 9 | test("it should return the given LicenseID if no record match", () => { 10 | assert.equal(closestSpdxLicenseID("foooobar"), "foooobar"); 11 | }); 12 | 13 | test("it should fix 'BSD 3-Clause' to 'BSD-3-Clause'", () => { 14 | assert.equal(closestSpdxLicenseID("BSD 3-Clause"), "BSD-3-Clause"); 15 | }); 16 | 17 | test("it should not fix 'BSD 3 Clause' because the distance is greater than one", () => { 18 | assert.equal(closestSpdxLicenseID("BSD 3 Clause"), "BSD 3 Clause"); 19 | }); 20 | }); 21 | 22 | describe("checkSpdx", () => { 23 | test("test with MIT license", () => { 24 | const mitLicense = checkSpdx("MIT"); 25 | assert.deepEqual(mitLicense, { 26 | osi: true, 27 | fsf: true, 28 | fsfAndOsi: true, 29 | includesDeprecated: false 30 | }); 31 | }); 32 | 33 | test("test with a deprecated license", () => { 34 | const deprecatedLicense = checkSpdx("AGPL-1.0"); 35 | assert.deepEqual(deprecatedLicense, { 36 | osi: false, 37 | fsf: true, 38 | fsfAndOsi: false, 39 | includesDeprecated: true 40 | }); 41 | }); 42 | 43 | test("test with a broken license", () => { 44 | const brokenLicense = checkSpdx("wrong"); 45 | assert.deepEqual(brokenLicense, { 46 | osi: false, 47 | fsf: false, 48 | fsfAndOsi: false, 49 | includesDeprecated: false 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /workspaces/conformance/test/utils.spec.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { describe, it, test } from "node:test"; 3 | import { Dirent } from "node:fs"; 4 | import assert from "node:assert"; 5 | 6 | // Import Internal Dependencies 7 | import * as utils from "../src/utils/index.js"; 8 | 9 | describe("checkEveryTruthy", () => { 10 | test("check a single true is true", () => { 11 | assert.equal(utils.checkEveryTruthy(true), true); 12 | }); 13 | 14 | test("check multiple true booleans are true", () => { 15 | assert.equal(utils.checkEveryTruthy(true, true, true), true); 16 | }); 17 | 18 | test("check that a single false is false", () => { 19 | assert.equal(utils.checkEveryTruthy(false), false); 20 | }); 21 | 22 | test("ensure that one false will result in a false return", () => { 23 | assert.equal(utils.checkEveryTruthy(true, false), false); 24 | }); 25 | }); 26 | 27 | describe("checkSomeTruthy", () => { 28 | test("check a single true is true", () => { 29 | assert.equal(utils.checkSomeTruthy(true), true); 30 | }); 31 | 32 | test("check multiple true booleans are true", () => { 33 | assert.equal(utils.checkSomeTruthy(true, true, true), true); 34 | }); 35 | 36 | test("check that a single false is false", () => { 37 | assert.equal(utils.checkSomeTruthy(false), false); 38 | }); 39 | 40 | test("ensure that one false will result in a true return", () => { 41 | assert.equal(utils.checkSomeTruthy(true, false), true); 42 | }); 43 | }); 44 | 45 | describe("createSpdxLink", () => { 46 | test("create an MIT SPDX link", () => { 47 | const link = utils.createSpdxLink("MIT"); 48 | 49 | assert.strictEqual(link, "https://spdx.org/licenses/MIT.html#licenseText"); 50 | }); 51 | }); 52 | 53 | describe("extractDirentLicenses", () => { 54 | it("should only extract file dirent that include 'license' in their name", () => { 55 | const dirents = [ 56 | createDirent("foobar", false), 57 | createDirent("LicenseFile"), 58 | createDirent("yoyoo") 59 | ]; 60 | 61 | assert.deepEqual( 62 | utils.extractDirentLicenses(dirents), 63 | ["LicenseFile"] 64 | ); 65 | }); 66 | }); 67 | 68 | function createDirent(name: string, isFile = true) { 69 | return { 70 | isFile: () => isFile, 71 | name 72 | } as Dirent; 73 | } 74 | -------------------------------------------------------------------------------- /workspaces/conformance/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | }, 7 | "include": ["src"], 8 | "references": [ 9 | { 10 | "path": "../mama" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /workspaces/contact/README.md: -------------------------------------------------------------------------------- 1 |

2 | @nodesecure/contact 3 |

4 | 5 |

6 | Utilities to extract/fetch data on NPM contacts (author, maintainers etc..) 7 |

8 | 9 | ## Requirements 10 | - [Node.js](https://nodejs.org/en/) v20 or higher 11 | 12 | ## Getting Started 13 | 14 | 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). 15 | 16 | ```bash 17 | $ npm i @nodesecure/contact 18 | # or 19 | $ yarn add @nodesecure/contact 20 | ``` 21 | 22 | ## Usage example 23 | 24 | Here is an example of usage from the Scanner. In this case, we are using **dependenciesMap**, which is a `Record`. However, you can build your own record of `ContactExtractorPackageMetadata`. 25 | 26 | ```ts 27 | import { 28 | ContactExtractor, 29 | type ContactExtractorPackageMetadata 30 | } from "@nodesecure/contact"; 31 | 32 | const dependencies: Record = Object.create(null); 33 | for (const [packageName, dependency] of dependenciesMap) { 34 | const { author, maintainers } = dependency.metadata; 35 | 36 | dependencies[packageName] = { 37 | maintainers, 38 | ...( author === null ? {} : { author } ) 39 | } 40 | } 41 | 42 | const extractor = new ContactExtractor({ 43 | highlight: [ 44 | { 45 | name: "Sindre Sorhus" 46 | } 47 | ] 48 | }); 49 | const contacts = extractor.fromDependencies( 50 | dependencies 51 | ); 52 | console.log(contacts); 53 | ``` 54 | 55 | ## API 56 | 57 | Contact is defined by the following TypeScript interface: 58 | ```ts 59 | interface Contact { 60 | email?: string; 61 | url?: string; 62 | name: string; 63 | } 64 | ``` 65 | 66 | > [!NOTE] 67 | > This package authorizes literal RegExp in the name property 68 | 69 | ### ContactExtractor 70 | 71 | The constructor take a list of contacts you want to find/extract. 72 | 73 | ```ts 74 | interface ContactExtractorOptions { 75 | highlight: Contact[]; 76 | } 77 | ``` 78 | 79 | The method **fromDependencies** will return an array of IlluminatedContact objects if any are found in the provided dependencies. 80 | 81 | ```ts 82 | type IlluminatedContact = Contact & { 83 | dependencies: string[]; 84 | } 85 | ``` 86 | 87 | ### compareContact(contactA: Contact, contactB: Contact, options?: CompareOptions): boolean 88 | 89 | Compare two contacts and return `true` if they are the same person 90 | 91 | ```ts 92 | import { 93 | compareContact 94 | } from "@nodesecure/contact"; 95 | import assert from "node:assert"; 96 | 97 | assert.ok( 98 | compareContact( 99 | { name: "john doe" }, 100 | { name: "John Doe" } 101 | ) 102 | ); 103 | ``` 104 | 105 | Each string is trimmed, converted to lowercase, and any multiple spaces are reduced to a single space. 106 | 107 | #### Options 108 | 109 | ```ts 110 | interface CompareOptions { 111 | /** 112 | * @default true 113 | */ 114 | compareName?: boolean; 115 | } 116 | ``` 117 | 118 | ## License 119 | MIT 120 | -------------------------------------------------------------------------------- /workspaces/contact/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nodesecure/contact", 3 | "version": "1.0.1", 4 | "description": "Utilities to extract/fetch data on NPM contacts (author, maintainers ..)", 5 | "type": "module", 6 | "exports": "./dist/index.js", 7 | "types": "./dist/index.d.ts", 8 | "engines": { 9 | "node": ">=20" 10 | }, 11 | "scripts": { 12 | "build": "tsc -b", 13 | "prepublishOnly": "npm run build", 14 | "test-only": "tsx --test ./test/**/*.spec.ts", 15 | "test": "c8 -r html npm run test-only" 16 | }, 17 | "files": [ 18 | "dist" 19 | ], 20 | "keywords": [ 21 | "author", 22 | "contact", 23 | "maintainer" 24 | ], 25 | "author": "GENTILHOMME Thomas ", 26 | "license": "MIT", 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/NodeSecure/scanner.git" 30 | }, 31 | "bugs": { 32 | "url": "https://github.com/NodeSecure/scanner/issues" 33 | }, 34 | "homepage": "https://github.com/NodeSecure/tree/master/workspaces/contact#readme", 35 | "devDependencies": { 36 | "@faker-js/faker": "^9.7.0" 37 | }, 38 | "dependencies": { 39 | "@nodesecure/npm-types": "^1.2.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /workspaces/contact/src/ContactExtractor.class.ts: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import type { Contact } from "@nodesecure/npm-types"; 3 | 4 | // Import Internal Dependencies 5 | import { 6 | UnlitContact, 7 | type IlluminatedContact 8 | } from "./UnlitContact.class.js"; 9 | 10 | export type { IlluminatedContact }; 11 | 12 | export interface ContactExtractorPackageMetadata { 13 | author?: Contact; 14 | maintainers: Contact[]; 15 | } 16 | 17 | export interface ContactExtractorOptions { 18 | highlight: Contact[]; 19 | } 20 | 21 | export class ContactExtractor { 22 | private highlighted: Contact[] = []; 23 | 24 | constructor( 25 | options: ContactExtractorOptions 26 | ) { 27 | const { highlight } = options; 28 | 29 | this.highlighted = structuredClone(highlight); 30 | } 31 | 32 | fromDependencies( 33 | dependencies: Record 34 | ): IlluminatedContact[] { 35 | const unlitContacts = this.highlighted 36 | .map((contact) => new UnlitContact(contact)); 37 | 38 | for (const [packageName, metadata] of Object.entries(dependencies)) { 39 | for (const unlit of unlitContacts) { 40 | const isMaintainer = extractMetadataContacts(metadata) 41 | .some((contact) => unlit.compareTo(contact)); 42 | if (isMaintainer) { 43 | unlit.dependencies.add(packageName); 44 | } 45 | } 46 | } 47 | 48 | return unlitContacts.flatMap( 49 | (unlit) => (unlit.dependencies.size > 0 ? [unlit.illuminate()] : []) 50 | ); 51 | } 52 | } 53 | 54 | function extractMetadataContacts( 55 | metadata: ContactExtractorPackageMetadata 56 | ): Contact[] { 57 | return [ 58 | ...(metadata.author ? [metadata.author] : []), 59 | ...metadata.maintainers 60 | ]; 61 | } 62 | -------------------------------------------------------------------------------- /workspaces/contact/src/UnlitContact.class.ts: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import type { Contact } from "@nodesecure/npm-types"; 3 | 4 | // Import Internal Dependencies 5 | import * as utils from "./utils/index.js"; 6 | 7 | export type IlluminatedContact = Contact & { 8 | dependencies: string[]; 9 | }; 10 | 11 | export class UnlitContact { 12 | private illuminated: Contact; 13 | private extendedName: RegExp | null = null; 14 | 15 | public dependencies = new Set(); 16 | 17 | constructor(contact: Contact) { 18 | this.illuminated = structuredClone(contact); 19 | this.extendedName = utils.parseRegExp(contact.name); 20 | } 21 | 22 | compareTo( 23 | contact: Contact 24 | ): boolean { 25 | if (this.extendedName === null) { 26 | return utils.compareContact(this.illuminated, contact); 27 | } 28 | 29 | if (this.extendedName.test(contact.name)) { 30 | return true; 31 | } 32 | 33 | return utils.compareContact(this.illuminated, contact, { 34 | compareName: false 35 | }); 36 | } 37 | 38 | illuminate(): IlluminatedContact { 39 | return { 40 | ...this.illuminated, 41 | dependencies: [...this.dependencies] 42 | }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /workspaces/contact/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ContactExtractor.class.js"; 2 | export { 3 | compareContact 4 | } from "./utils/index.js"; 5 | -------------------------------------------------------------------------------- /workspaces/contact/src/utils/compareContact.ts: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import type { Contact } from "@nodesecure/npm-types"; 3 | 4 | export interface CompareOptions { 5 | /** 6 | * @default true 7 | */ 8 | compareName?: boolean; 9 | } 10 | 11 | /** 12 | * Compare two contacts and return true if they are the same person 13 | * 14 | * TODO: 15 | * - name separated by comma instead of space 16 | * - lot of emails is a combinaison of last name + first name 17 | * - look for name in email and url 18 | * - add options for custom/advanced comparaison 19 | */ 20 | export function compareContact( 21 | contactA: Contact, 22 | contactB: Contact, 23 | options: CompareOptions = Object.create(null) 24 | ): boolean { 25 | const { compareName = true } = options; 26 | 27 | if ( 28 | compareName && 29 | typeof contactA.name === "string" && 30 | typeof contactB.name === "string" 31 | ) { 32 | const aName = cleanup(contactA.name); 33 | const bName = cleanup(contactB.name); 34 | 35 | const aNameReversed = reverse(aName); 36 | const bNameReversed = reverse(bName); 37 | 38 | if ( 39 | aName === bName || 40 | aNameReversed === bName || 41 | aName === bNameReversed || 42 | aNameReversed === bNameReversed 43 | ) { 44 | return true; 45 | } 46 | } 47 | 48 | if ( 49 | typeof contactA.email === "string" && 50 | typeof contactB.email === "string" && 51 | compareEmail(contactA.email, contactB.email) 52 | ) { 53 | return true; 54 | } 55 | 56 | if ( 57 | typeof contactA.url === "string" && 58 | typeof contactB.url === "string" && 59 | compareURL(contactA.url, contactB.url) 60 | ) { 61 | return true; 62 | } 63 | 64 | return false; 65 | } 66 | 67 | function compareEmail( 68 | emailA: string, 69 | emailB: string 70 | ) { 71 | const cleanEmailA = cleanup(emailA); 72 | const cleanEmailB = cleanup(emailB); 73 | 74 | return cleanEmailA === cleanEmailB; 75 | } 76 | 77 | function compareURL( 78 | urlA: string, 79 | urlB: string 80 | ) { 81 | const cleanURLA = cleanup(urlA); 82 | const cleanURLB = cleanup(urlB); 83 | 84 | return cleanURLA === cleanURLB; 85 | } 86 | 87 | /** 88 | * A minimal cleanup to avoid any mistakes 89 | * @example 90 | * cleanup(" John Doe"); // "john doe" 91 | */ 92 | function cleanup( 93 | value: string 94 | ): string { 95 | return value 96 | .trim() 97 | .replace(/\s+/g, " ") 98 | .toLowerCase(); 99 | } 100 | 101 | /** 102 | * The goal of this function is to reverse first name and last name 103 | * @example 104 | * reverse("john doe"); // "doe john" 105 | */ 106 | function reverse( 107 | name: string 108 | ): string { 109 | return name 110 | .split(" ") 111 | .reverse() 112 | .join(" "); 113 | } 114 | -------------------------------------------------------------------------------- /workspaces/contact/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./compareContact.js"; 2 | export * from "./parseRegexp.js"; 3 | -------------------------------------------------------------------------------- /workspaces/contact/src/utils/parseRegexp.ts: -------------------------------------------------------------------------------- 1 | 2 | export function parseRegExp( 3 | input: string 4 | ): null | RegExp { 5 | const match = input.match(/(\/+)(.+)\1([a-z]*)/i); 6 | if (!match) { 7 | return null; 8 | } 9 | 10 | const validFlags = Array.from(new Set(match[3])) 11 | .filter((flag) => "gimsuy".includes(flag)) 12 | .join(""); 13 | 14 | return new RegExp(match[2], validFlags); 15 | } 16 | -------------------------------------------------------------------------------- /workspaces/contact/test/utils/parseRegexp.spec.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import assert from "node:assert"; 3 | import { describe, test } from "node:test"; 4 | 5 | // Import Internal Dependencies 6 | import { parseRegExp } from "../../src/utils/index.js"; 7 | 8 | describe("parseRegExp", () => { 9 | test("Given a literal with no slash then it must return null", () => { 10 | assert.strictEqual( 11 | parseRegExp("hello"), 12 | null 13 | ); 14 | }); 15 | 16 | test("Given a literal with a valid RegExp in it then it must return a RegExp", () => { 17 | const regexp = parseRegExp("/^hello/i"); 18 | 19 | assert.ok(regexp instanceof RegExp); 20 | assert.ok( 21 | regexp.test("Hello World") 22 | ); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /workspaces/contact/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | }, 7 | "include": ["src"], 8 | "references": [ 9 | { 10 | "path": "../npm-types" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /workspaces/i18n/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nodesecure/i18n", 3 | "version": "4.0.1", 4 | "description": "NodeSecure Internationalization", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "scripts": { 8 | "build": "tsc", 9 | "prepublishOnly": "npm run build", 10 | "test-only": "tsx --test ./test/**/*.spec.ts", 11 | "test": "c8 -r html npm run test-only", 12 | "build:documentation": "tsx ./scripts/buildDocumentation.ts" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/NodeSecure/scanner.git" 17 | }, 18 | "keywords": [ 19 | "i18n", 20 | "nodesecure" 21 | ], 22 | "author": "GENTILHOMME Thomas ", 23 | "files": [ 24 | "index.d.ts", 25 | "index.js", 26 | "languages", 27 | "src" 28 | ], 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/NodeSecure/scanner/issues" 32 | }, 33 | "homepage": "https://github.com/NodeSecure/tree/master/workspaces/i18n#readme", 34 | "devDependencies": { 35 | "@types/lodash.get": "^4.4.9", 36 | "zup": "^0.0.2" 37 | }, 38 | "type": "module", 39 | "engines": { 40 | "node": ">=20" 41 | }, 42 | "dependencies": { 43 | "cacache": "^19.0.1", 44 | "deepmerge": "^4.3.1", 45 | "lodash.get": "^4.4.2" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /workspaces/i18n/public/css/reset.css: -------------------------------------------------------------------------------- 1 | 2 | *, 3 | *::before, 4 | *::after { 5 | margin:0; 6 | padding: 0; 7 | box-sizing: border-box; 8 | } 9 | html, 10 | body { 11 | height: 100%; 12 | } 13 | body { 14 | line-height: 1.5; 15 | -webkit-font-smoothing: antialiased; 16 | } 17 | img, 18 | picture, 19 | video, 20 | canvas, 21 | svg { 22 | display: block; 23 | max-width: 100%; 24 | } 25 | input, 26 | button, 27 | textarea, 28 | select { 29 | font: inherit; 30 | } 31 | p, 32 | h1, 33 | h2, 34 | h3, 35 | h4, 36 | h5, 37 | h6 { 38 | overflow-wrap: break-word; 39 | } 40 | ol, 41 | ul{ 42 | list-style-type: none; 43 | } -------------------------------------------------------------------------------- /workspaces/i18n/public/fonts/mononoki-Bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NodeSecure/scanner/97a36b523aa9b22900cd4ad822aa6a083e254121/workspaces/i18n/public/fonts/mononoki-Bold.eot -------------------------------------------------------------------------------- /workspaces/i18n/public/fonts/mononoki-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NodeSecure/scanner/97a36b523aa9b22900cd4ad822aa6a083e254121/workspaces/i18n/public/fonts/mononoki-Bold.woff2 -------------------------------------------------------------------------------- /workspaces/i18n/public/fonts/mononoki-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NodeSecure/scanner/97a36b523aa9b22900cd4ad822aa6a083e254121/workspaces/i18n/public/fonts/mononoki-Regular.eot -------------------------------------------------------------------------------- /workspaces/i18n/public/fonts/mononoki-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NodeSecure/scanner/97a36b523aa9b22900cd4ad822aa6a083e254121/workspaces/i18n/public/fonts/mononoki-Regular.woff2 -------------------------------------------------------------------------------- /workspaces/i18n/public/img/devto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NodeSecure/scanner/97a36b523aa9b22900cd4ad822aa6a083e254121/workspaces/i18n/public/img/devto.png -------------------------------------------------------------------------------- /workspaces/i18n/public/img/flag-fr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NodeSecure/scanner/97a36b523aa9b22900cd4ad822aa6a083e254121/workspaces/i18n/public/img/flag-fr.png -------------------------------------------------------------------------------- /workspaces/i18n/public/img/flag-uk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NodeSecure/scanner/97a36b523aa9b22900cd4ad822aa6a083e254121/workspaces/i18n/public/img/flag-uk.png -------------------------------------------------------------------------------- /workspaces/i18n/public/img/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NodeSecure/scanner/97a36b523aa9b22900cd4ad822aa6a083e254121/workspaces/i18n/public/img/github.png -------------------------------------------------------------------------------- /workspaces/i18n/public/img/linkedin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NodeSecure/scanner/97a36b523aa9b22900cd4ad822aa6a083e254121/workspaces/i18n/public/img/linkedin.png -------------------------------------------------------------------------------- /workspaces/i18n/public/img/nodesecure-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NodeSecure/scanner/97a36b523aa9b22900cd4ad822aa6a083e254121/workspaces/i18n/public/img/nodesecure-logo.png -------------------------------------------------------------------------------- /workspaces/i18n/public/js/main.js: -------------------------------------------------------------------------------- 1 | const selectWrapper = document.getElementById("wrapper-select"); 2 | const selectLanguage = document.getElementById("language-select"); 3 | const tabsButtons = document.querySelectorAll(".table__tabs-item"); 4 | 5 | selectLanguage.addEventListener("change", handleLanguageChange); 6 | tabsButtons.forEach((tabButton) => tabButton.addEventListener("click", handleSwitchTabs)); 7 | 8 | function handleBodyLanguage(tBodies, lang) { 9 | const [englishTBody, frenchTBody] = tBodies; 10 | 11 | if (lang === "en") { 12 | frenchTBody.classList.remove("active"); 13 | englishTBody.classList.add("active"); 14 | } 15 | else { 16 | englishTBody.classList.remove("active"); 17 | frenchTBody.classList.add("active"); 18 | } 19 | } 20 | 21 | function handleLanguageChange(event) { 22 | const currentPan = document.querySelector("table.active"); 23 | 24 | selectWrapper.className = `select__wrapper ${event.target.value}`; 25 | 26 | handleBodyLanguage(currentPan.tBodies, event.target.value); 27 | } 28 | 29 | function handleSwitchTabs(event) { 30 | const currentTab = event.target; 31 | const activeTab = document.querySelector(".table__tabs-item.active"); 32 | const activePan = document.getElementById(activeTab.attributes["aria-controls"].value); 33 | const currentPan = document.getElementById(currentTab.attributes["aria-controls"].value); 34 | 35 | if (currentTab === activeTab) { 36 | return; 37 | } 38 | 39 | activeTab.classList.remove("active"); 40 | activeTab.ariaSelected = false; 41 | currentTab.classList.add("active"); 42 | currentTab.ariaSelected = true; 43 | 44 | activePan.classList.remove("active"); 45 | currentPan.classList.add("active"); 46 | 47 | handleBodyLanguage(currentPan.tBodies, selectLanguage.value); 48 | } 49 | -------------------------------------------------------------------------------- /workspaces/i18n/scripts/buildDocumentation.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import path from "node:path"; 3 | import fs from "node:fs"; 4 | import os from "node:os"; 5 | import url from "node:url"; 6 | import { fileURLToPath } from "node:url"; 7 | 8 | // Import Third-party Dependencies 9 | import zup from "zup"; 10 | 11 | // Import Internal Dependencies 12 | import { english } from "../src/languages/english.js"; 13 | import { french } from "../src/languages/french.js"; 14 | 15 | // CONSTANTS 16 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 17 | const kProjectRootDir = path.join(__dirname, ".."); 18 | const kTokens = { 19 | english, 20 | french 21 | }; 22 | const kExternalI18nRepos = { 23 | scanner: "workspaces/scanner/src/i18n", 24 | cli: "i18n" 25 | }; 26 | const kTaggedStringPath = url.pathToFileURL(path.join(import.meta.dirname, "../src/utils.js")); 27 | 28 | function flatten( 29 | obj: any, 30 | roots: string[] = [], 31 | sep = "." 32 | ) { 33 | return Object 34 | .keys(obj) 35 | .reduce( 36 | (memo, prop) => Object.assign( 37 | {}, 38 | memo, 39 | Object.prototype.toString.call(obj[prop]) === "[object Object]" 40 | ? flatten(obj[prop], roots.concat([prop]), sep) 41 | : { [roots.concat([prop]).join(sep)]: obj[prop] } 42 | ), 43 | {} 44 | ); 45 | } 46 | 47 | function formatValue( 48 | key: string, 49 | obj: any 50 | ): string { 51 | const placeholders = Array.from({ length: 9 }, (_value, index) => `{${index}}`); 52 | 53 | return typeof obj[key] === "function" ? obj[key](...placeholders) : obj[key]; 54 | } 55 | 56 | for (const [repo, i18nPath] of Object.entries(kExternalI18nRepos)) { 57 | const frReq = await fetch(`https://raw.githubusercontent.com/NodeSecure/${repo}/refs/heads/master/${i18nPath}/french.js`); 58 | const frRaw = await frReq.text(); 59 | const enReq = await fetch(`https://raw.githubusercontent.com/NodeSecure/${repo}/refs/heads/master/${i18nPath}/english.js`); 60 | const enRaw = await enReq.text(); 61 | 62 | const tmpPathFr = path.join(os.tmpdir(), `fr-${repo}`); 63 | const tmpPathEn = path.join(os.tmpdir(), `en-${repo}`); 64 | fs.writeFileSync(tmpPathFr, frRaw.replace(`from "@nodesecure/i18n";`, `from "${kTaggedStringPath}"`)); 65 | fs.writeFileSync(tmpPathEn, enRaw.replace(`from "@nodesecure/i18n";`, `from "${kTaggedStringPath}"`)); 66 | 67 | const { default: fr } = await import(url.pathToFileURL(tmpPathFr).href); 68 | const { default: en } = await import(url.pathToFileURL(tmpPathEn).href); 69 | 70 | Object.assign(kTokens.french, { [repo]: fr[repo] }); 71 | Object.assign(kTokens.english, { [repo]: en[repo] }); 72 | } 73 | 74 | const HTMLStr = fs.readFileSync(path.join(kProjectRootDir, "views", "index.html"), "utf-8"); 75 | const templateStr = zup(HTMLStr)({ 76 | template: (obj, language) => flatten(kTokens[language][obj]), 77 | printKey: (key: string) => key, 78 | printValue: (key: string, obj: any) => formatValue(key, obj) 79 | }); 80 | 81 | fs.writeFileSync( 82 | path.join(kProjectRootDir, "index.html"), 83 | templateStr 84 | ); 85 | -------------------------------------------------------------------------------- /workspaces/i18n/src/constants.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import os from "node:os"; 3 | import path from "node:path"; 4 | 5 | export type Languages = "french" | "english" | (string & {}); 6 | 7 | export const CACHE_PATH = path.join(os.tmpdir(), "nsecure-cli"); 8 | export const CURRENT_LANG: Languages = "english"; 9 | -------------------------------------------------------------------------------- /workspaces/i18n/src/languages/english.ts: -------------------------------------------------------------------------------- 1 | // Require Internal Dependencies 2 | import { taggedString as tS } from "../utils.js"; 3 | 4 | const lang = "en"; 5 | 6 | const depWalker = { 7 | dep_tree: "dependency tree", 8 | fetch_and_walk_deps: "Fetching and walking through all dependencies...", 9 | fetch_on_registry: "Waiting for packages to fetch from npm registry...", 10 | waiting_tarball: "Waiting tarballs to be analyzed...", 11 | fetch_metadata: "Fetched package metadata:", 12 | analyzed_tarball: "Analyzed npm tarballs:", 13 | success_fetch_deptree: tS`Successfully navigated through the ${0} in ${1}`, 14 | success_tarball: tS`Successfully analyzed ${0} packages tarballs in ${1}`, 15 | success_registry_metadata: "Successfully fetched required metadata for all packages!", 16 | failed_rmdir: tS`Failed to remove directory ${0}!` 17 | }; 18 | 19 | const warnings = { 20 | disable_scarf: "This dependency could collect data against your will so think to disable it with the env var: SCARF_ANALYTICS", 21 | keylogging: "This dependency can retrieve your keyboard and mouse inputs. It can be used for 'keylogging' attacks/malwares." 22 | }; 23 | 24 | const sast_warnings = { 25 | parsing_error: "An error occured when parsing the JavaScript code with meriyah. It mean that the conversion from string to AST has failed. If you encounter such an error, please open an issue here.", 26 | unsafe_import: "Unable to follow an import (require, require.resolve) statement/expr.", 27 | unsafe_regex: "A RegEx as been detected as unsafe and may be used for a ReDoS Attack.", 28 | unsafe_stmt: "Usage of dangerous statement like eval() or Function(\"\").", 29 | unsafe_assign: "Assignment of a protected global like process or require.", 30 | encoded_literal: "An encoded literal has been detected (it can be an hexa value, unicode sequence, base64 string etc)", 31 | suspicious_file: "A suspicious file with more than ten encoded-literal in it.", 32 | short_identifiers: "This mean that all identifiers has an average length below 1.5. Only possible if the file contains more than 5 identifiers.", 33 | suspicious_literal: "This mean that the sum of suspicious score of all Literals is bigger than 3.", 34 | obfuscated_code: "There's a very high probability that the code is obfuscated...", 35 | weak_crypto: "The code probably contains a weak crypto algorithm (md5, sha1...)", 36 | shady_link: "A Literal (string) contains an URL to a domain with a suspicious extension.", 37 | zeroSemVer: "Semantic version starting with 0.x (unstable project or without serious versioning)" 38 | }; 39 | 40 | export const english = { lang, depWalker, warnings, sast_warnings }; 41 | -------------------------------------------------------------------------------- /workspaces/i18n/src/languages/french.ts: -------------------------------------------------------------------------------- 1 | // Import Internal Dependencies 2 | import { taggedString as tS } from "../utils.js"; 3 | 4 | const lang = "fr"; 5 | 6 | const depWalker = { 7 | dep_tree: "arbre de dépendances", 8 | fetch_and_walk_deps: "Importation et analyse de l'intégralité des dépendances...", 9 | fetch_on_registry: "En attente de l'importation des packages du registre npm...", 10 | waiting_tarball: "En attente de l'analyse des tarballs...", 11 | fetch_metadata: "Metadonnées importées :", 12 | analyzed_tarball: "Tarballs en cours d'analyse :", 13 | success_fetch_deptree: tS`Analyse de l'${0} terminée avec succès en ${1}`, 14 | success_tarball: tS`${0} tarballs analysés avec succès en ${1}`, 15 | success_registry_metadata: "Metadonnées requises pour tous les packages importées avec succès !", 16 | failed_rmdir: tS`Suppression du dossier ${0} échouée !` 17 | }; 18 | 19 | const warnings = { 20 | disable_scarf: "Cette dépendance peut récolter des données contre votre volonté, pensez donc à la désactiver en fournissant la variable d'environnement SCARF_ANALYTICS", 21 | keylogging: "Cette dépendance peut obtenir vos entrées clavier ou de souris. Cette dépendance peut être utilisée en tant que 'keylogging' attacks/malwares." 22 | }; 23 | 24 | const sast_warnings = { 25 | parsing_error: `Une erreur s'est produite lors de l'analyse du code JavaScript avec meriyah. 26 | Cela signifie que la conversion de la chaîne de caractères AST a échoué. 27 | Si vous rencontrez une telle erreur, veuillez ouvrir une issue.`, 28 | unsafe_import: "Impossible de suivre l'import (require, require.resolve) statement/expr.", 29 | unsafe_regex: "Un RegEx a été détecté comme non sûr et peut être utilisé pour une attaque ReDoS.", 30 | unsafe_stmt: "Utilisation d'instructions dangereuses comme eval() ou Function(\"\").", 31 | unsafe_assign: "Attribution d'un processus ou d'un require global protégé..", 32 | encoded_literal: "Un code littérale a été découvert (il peut s'agir d'une valeur hexa, d'une séquence unicode, d'une chaîne de caractères base64, etc.)", 33 | short_identifiers: "Cela signifie que tous les identifiants ont une longueur moyenne inférieure à 1,5. Seulement possible si le fichier contient plus de 5 identifiants.", 34 | suspicious_literal: "Cela signifie que la somme des scores suspects de tous les littéraux est supérieure à 3.", 35 | suspicious_file: "Un fichier suspect contenant plus de dix chaines de caractères encodés", 36 | obfuscated_code: "Il y a une très forte probabilité que le code soit obscurci...", 37 | weak_crypto: "Le code contient probablement un algorithme de chiffrement faiblement sécurisé (md5, sha1...).", 38 | shady_link: "Un Literal (string) contient une URL vers un domaine avec une extension suspecte.", 39 | zeroSemVer: "Version sémantique commençant par 0.x (projet instable ou sans versionnement sérieux)" 40 | }; 41 | 42 | export const french = { lang, depWalker, warnings, sast_warnings }; 43 | -------------------------------------------------------------------------------- /workspaces/i18n/src/languages/index.ts: -------------------------------------------------------------------------------- 1 | import { english } from "./english.js"; 2 | import { french } from "./french.js"; 3 | 4 | export const languages: Record = { 5 | english, 6 | french 7 | }; 8 | -------------------------------------------------------------------------------- /workspaces/i18n/src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @function taggedString 3 | * @memberof utils# 4 | * @description Create a tagged String 5 | * @param {TemplateStringsArray} chaines initial string 6 | * @param {string[] | number[]} cles keys 7 | * @returns {Function} Return clojure function to build the final string 8 | * 9 | * @example 10 | * const { taggedString } = require("@myunisoft/utils"); 11 | * 12 | * const myStrClojure = taggedString`Hello ${0}!`; 13 | * console.log(myStrClojure("Thomas")); // stdout: Hello Thomas! 14 | */ 15 | export function taggedString( 16 | chaines: TemplateStringsArray, 17 | ...cles: string[] | number[] 18 | ) { 19 | return function cur(...valeurs: any[]): string { 20 | const dict = valeurs[valeurs.length - 1] || {}; 21 | const resultat = [chaines[0]]; 22 | cles.forEach((cle, index) => { 23 | resultat.push( 24 | typeof cle === "number" ? valeurs[cle] : dict[cle], 25 | chaines[index + 1] 26 | ); 27 | }); 28 | 29 | return resultat.join(""); 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /workspaces/i18n/test/utils.spec.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Internal Dependencies 6 | import { taggedString } from "../src/utils.js"; 7 | 8 | test("taggedString", () => { 9 | const clojureHello = taggedString`Hello ${0}`; 10 | assert.strictEqual(clojureHello(), "Hello "); 11 | assert.strictEqual(clojureHello("world"), "Hello world"); 12 | 13 | const clojureFoo = taggedString`Hello ${"word"}`; 14 | assert.strictEqual(clojureFoo({ word: "bar" }), "Hello bar"); 15 | }); 16 | -------------------------------------------------------------------------------- /workspaces/i18n/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | }, 7 | "include": [ 8 | "src" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /workspaces/mama/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @nodesecure/mama 2 | 3 | ## 1.2.0 4 | 5 | ### Minor Changes 6 | 7 | - [#397](https://github.com/NodeSecure/scanner/pull/397) [`3ee9a2e`](https://github.com/NodeSecure/scanner/commit/3ee9a2e17c877e7ea6fe23fc4ffc86578e6d0b72) Thanks [@fraxken](https://github.com/fraxken)! - implement Manifest module type detection (with cjs, esm, dual, faux esm and dts) 8 | -------------------------------------------------------------------------------- /workspaces/mama/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nodesecure/mama", 3 | "version": "1.2.0", 4 | "description": "Manifest Manager", 5 | "type": "module", 6 | "exports": "./dist/index.js", 7 | "types": "./dist/index.d.ts", 8 | "scripts": { 9 | "build": "tsc -b", 10 | "prepublishOnly": "npm run build", 11 | "test-only": "tsx --test ./test/**/*.spec.ts", 12 | "test": "c8 -r html npm run test-only" 13 | }, 14 | "files": [ 15 | "dist" 16 | ], 17 | "keywords": [ 18 | "manifest", 19 | "manager", 20 | "pacote", 21 | "security" 22 | ], 23 | "author": "GENTILHOMME Thomas ", 24 | "license": "MIT", 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/NodeSecure/scanner.git" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/NodeSecure/scanner/issues" 31 | }, 32 | "homepage": "https://github.com/NodeSecure/tree/master/workspaces/mama#readme", 33 | "dependencies": { 34 | "@nodesecure/npm-types": "^1.2.0", 35 | "object-hash": "^3.0.0" 36 | }, 37 | "devDependencies": { 38 | "@types/object-hash": "^3.0.6" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /workspaces/mama/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ManifestManager.class.js"; 2 | export { 3 | packageJSONIntegrityHash, 4 | inspectModuleType, 5 | type PackageModuleType 6 | } from "./utils/index.js"; 7 | -------------------------------------------------------------------------------- /workspaces/mama/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./integrity-hash.js"; 2 | export * from "./inspectModuleType.js"; 3 | -------------------------------------------------------------------------------- /workspaces/mama/src/utils/integrity-hash.ts: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import hash from "object-hash"; 3 | import type { 4 | PackumentVersion, PackageJSON, WorkspacesPackageJSON 5 | } from "@nodesecure/npm-types"; 6 | 7 | export interface packageJSONIntegrityHashOptions { 8 | /** 9 | * Know whether the document comes from the NPM registry or a local tarball/project 10 | * 11 | * @default false 12 | */ 13 | isFromRemoteRegistry?: boolean; 14 | } 15 | 16 | export function packageJSONIntegrityHash( 17 | document: PackumentVersion | PackageJSON | WorkspacesPackageJSON, 18 | options: packageJSONIntegrityHashOptions = {} 19 | ) { 20 | const { isFromRemoteRegistry = false } = options; 21 | const { dependencies = {}, license = "NONE", scripts = {} } = document; 22 | 23 | if (isFromRemoteRegistry) { 24 | // See https://github.com/npm/cli/issues/5234 25 | if ("install" in dependencies && dependencies.install === "node-gyp rebuild") { 26 | delete dependencies.install; 27 | } 28 | } 29 | 30 | return hash({ 31 | name: document.name, 32 | version: document.version, 33 | dependencies, 34 | license, 35 | scripts 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /workspaces/mama/test/packageJSONIntegrityHash.spec.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import assert from "node:assert"; 3 | import { describe, test } from "node:test"; 4 | 5 | // Import Third-party Dependencies 6 | import hash from "object-hash"; 7 | 8 | // Import Internal Dependencies 9 | import { packageJSONIntegrityHash } from "../src/utils/index.js"; 10 | 11 | // CONSTANTS 12 | const kMinimalPackageJSON = { 13 | name: "foo", 14 | version: "1.5.0" 15 | }; 16 | const kMinimalPackageJSONIntegrity = hash({ 17 | ...kMinimalPackageJSON, 18 | dependencies: {}, 19 | scripts: {}, 20 | license: "NONE" 21 | }); 22 | 23 | describe("packageJSONIntegrityHash", () => { 24 | test("Given isFromRemoteRegistry: true then it should remove install script if it contains 'node-gyp rebuild'", () => { 25 | const integrityHash = packageJSONIntegrityHash({ 26 | ...kMinimalPackageJSON, 27 | dependencies: { 28 | install: "node-gyp rebuild" 29 | } 30 | }, { isFromRemoteRegistry: true }); 31 | 32 | assert.strictEqual( 33 | integrityHash, 34 | kMinimalPackageJSONIntegrity 35 | ); 36 | }); 37 | 38 | test("Given isFromRemoteRegistry: false then the integrity should not match", () => { 39 | const integrityHash = packageJSONIntegrityHash({ 40 | ...kMinimalPackageJSON, 41 | dependencies: { 42 | install: "node-gyp rebuild" 43 | } 44 | }); 45 | 46 | assert.notStrictEqual( 47 | integrityHash, 48 | kMinimalPackageJSONIntegrity 49 | ); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /workspaces/mama/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | }, 7 | "include": ["src"], 8 | "references": [ 9 | { 10 | "path": "../npm-types" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /workspaces/npm-types/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @nodesecure/npm-types 2 | 3 | ## 1.2.1 4 | 5 | ### Patch Changes 6 | 7 | - [#393](https://github.com/NodeSecure/scanner/pull/393) [`0e37b73`](https://github.com/NodeSecure/scanner/commit/0e37b734181f737f20216fce5f7495460b5abb6b) Thanks [@fraxken](https://github.com/fraxken)! - fix package.json Node.js type by replacing literal value script with commonjs 8 | -------------------------------------------------------------------------------- /workspaces/npm-types/README.md: -------------------------------------------------------------------------------- 1 |

2 | @nodesecure/npm-types 3 |

4 | 5 |

6 | Up to date typescript definitions for npm registry content 7 |

8 | 9 | ## Requirements 10 | - [Node.js](https://nodejs.org/en/) v20 or higher 11 | 12 | ## Getting Started 13 | 14 | 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). 15 | 16 | ```bash 17 | $ npm i @nodesecure/npm-types -D 18 | # or 19 | $ yarn add @nodesecure/npm-types -D 20 | ``` 21 | 22 | ## Usage example 23 | 24 | ```ts 25 | import type { PackageJSON } from "@nodesecure/npm-types"; 26 | ``` 27 | -------------------------------------------------------------------------------- /workspaces/npm-types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nodesecure/npm-types", 3 | "version": "1.2.1", 4 | "description": "Up to date typescript definitions for npm registry content", 5 | "types": "./src/index.d.ts", 6 | "exports": "./src/index.d.ts", 7 | "files": [ 8 | "src" 9 | ], 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/NodeSecure/scanner.git" 13 | }, 14 | "keywords": [ 15 | "npm registry", 16 | "types", 17 | "typescript", 18 | "definitions", 19 | "typings" 20 | ], 21 | "author": "NodeSecure", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/NodeSecure/scanner/issues" 25 | }, 26 | "homepage": "https://github.com/NodeSecure/tree/master/workspaces/npm-types#readme", 27 | "private": false 28 | } 29 | -------------------------------------------------------------------------------- /workspaces/npm-types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json" 3 | } 4 | -------------------------------------------------------------------------------- /workspaces/rc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nodesecure/rc", 3 | "version": "4.1.0", 4 | "description": "NodeSecure runtime configuration", 5 | "type": "module", 6 | "main": "./dist/index.js", 7 | "types": "./dist/index.d.ts", 8 | "engines": { 9 | "node": ">=20" 10 | }, 11 | "scripts": { 12 | "build": "tsc", 13 | "prepublishOnly": "npm run build", 14 | "test-only": "tsx --test ./test/**/*.spec.ts", 15 | "test:tsd": "npm run build && tsd", 16 | "test": "c8 -r html npm run test-only && npm run test:tsd" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/NodeSecure/scanner.git" 21 | }, 22 | "files": [ 23 | "dist" 24 | ], 25 | "keywords": [ 26 | "rc", 27 | "config", 28 | "configuration" 29 | ], 30 | "author": "GENTILHOMME Thomas ", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/NodeSecure/scanner/issues" 34 | }, 35 | "homepage": "https://github.com/NodeSecure/tree/master/workspaces/rc#readme", 36 | "devDependencies": { 37 | "@types/lodash.merge": "^4.6.7", 38 | "@types/zen-observable": "^0.8.4", 39 | "ajv": "^8.12.0" 40 | }, 41 | "dependencies": { 42 | "@nodesecure/js-x-ray": "^8.1.0", 43 | "@nodesecure/npm-types": "^1.2.0", 44 | "@nodesecure/vulnera": "^2.0.1", 45 | "@openally/config": "^1.0.1", 46 | "@openally/result": "^1.2.1", 47 | "lodash.merge": "^4.6.2", 48 | "type-fest": "^4.41.0" 49 | }, 50 | "tsd": { 51 | "directory": "test/types" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /workspaces/rc/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const CONFIGURATION_NAME = ".nodesecurerc"; 2 | export const GLOBAL_CONFIGURATION_DIRECTORY = "nodesecure"; 3 | -------------------------------------------------------------------------------- /workspaces/rc/src/functions/memoize.ts: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import merge from "lodash.merge"; 3 | import { Some, None, type Option } from "@openally/result"; 4 | 5 | // Import Internal Dependencies 6 | import type { RC } from "../rc.js"; 7 | 8 | let memoizedValue: Partial | null = null; 9 | 10 | export interface memoizeOptions { 11 | /** 12 | * If enabled it will overwrite (crush) the previous memoized RC 13 | * @default true 14 | */ 15 | overwrite?: boolean; 16 | } 17 | 18 | export function memoize( 19 | payload: Partial, 20 | options: memoizeOptions = {} 21 | ): void { 22 | const { overwrite = true } = options; 23 | 24 | if (memoizedValue === null || overwrite) { 25 | memoizedValue = payload; 26 | } 27 | else { 28 | memoizedValue = merge({}, memoizedValue, payload); 29 | } 30 | } 31 | 32 | export interface memoizedOptions { 33 | defaultValue: Partial; 34 | } 35 | 36 | export function memoized( 37 | options?: memoizedOptions 38 | ): Partial | null { 39 | const { defaultValue = null } = options ?? {}; 40 | 41 | return memoizedValue ?? defaultValue; 42 | } 43 | 44 | export function maybeMemoized(): Option> { 45 | return memoizedValue === null ? 46 | None : 47 | Some(memoizedValue); 48 | } 49 | 50 | export function clearMemoized(): void { 51 | memoizedValue = null; 52 | } 53 | 54 | -------------------------------------------------------------------------------- /workspaces/rc/src/functions/read.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import path from "node:path"; 3 | import { once } from "node:events"; 4 | 5 | // Import Third-party Dependencies 6 | import { AsynchronousConfig } from "@openally/config"; 7 | import { Ok, Err, Result } from "@openally/result"; 8 | import type { RequireAtLeastOne } from "type-fest"; 9 | 10 | // Import Internal Dependencies 11 | import { 12 | JSONSchema, 13 | generateDefaultRC, 14 | type RCGenerationMode, 15 | type RC 16 | } from "../rc.js"; 17 | import * as CONSTANTS from "../constants.js"; 18 | import { memoize } from "./memoize.js"; 19 | 20 | interface createReadOptions { 21 | /** 22 | * If enabled the file will be created if it does not exist on the disk. 23 | * 24 | * @default false 25 | */ 26 | createIfDoesNotExist?: boolean; 27 | /** 28 | * RC Generation mode. This option allows to generate a more or less complete configuration for some NodeSecure tools. 29 | * 30 | * @default `minimal` 31 | */ 32 | createMode?: RCGenerationMode | RCGenerationMode[]; 33 | 34 | /** 35 | * RC automatic caching option. This option allows to cache a configuration passed in parameter. 36 | * 37 | * @default false 38 | */ 39 | memoize?: boolean; 40 | } 41 | 42 | export type readOptions = RequireAtLeastOne; 43 | 44 | export async function read( 45 | location = process.cwd(), 46 | options: readOptions = Object.create(null) 47 | ): Promise> { 48 | try { 49 | const { createIfDoesNotExist = Boolean(options.createMode), createMode, memoize: memoizeRc = false } = options; 50 | 51 | const cfgPath = path.join(location, CONSTANTS.CONFIGURATION_NAME); 52 | const cfg = new AsynchronousConfig(cfgPath, { 53 | jsonSchema: JSONSchema, 54 | createOnNoEntry: createIfDoesNotExist 55 | }); 56 | 57 | await cfg.read(createIfDoesNotExist ? generateDefaultRC(createMode) : void 0); 58 | if (createIfDoesNotExist) { 59 | await once(cfg, "configWritten"); 60 | } 61 | const result = cfg.payload; 62 | 63 | if (memoizeRc) { 64 | memoize(result); 65 | } 66 | 67 | await cfg.close(); 68 | 69 | return Ok(result); 70 | } 71 | catch (error) { 72 | return Err(error as NodeJS.ErrnoException); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /workspaces/rc/src/functions/write.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import path from "node:path"; 3 | 4 | // Import Third-party Dependencies 5 | import { AsynchronousConfig } from "@openally/config"; 6 | import { Ok, Err, Result } from "@openally/result"; 7 | 8 | // Import Internal Dependencies 9 | import { JSONSchema, type RC } from "../rc.js"; 10 | import * as CONSTANTS from "../constants.js"; 11 | /** 12 | * Overwrite the complete payload. partialUpdate property is mandatory. 13 | */ 14 | export interface writeCompletePayload { 15 | payload: RC; 16 | partialUpdate?: false; 17 | } 18 | 19 | /** 20 | * Partially update the payload. This implies not to rewrite the content of the file when enabled. 21 | **/ 22 | export interface writePartialPayload { 23 | payload: Partial; 24 | partialUpdate: true; 25 | } 26 | 27 | export type writeOptions = writeCompletePayload | writePartialPayload; 28 | 29 | export async function write( 30 | location: string, 31 | options: writeOptions 32 | ): Promise> { 33 | try { 34 | const { payload, partialUpdate = false } = options; 35 | 36 | const cfgPath = path.join(location, CONSTANTS.CONFIGURATION_NAME); 37 | const cfg = new AsynchronousConfig(cfgPath, { 38 | jsonSchema: JSONSchema 39 | }); 40 | await cfg.read(); 41 | 42 | const newPayloadValue = partialUpdate ? Object.assign(cfg.payload, payload) : payload as RC; 43 | cfg.payload = newPayloadValue; 44 | 45 | await cfg.close(); 46 | 47 | return Ok(void 0); 48 | } 49 | catch (error) { 50 | return Err(error as NodeJS.ErrnoException); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /workspaces/rc/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./functions/read.js"; 2 | export * from "./functions/write.js"; 3 | export * from "./functions/memoize.js"; 4 | export * as CONSTANTS from "./constants.js"; 5 | 6 | export { 7 | type RC, 8 | type CiConfiguration, 9 | type CiWarnings, 10 | type ReportConfiguration, 11 | type ReportChart, 12 | type ScannerConfiguration, 13 | JSONSchema, 14 | homedir 15 | } from "./rc.js"; 16 | -------------------------------------------------------------------------------- /workspaces/rc/src/projects/ci.ts: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import type { WarningName } from "@nodesecure/js-x-ray"; 3 | 4 | /** 5 | * Configuration dedicated for NodeSecure CI (or nsci) 6 | * @see https://github.com/NodeSecure/ci 7 | * @see https://github.com/NodeSecure/ci-action 8 | */ 9 | export interface CiConfiguration { 10 | /** 11 | * List of enabled reporters 12 | * @see https://github.com/NodeSecure/ci#reporters 13 | */ 14 | reporters?: ("console" | "html")[]; 15 | vulnerabilities?: { 16 | severity?: "medium" | "high" | "critical" | "all"; 17 | }; 18 | /** 19 | * JS-X-Ray warnings configuration 20 | * @see https://github.com/NodeSecure/js-x-ray#warnings-legends-v20 21 | */ 22 | warnings?: CiWarnings | Record; 23 | } 24 | export type CiWarnings = "off" | "error" | "warning"; 25 | 26 | export function generateCIConfiguration(): { ci: CiConfiguration; } { 27 | const ci: CiConfiguration = { 28 | reporters: ["console"], 29 | vulnerabilities: { 30 | severity: "medium" 31 | }, 32 | warnings: "error" 33 | }; 34 | 35 | return { ci }; 36 | } 37 | -------------------------------------------------------------------------------- /workspaces/rc/src/projects/report.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Configuration dedicated for NodeSecure Report 4 | * @see https://github.com/NodeSecure/report 5 | */ 6 | export interface ReportConfiguration { 7 | /** 8 | * @default `light` 9 | */ 10 | theme?: "light" | "dark"; 11 | title: string; 12 | /** 13 | * URL to a logo to show on the final HTML/PDF Report 14 | */ 15 | logoUrl?: string; 16 | /** 17 | * Show/categorize internal dependencies as transitive 18 | * @default false 19 | */ 20 | includeTransitiveInternal?: boolean; 21 | npm?: { 22 | /** 23 | * NPM organization prefix starting with @ 24 | * @example `@nodesecure` 25 | */ 26 | organizationPrefix: string; 27 | packages: string[]; 28 | }; 29 | git?: { 30 | /** 31 | * GitHub organization URL 32 | * @example `https://github.com/NodeSecure` 33 | */ 34 | organizationUrl: string; 35 | /** 36 | * List of repositories (name are enough, no need to provide .git url or any equivalent) 37 | */ 38 | repositories: string[]; 39 | }; 40 | /** 41 | * @default html,pdf 42 | */ 43 | reporters?: ("html" | "pdf")[]; 44 | /** 45 | * Show/Hide flags emojis next to packages. 46 | * @default true 47 | */ 48 | showFlags?: boolean; 49 | charts?: ReportChart[]; 50 | } 51 | 52 | export interface ReportChart { 53 | /** 54 | * List of available charts. 55 | */ 56 | name: "Extensions" | "Licenses" | "Warnings" | "Flags"; 57 | /** 58 | * @default true 59 | */ 60 | display?: boolean; 61 | /** 62 | * Chart.js chart type. 63 | * 64 | * @see https://www.chartjs.org/docs/latest/charts 65 | * @default `bar` 66 | */ 67 | type?: "bar" | "horizontalBar" | "polarArea" | "doughnut"; 68 | /** 69 | * D3 Interpolation color. Will be picked randomly by default if not provided. 70 | * @see https://github.com/d3/d3-scale-chromatic/blob/main/README.md 71 | */ 72 | interpolation?: string; 73 | } 74 | 75 | export function generateReportConfiguration(): { report: Partial; } { 76 | const report: Partial = { 77 | theme: "light" as const, 78 | includeTransitiveInternal: false, 79 | reporters: ["html", "pdf"], 80 | charts: [ 81 | { 82 | name: "Extensions" as const, 83 | display: true, 84 | interpolation: "d3.interpolateRainbow" 85 | }, 86 | { 87 | name: "Licenses" as const, 88 | display: true, 89 | interpolation: "d3.interpolateCool" 90 | }, 91 | { 92 | name: "Warnings" as const, 93 | display: true, 94 | type: "horizontalBar" as const, 95 | interpolation: "d3.interpolateInferno" 96 | }, 97 | { 98 | name: "Flags" as const, 99 | display: true, 100 | type: "horizontalBar" as const, 101 | interpolation: "d3.interpolateSinebow" 102 | } 103 | ] 104 | }; 105 | 106 | return { report }; 107 | } 108 | -------------------------------------------------------------------------------- /workspaces/rc/src/projects/scanner.ts: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import type { Contact } from "@nodesecure/npm-types"; 3 | 4 | /** 5 | * Configuration dedicated for NodeSecure scanner 6 | * @see https://github.com/NodeSecure/scanner 7 | */ 8 | export interface ScannerConfiguration { 9 | highlight?: { 10 | contacts: Contact[]; 11 | }; 12 | } 13 | 14 | export function generateScannerConfiguration(): { scanner: ScannerConfiguration; } { 15 | const scanner: ScannerConfiguration = { 16 | highlight: { 17 | contacts: [] 18 | } 19 | }; 20 | 21 | return { scanner }; 22 | } 23 | -------------------------------------------------------------------------------- /workspaces/rc/src/rc.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import os from "node:os"; 3 | import path from "node:path"; 4 | 5 | // Import Third-party Dependencies 6 | import * as vulnera from "@nodesecure/vulnera"; 7 | 8 | // Import Internal Dependencies 9 | import { GLOBAL_CONFIGURATION_DIRECTORY } from "./constants.js"; 10 | import { loadJSONSchemaSync } from "./schema/loader.js"; 11 | 12 | import { 13 | generateCIConfiguration, 14 | type CiConfiguration, 15 | type CiWarnings 16 | } from "./projects/ci.js"; 17 | import { 18 | generateReportConfiguration, 19 | type ReportConfiguration, 20 | type ReportChart 21 | } from "./projects/report.js"; 22 | import { 23 | generateScannerConfiguration, 24 | type ScannerConfiguration 25 | } from "./projects/scanner.js"; 26 | 27 | // CONSTANTS 28 | // eslint-disable-next-line @openally/constants 29 | export const JSONSchema = loadJSONSchemaSync(); 30 | 31 | export interface RC { 32 | /** version of the rc package used to generate the nodesecurerc file */ 33 | version: string; 34 | /** 35 | * Language to use for i18n (translation in NodeSecure tools). 36 | * @see https://developer.mozilla.org/en-US/docs/Glossary/I18N 37 | * @see https://github.com/NodeSecure/i18n 38 | * 39 | * @default `english` 40 | */ 41 | i18n?: "english" | "french"; 42 | /** 43 | * Vulnerability strategy to use. Can be disabled by using `none` as value. 44 | * @see https://github.com/NodeSecure/vuln#available-strategy 45 | * 46 | * @default `github-advisory` 47 | */ 48 | strategy?: vulnera.Kind; 49 | /** 50 | * Package Registry (default to NPM public registry) 51 | * @default `https://registry.npmjs.org` 52 | */ 53 | registry?: string; 54 | /** NodeSecure scanner Object configuration */ 55 | scanner?: ScannerConfiguration; 56 | /** NodeSecure ci Object configuration */ 57 | ci?: CiConfiguration; 58 | /** NodeSecure report Object configuration */ 59 | report?: ReportConfiguration; 60 | } 61 | 62 | export type RCGenerationMode = "minimal" | "ci" | "report" | "scanner" | "complete"; 63 | 64 | /** 65 | * @example 66 | * generateDefaultRC("complete"); 67 | * generateDefaultRC(["ci", "report"]); // minimal + ci + report 68 | */ 69 | export function generateDefaultRC( 70 | mode: RCGenerationMode | RCGenerationMode[] = "minimal" 71 | ): RC { 72 | const modes = new Set(typeof mode === "string" ? [mode] : mode); 73 | 74 | const minimalRC = { 75 | version: "1.0.0", 76 | i18n: "english" as const, 77 | strategy: "github-advisory" as const, 78 | registry: "https://registry.npmjs.org" 79 | }; 80 | const complete = modes.has("complete"); 81 | 82 | return Object.assign( 83 | minimalRC, 84 | complete || modes.has("ci") ? generateCIConfiguration() : {}, 85 | complete || modes.has("report") ? generateReportConfiguration() : {}, 86 | complete || modes.has("scanner") ? generateScannerConfiguration() : {} 87 | ); 88 | } 89 | 90 | /** 91 | * Dedicated directory for NodeSecure to store the configuration in the os HOME directory. 92 | */ 93 | export function homedir(): string { 94 | return path.join(os.homedir(), GLOBAL_CONFIGURATION_DIRECTORY); 95 | } 96 | 97 | export { 98 | generateCIConfiguration, 99 | type CiConfiguration, 100 | type CiWarnings, 101 | 102 | generateReportConfiguration, 103 | type ReportConfiguration, 104 | type ReportChart, 105 | 106 | generateScannerConfiguration, 107 | type ScannerConfiguration 108 | }; 109 | -------------------------------------------------------------------------------- /workspaces/rc/src/schema/defs/ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "reporters": { 5 | "type": "array", 6 | "uniqueItems": true, 7 | "items": { 8 | "type": "string", 9 | "enum": [ 10 | "html", 11 | "console" 12 | ] 13 | }, 14 | "default": [ 15 | "console" 16 | ] 17 | }, 18 | "vulnerabilities": { 19 | "type": "object", 20 | "properties": { 21 | "severity": { 22 | "type": "string", 23 | "enum": [ 24 | "medium", 25 | "high", 26 | "critical", 27 | "all" 28 | ], 29 | "default": "all" 30 | } 31 | }, 32 | "additionalProperties": false 33 | }, 34 | "warnings": { 35 | "default": "off", 36 | "description": "JS-X-Ray warnings configuration", 37 | "oneOf": [ 38 | { 39 | "$ref": "#/$defs/ciWarnings" 40 | }, 41 | { 42 | "type": "object", 43 | "minProperties": 1, 44 | "patternProperties": { 45 | "^[A-Za-z-]+$": { 46 | "$ref": "#/$defs/ciWarnings" 47 | } 48 | } 49 | } 50 | ] 51 | } 52 | }, 53 | "required": [ 54 | "reporters", 55 | "warnings" 56 | ], 57 | "additionalProperties": false 58 | } 59 | -------------------------------------------------------------------------------- /workspaces/rc/src/schema/defs/ciWarnings.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "string", 3 | "enum": [ 4 | "off", 5 | "error", 6 | "warning" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /workspaces/rc/src/schema/defs/contact.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "name": { 5 | "type": "string" 6 | }, 7 | "email": { 8 | "type": "string" 9 | }, 10 | "url": { 11 | "type": "string" 12 | } 13 | }, 14 | "required": ["name"], 15 | "additionalProperties": false 16 | } 17 | -------------------------------------------------------------------------------- /workspaces/rc/src/schema/defs/report.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Report configuration", 3 | "type": "object", 4 | "additionalProperties": false, 5 | "required": [ 6 | "title" 7 | ], 8 | "properties": { 9 | "theme": { 10 | "type": "string", 11 | "enum": [ 12 | "light", 13 | "dark" 14 | ], 15 | "default": "light" 16 | }, 17 | "title": { 18 | "type": "string", 19 | "description": "Report title", 20 | "default": "Default report title" 21 | }, 22 | "logoUrl": { 23 | "type": "string", 24 | "description": "Logo", 25 | "nullable": true 26 | }, 27 | "includeTransitiveInternal": { 28 | "type": "boolean", 29 | "default": false, 30 | "description": "Show/categorize internal dependencies as transitive" 31 | }, 32 | "npm": { 33 | "type": "object", 34 | "additionalProperties": false, 35 | "required": [ 36 | "organizationPrefix", 37 | "packages" 38 | ], 39 | "properties": { 40 | "organizationPrefix": { 41 | "type": "string", 42 | "description": "NPM organization prefix starting with @" 43 | }, 44 | "packages": { 45 | "type": "array", 46 | "items": { 47 | "type": "string" 48 | }, 49 | "uniqueItems": true 50 | } 51 | } 52 | }, 53 | "git": { 54 | "type": "object", 55 | "additionalProperties": false, 56 | "required": [ 57 | "organizationUrl", 58 | "repositories" 59 | ], 60 | "properties": { 61 | "organizationUrl": { 62 | "type": "string", 63 | "description": "GitHub organization URL" 64 | }, 65 | "repositories": { 66 | "type": "array", 67 | "description": "List of repositories (name are enough, no need to provide .git url or any equivalent)", 68 | "items": { 69 | "type": "string" 70 | }, 71 | "uniqueItems": true 72 | } 73 | } 74 | }, 75 | "reporters": { 76 | "type": "array", 77 | "uniqueItems": true, 78 | "items": { 79 | "type": "string", 80 | "enum": [ 81 | "html", 82 | "pdf" 83 | ] 84 | }, 85 | "default": [ 86 | "html", 87 | "pdf" 88 | ] 89 | }, 90 | "showFlags": { 91 | "type": "boolean", 92 | "default": true 93 | }, 94 | "charts": { 95 | "type": "array", 96 | "items": { 97 | "$ref": "#/$defs/reportChart" 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /workspaces/rc/src/schema/defs/reportChart.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "additionalProperties": false, 4 | "required": [ 5 | "name" 6 | ], 7 | "properties": { 8 | "name": { 9 | "type": "string", 10 | "enum": ["Extensions", "Licenses", "Warnings", "Flags"] 11 | }, 12 | "display": { 13 | "type": "boolean", 14 | "default": true 15 | }, 16 | "type": { 17 | "type": "string", 18 | "enum": ["bar", "horizontalBar", "polarArea", "doughnut"], 19 | "default": "bar", 20 | "description": "Chart.js chart type." 21 | }, 22 | "interpolation": { 23 | "type": "string", 24 | "description": "D3.js chromatic interpolation set of colors" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /workspaces/rc/src/schema/defs/scanner.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "highlight": { 5 | "type": "object", 6 | "properties": { 7 | "contacts": { 8 | "type": "array", 9 | "items": { 10 | "$ref": "#/$defs/contact" 11 | } 12 | } 13 | }, 14 | "required": ["contacts"], 15 | "additionalProperties": false 16 | } 17 | }, 18 | "additionalProperties": false 19 | } 20 | -------------------------------------------------------------------------------- /workspaces/rc/src/schema/loader.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { readdirSync } from "node:fs"; 3 | import path from "node:path"; 4 | 5 | // Import Internal Dependencies 6 | import { readJSONSync } from "../utils/index.js"; 7 | 8 | // CONSTANTS 9 | const kDefsDirectory = new URL("./defs", import.meta.url); 10 | 11 | function loadJSONSchemaDefinition($defs: Record, fileName: string) { 12 | const defName = path.basename(fileName, ".json"); 13 | const jsonSchema = readJSONSync(`./defs/${fileName}`, import.meta.url); 14 | 15 | return { ...$defs, [defName]: jsonSchema }; 16 | } 17 | 18 | export function loadJSONSchemaSync() { 19 | const mainSchema = readJSONSync("./nodesecurerc.json", import.meta.url); 20 | const $defs = readdirSync(kDefsDirectory) 21 | .filter((fileName) => path.extname(fileName) === ".json") 22 | .reduce(loadJSONSchemaDefinition, {}); 23 | 24 | return Object.assign(mainSchema, { $defs }); 25 | } 26 | -------------------------------------------------------------------------------- /workspaces/rc/src/schema/nodesecurerc.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "version": { 5 | "type": "string", 6 | "description": "version of the rc package used to generate the nodesecurerc file" 7 | }, 8 | "i18n": { 9 | "type": "string", 10 | "enum": [ 11 | "french", 12 | "english" 13 | ], 14 | "default": "english", 15 | "description": "Language to use for i18n" 16 | }, 17 | "strategy": { 18 | "type": "string", 19 | "enum": [ 20 | "github-advisory", 21 | "sonatype", 22 | "snyk", 23 | "none" 24 | ], 25 | "default": "github-advisory", 26 | "description": "Vulnerability strategy to use" 27 | }, 28 | "registry": { 29 | "type": "string", 30 | "description": "Package Registry (default to NPM public registry)", 31 | "pattern": "^(https?|http?)://", 32 | "default": "https://registry.npmjs.org" 33 | }, 34 | "scanner": { 35 | "$ref": "#/$defs/scanner" 36 | }, 37 | "ci": { 38 | "$ref": "#/$defs/ci" 39 | }, 40 | "report": { 41 | "$ref": "#/$defs/report" 42 | } 43 | }, 44 | "required": [ 45 | "version" 46 | ], 47 | "additionalProperties": false 48 | } 49 | -------------------------------------------------------------------------------- /workspaces/rc/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./readJSON.js"; 2 | -------------------------------------------------------------------------------- /workspaces/rc/src/utils/readJSON.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { readFileSync } from "node:fs"; 3 | 4 | export function readJSONSync(path: string, base?: string | URL) { 5 | const buf = readFileSync(typeof base === "string" ? new URL(path, base) : path); 6 | 7 | return JSON.parse(buf.toString()); 8 | } 9 | -------------------------------------------------------------------------------- /workspaces/rc/test/fixtures/.nodesecurerc: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.1.0", 3 | "i18n": "french", 4 | "strategy": "none", 5 | "registry": "https://registry.npmjs.org" 6 | } -------------------------------------------------------------------------------- /workspaces/rc/test/fixtures/configuration/ci_v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "i18n": "english", 4 | "strategy": "github-advisory", 5 | "ci": { 6 | "reporters": ["console"], 7 | "vulnerabilities": { 8 | "severity": "medium" 9 | }, 10 | "warnings": "error" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /workspaces/rc/test/fixtures/configuration/ci_v2.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "i18n": "english", 4 | "strategy": "github-advisory", 5 | "ci": { 6 | "reporters": ["console"], 7 | "vulnerabilities": { 8 | "severity": "medium" 9 | }, 10 | "warnings": { 11 | "obfuscated-code": "off", 12 | "unsafe-regex": "warning", 13 | "short-identifiers": "error" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /workspaces/rc/test/index.spec.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import path from "node:path"; 3 | import { readdirSync } from "node:fs"; 4 | import { fileURLToPath } from "node:url"; 5 | import assert from "node:assert"; 6 | import { describe, it } from "node:test"; 7 | 8 | // Import Third-party Dependencies 9 | import Ajv from "ajv"; 10 | import merge from "lodash.merge"; 11 | 12 | // Import Internal Dependencies 13 | import * as RC from "../src/index.js"; 14 | import { readJSONSync } from "../src/utils/readJSON.js"; 15 | import { generateDefaultRC } from "../src/rc.js"; 16 | 17 | // CONSTANTS 18 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 19 | 20 | describe("CONSTANTS", () => { 21 | it("should export a CONSTANTS variable", () => { 22 | assert("CONSTANTS" in RC); 23 | assert.equal(RC.CONSTANTS.CONFIGURATION_NAME, ".nodesecurerc"); 24 | }); 25 | }); 26 | 27 | describe("JSON Schema", () => { 28 | const kDummyPartialMandatoryRC: Partial = { 29 | report: { 30 | title: "hello report", 31 | logoUrl: "foobar" 32 | } 33 | }; 34 | 35 | it("should export a valid JSON Schema", () => { 36 | const ajv = new Ajv(); 37 | const validate = ajv.compile(RC.JSONSchema); 38 | 39 | assert(validate(generateDefaultRC())); 40 | 41 | const completeRC = merge( 42 | generateDefaultRC("complete"), 43 | kDummyPartialMandatoryRC 44 | ); 45 | assert(validate(completeRC)); 46 | 47 | assert(!validate({ foo: "bar" })); 48 | }); 49 | 50 | it("logoUrl should be optional", () => { 51 | const withoutLogoUrl: Partial = { 52 | report: { 53 | title: "hello report" 54 | } 55 | }; 56 | const ajv = new Ajv(); 57 | const validate = ajv.compile(RC.JSONSchema); 58 | 59 | assert(validate(generateDefaultRC())); 60 | 61 | const completeRC = merge( 62 | generateDefaultRC("complete"), 63 | withoutLogoUrl 64 | ); 65 | assert(validate(completeRC)); 66 | }); 67 | 68 | it("showFlags should be true by default", () => { 69 | const withoutshowFlags: Partial = { 70 | report: { 71 | title: "hello report" 72 | } 73 | }; 74 | const ajv = new Ajv({ useDefaults: true }); 75 | const validate = ajv.compile(RC.JSONSchema); 76 | 77 | const completeRC = merge( 78 | generateDefaultRC("complete"), 79 | withoutshowFlags 80 | ); 81 | validate(completeRC); 82 | assert.strictEqual(completeRC.report!.showFlags, true); 83 | }); 84 | 85 | it("should validate all fixtures configuration", () => { 86 | const ajv = new Ajv(); 87 | const validate = ajv.compile(RC.JSONSchema); 88 | 89 | const configurationPath = path.join(__dirname, "fixtures", "configuration"); 90 | const configurationFiles = readdirSync(configurationPath, { withFileTypes: true }) 91 | .filter((dirent) => dirent.isFile() && path.extname(dirent.name) === ".json") 92 | .map((dirent) => path.join(configurationPath, dirent.name)); 93 | 94 | for (const fileLocation of configurationFiles) { 95 | const json = readJSONSync(fileLocation); 96 | assert( 97 | validate(json), 98 | `Should be able to validate RC '${fileLocation}'` 99 | ); 100 | } 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /workspaces/rc/test/memoize.spec.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import assert from "node:assert"; 3 | import { describe, beforeEach, it } from "node:test"; 4 | 5 | // Import Internal Dependencies 6 | import { generateDefaultRC, RC } from "../src/rc.js"; 7 | import { memoize, memoized, maybeMemoized, clearMemoized } from "../src/index.js"; 8 | 9 | describe("memoize", () => { 10 | beforeEach(() => { 11 | clearMemoized(); 12 | }); 13 | 14 | it("should store the payload in memory", () => { 15 | const payload = generateDefaultRC(); 16 | memoize(payload); 17 | 18 | assert.deepEqual(memoized(), payload); 19 | }); 20 | 21 | it("should overwrite the previous payload if the overwrite option is true", () => { 22 | memoize(generateDefaultRC()); 23 | const payload: Partial = { 24 | version: "2.0.0", 25 | i18n: "french", 26 | strategy: "snyk" 27 | }; 28 | memoize(payload, { overwrite: true }); 29 | 30 | assert.deepEqual(memoized(), payload); 31 | }); 32 | 33 | it("should merge with the previous memoized payload if overwrite option is set to false", () => { 34 | const rc = generateDefaultRC(); 35 | memoize(rc, { overwrite: true }); 36 | 37 | const payload: Partial = { 38 | version: "2.0.0", 39 | i18n: "french", 40 | strategy: "snyk" 41 | }; 42 | memoize(payload, { overwrite: false }); 43 | 44 | assert.deepEqual(memoized(), { ...rc, ...payload }); 45 | }); 46 | }); 47 | 48 | describe("memoized", () => { 49 | beforeEach(() => { 50 | clearMemoized(); 51 | }); 52 | 53 | it("should return null when there is no memoized value", () => { 54 | assert.equal(memoized(), null); 55 | }); 56 | 57 | it("should return previously memoized RC", () => { 58 | const rc = generateDefaultRC(); 59 | memoize(rc); 60 | 61 | assert.deepEqual(memoized(), rc); 62 | }); 63 | }); 64 | 65 | describe("maybeMemoized", () => { 66 | beforeEach(() => { 67 | clearMemoized(); 68 | }); 69 | 70 | it("should return None when there is no memoized value", () => { 71 | const option = maybeMemoized(); 72 | assert(option.none); 73 | assert.equal(option.unwrapOr(null), null); 74 | }); 75 | 76 | it("should unwrap previously memoized RC", () => { 77 | const rc = generateDefaultRC(); 78 | memoize(rc); 79 | 80 | const option = maybeMemoized(); 81 | assert(option.some); 82 | assert.deepEqual(option.unwrap(), rc); 83 | }); 84 | }); 85 | 86 | describe("clearMemoized", () => { 87 | it("should clear memoized value", () => { 88 | const rc = generateDefaultRC(); 89 | memoize(rc); 90 | 91 | assert.notEqual(memoized(), null); 92 | clearMemoized(); 93 | assert.equal(memoized(), null); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /workspaces/rc/test/rc.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 { 7 | generateDefaultRC, 8 | generateCIConfiguration, 9 | generateReportConfiguration, 10 | generateScannerConfiguration 11 | } from "../src/rc.js"; 12 | 13 | describe("generateDefaultRC (internals)", () => { 14 | it(`should generate a RC with argument 'mode' equal 'ci' and 15 | then return an RC combining Default + CIConfiguration`, () => { 16 | const rc = generateDefaultRC("ci"); 17 | const expectedResult = Object.assign( 18 | generateDefaultRC(), 19 | generateCIConfiguration() 20 | ); 21 | 22 | assert.deepEqual(rc, expectedResult); 23 | }); 24 | 25 | it(`should generate a RC with argument 'mode' equal 'report' and 26 | then return an RC combining Default + ReportConfiguration`, () => { 27 | const rc = generateDefaultRC("report"); 28 | const expectedResult = Object.assign( 29 | generateDefaultRC(), 30 | generateReportConfiguration() 31 | ); 32 | 33 | assert.deepEqual(rc, expectedResult); 34 | }); 35 | 36 | it(`should generate a RC with argument 'mode' equal 'scanner' and 37 | then return an RC combining Default + ScannerConfiguration`, () => { 38 | const rc = generateDefaultRC("scanner"); 39 | const expectedResult = Object.assign( 40 | generateDefaultRC(), 41 | generateScannerConfiguration() 42 | ); 43 | 44 | assert.deepEqual(rc, expectedResult); 45 | }); 46 | 47 | it(`should generate a RC with argument 'mode' equal an Array ['complete'] and 48 | then return an RC combining all kind of available configurations internally`, () => { 49 | const rc = generateDefaultRC(["complete"]); 50 | const expectedResult = Object.assign( 51 | generateDefaultRC(), 52 | generateCIConfiguration(), 53 | generateReportConfiguration(), 54 | generateScannerConfiguration() 55 | ); 56 | 57 | assert.deepEqual(rc, expectedResult); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /workspaces/rc/test/types/index.test-d.ts: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import type { Result } from "@openally/result"; 3 | import { expectAssignable } from "tsd"; 4 | 5 | // Import Internal Dependencies 6 | import { read, write, type RC } from "../../dist/index.js"; 7 | 8 | expectAssignable>>(read()); 9 | expectAssignable>>(write("test", { 10 | payload: {}, 11 | partialUpdate: true 12 | })); 13 | -------------------------------------------------------------------------------- /workspaces/rc/test/types/rc.test-d.ts: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import { expectAssignable } from "tsd"; 3 | 4 | // Import Internal Dependencies 5 | import { 6 | generateDefaultRC, 7 | generateCIConfiguration, 8 | type RC, 9 | type CiConfiguration 10 | } from "../../dist/rc.js"; 11 | 12 | expectAssignable(generateDefaultRC()); 13 | expectAssignable<{ ci: CiConfiguration; }>(generateCIConfiguration()); 14 | -------------------------------------------------------------------------------- /workspaces/rc/test/write.spec.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import * as fs from "node:fs/promises"; 3 | import path from "node:path"; 4 | import os from "node:os"; 5 | import assert from "node:assert"; 6 | import { describe, it, before, beforeEach, after } from "node:test"; 7 | 8 | // Import Internal Dependencies 9 | import { read, write, CONSTANTS } from "../src/index.js"; 10 | import { generateDefaultRC } from "../src/rc.js"; 11 | 12 | describe("write and/or update .nodesecurerc", () => { 13 | const location = path.join(os.tmpdir(), "rcwrite"); 14 | 15 | before(async() => { 16 | await fs.mkdir(location); 17 | }); 18 | 19 | beforeEach(async() => { 20 | await fs.rm(path.join(location, CONSTANTS.CONFIGURATION_NAME), { force: true }); 21 | await read(location, { createIfDoesNotExist: true }); 22 | }); 23 | 24 | after(async() => { 25 | await fs.rm(location, { force: true, recursive: true }); 26 | }); 27 | 28 | it("should return a Node.js ENOENT Error because there is no .nodesecurerc file at the given location", async() => { 29 | await fs.rm(path.join(location, CONSTANTS.CONFIGURATION_NAME), { force: true }); 30 | 31 | const payload = { ...generateDefaultRC(), version: "4.5.2" }; 32 | const result = await write(location, { payload }); 33 | 34 | assert(!result.ok); 35 | 36 | const nodejsError = result.val as NodeJS.ErrnoException; 37 | assert(nodejsError instanceof Error); 38 | assert.equal(nodejsError.code, "ENOENT"); 39 | }); 40 | 41 | it("should fail to write because the payload is not matching the JSON Schema", async() => { 42 | const payload = { foo: "bar" } as any; 43 | const result = await write(location, { payload }); 44 | 45 | assert(!result.ok); 46 | 47 | const value = result.val as Error; 48 | assert(value instanceof Error); 49 | assert(value.message.includes("must have required property 'version'")); 50 | }); 51 | 52 | it("should rewrite a complete payload (content of .nodesecurerc)", async() => { 53 | const payload = { ...generateDefaultRC(), version: "4.5.2" }; 54 | 55 | const writeResult = await write(location, { payload }); 56 | assert(writeResult.ok); 57 | assert.equal(writeResult.val, void 0); 58 | 59 | const readResult = await read(location, { createIfDoesNotExist: false }); 60 | assert(readResult.ok); 61 | assert.deepEqual(readResult.val, payload); 62 | }); 63 | 64 | it("should partially update payload (content of .nodesecurerc)", async() => { 65 | const defaultRC = generateDefaultRC(); 66 | const payload = { i18n: "french" as const }; 67 | 68 | const writeResult = await write(location, { payload, partialUpdate: true }); 69 | assert(writeResult.ok); 70 | assert.equal(writeResult.val, void 0); 71 | 72 | const readResult = await read(location, { createIfDoesNotExist: false }); 73 | assert(readResult.ok); 74 | assert.deepEqual(readResult.val, { ...defaultRC, ...payload }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /workspaces/rc/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | }, 7 | "include": [ 8 | "src", 9 | "src/schema/**/*.json" 10 | ], 11 | "references": [ 12 | { 13 | "path": "../npm-types" 14 | }, 15 | { 16 | "path": "../i18n" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /workspaces/scanner/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @nodesecure/scanner 2 | 3 | ## 6.5.0 4 | 5 | ### Minor Changes 6 | 7 | - [#415](https://github.com/NodeSecure/scanner/pull/415) [`dd35d78`](https://github.com/NodeSecure/scanner/commit/dd35d78f159a70a5ec5e8f2a1cfb326e0a522247) Thanks [@fraxken](https://github.com/fraxken)! - Simplify extractors name & add way to inject fast probes with callbacks" 8 | 9 | - [#406](https://github.com/NodeSecure/scanner/pull/406) [`5e8beea`](https://github.com/NodeSecure/scanner/commit/5e8beead9fc1d2b3516dd410f4d0c8f2088655e4) Thanks [@fraxken](https://github.com/fraxken)! - Enhance warnings extractor by adding unique kinds & refactoring response 10 | 11 | - [#391](https://github.com/NodeSecure/scanner/pull/391) [`cd7ea18`](https://github.com/NodeSecure/scanner/commit/cd7ea1892a06af8cdf0b4cf651cc39b9252f1651) Thanks [@clemgbld](https://github.com/clemgbld)! - (Scanner) Implement Packument 'deprecated' property in DependencyVersion 12 | 13 | to include the message which come with the property when we detect it 14 | 15 | - [#407](https://github.com/NodeSecure/scanner/pull/407) [`3baa212`](https://github.com/NodeSecure/scanner/commit/3baa212d0caf6de0e9b792fbad03b94990450156) Thanks [@fraxken](https://github.com/fraxken)! - Implement a new Probe extractor for flags 16 | 17 | - [#409](https://github.com/NodeSecure/scanner/pull/409) [`0fc1156`](https://github.com/NodeSecure/scanner/commit/0fc11567e916a67066b149dea4a71d7cdf18b0fc) Thanks [@clemgbld](https://github.com/clemgbld)! - Implement EventEmitter with two events on Payload class (Extractors) 18 | 19 | - [#404](https://github.com/NodeSecure/scanner/pull/404) [`40a9350`](https://github.com/NodeSecure/scanner/commit/40a93507e20e1002059f71a40539dfd058879257) Thanks [@fraxken](https://github.com/fraxken)! - Implement new DependencyVersion type to detect the kind of module (cjs/esm/dual..) 20 | 21 | - [#402](https://github.com/NodeSecure/scanner/pull/402) [`d02c1e8`](https://github.com/NodeSecure/scanner/commit/d02c1e833f0c38dfc6dfb7dea481cae4c1ec0d1d) Thanks [@fraxken](https://github.com/fraxken)! - Add a new Extraction probe for vulnerabilities 22 | 23 | - [#399](https://github.com/NodeSecure/scanner/pull/399) [`cee3398`](https://github.com/NodeSecure/scanner/commit/cee3398da6610476991fcedae0efba98f83c46e5) Thanks [@fraxken](https://github.com/fraxken)! - Implement a new extraction probe for warnings 24 | 25 | - [#414](https://github.com/NodeSecure/scanner/pull/414) [`414d6da`](https://github.com/NodeSecure/scanner/commit/414d6dad49535ba84adf15c18f8f58b67bbb3e16) Thanks [@fraxken](https://github.com/fraxken)! - update @nodesecure/flags to major v3.x 26 | 27 | ### Patch Changes 28 | 29 | - Updated dependencies [[`3ee9a2e`](https://github.com/NodeSecure/scanner/commit/3ee9a2e17c877e7ea6fe23fc4ffc86578e6d0b72), [`0137bc6`](https://github.com/NodeSecure/scanner/commit/0137bc6060fe56c673b1ab92214debe63ce35958), [`55af858`](https://github.com/NodeSecure/scanner/commit/55af858f993520bca6f0fc5b0dbddf0b329ab5e0), [`40a9350`](https://github.com/NodeSecure/scanner/commit/40a93507e20e1002059f71a40539dfd058879257)]: 30 | - @nodesecure/mama@1.2.0 31 | - @nodesecure/tarball@1.2.0 32 | - @nodesecure/tree-walker@1.3.0 33 | -------------------------------------------------------------------------------- /workspaces/scanner/README.md: -------------------------------------------------------------------------------- 1 |

2 | @nodesecure/scanner 3 |

4 | 5 |

6 | The documentation of this project is in the root README 7 |

8 | -------------------------------------------------------------------------------- /workspaces/scanner/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nodesecure/scanner", 3 | "version": "6.5.0", 4 | "description": "A package API to run a static analysis of your module's dependencies.", 5 | "type": "module", 6 | "exports": "./dist/index.js", 7 | "types": "./dist/index.d.ts", 8 | "engines": { 9 | "node": ">=20" 10 | }, 11 | "scripts": { 12 | "build": "tsc -b", 13 | "lint": "eslint src test", 14 | "prepublishOnly": "npm run build && pkg-ok", 15 | "test": "npm run test-only", 16 | "test-only": "tsx --test ./test/**/*.spec.ts", 17 | "coverage": "c8 -r html npm run test-only" 18 | }, 19 | "files": [ 20 | "dist" 21 | ], 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/NodeSecure/scanner.git" 25 | }, 26 | "keywords": [ 27 | "node", 28 | "nodejs", 29 | "security", 30 | "cli", 31 | "sast", 32 | "scanner", 33 | "static", 34 | "code", 35 | "analysis", 36 | "node_modules", 37 | "tree", 38 | "npm", 39 | "registry", 40 | "graph", 41 | "visualization", 42 | "dependencies" 43 | ], 44 | "author": "NodeSecure", 45 | "license": "MIT", 46 | "bugs": { 47 | "url": "https://github.com/NodeSecure/scanner/issues" 48 | }, 49 | "homepage": "https://github.com/NodeSecure/tree/master/workspaces/scanner#readme", 50 | "dependencies": { 51 | "@fastify/deepmerge": "^3.1.0", 52 | "@nodesecure/conformance": "^1.0.0", 53 | "@nodesecure/contact": "^1.0.1", 54 | "@nodesecure/flags": "^3.0.3", 55 | "@nodesecure/i18n": "^4.0.1", 56 | "@nodesecure/js-x-ray": "^8.1.0", 57 | "@nodesecure/mama": "^1.2.0", 58 | "@nodesecure/npm-registry-sdk": "^3.0.0", 59 | "@nodesecure/npm-types": "^1.2.0", 60 | "@nodesecure/rc": "^4.1.0", 61 | "@nodesecure/tarball": "^1.2.0", 62 | "@nodesecure/tree-walker": "^1.3.0", 63 | "@nodesecure/vulnera": "^2.0.1", 64 | "@openally/mutex": "^1.0.0", 65 | "frequency-set": "^1.0.2", 66 | "pacote": "^21.0.0", 67 | "semver": "^7.5.4", 68 | "type-fest": "^4.41.0" 69 | }, 70 | "devDependencies": { 71 | "@types/node": "^22.15.17", 72 | "c8": "^10.1.3", 73 | "tsx": "^4.19.4", 74 | "typescript": "^5.8.3" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /workspaces/scanner/src/class/logger.class.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { EventEmitter } from "node:events"; 3 | import { performance } from "node:perf_hooks"; 4 | 5 | export const ScannerLoggerEvents = { 6 | done: "depWalkerFinished", 7 | analysis: { 8 | tree: "walkTree", 9 | tarball: "tarball", 10 | registry: "registry" 11 | }, 12 | manifest: { 13 | read: "readManifest", 14 | fetch: "fetchManifest" 15 | } 16 | } as const; 17 | 18 | export interface LoggerEventData { 19 | /** UNIX Timestamp */ 20 | startedAt: number; 21 | /** Count of triggered event */ 22 | count: number; 23 | } 24 | 25 | export class Logger extends EventEmitter { 26 | public events: Map = new Map(); 27 | 28 | start(eventName: string): this { 29 | if (this.events.has(eventName)) { 30 | return this; 31 | } 32 | 33 | this.events.set(eventName, { 34 | startedAt: performance.now(), 35 | count: 0 36 | }); 37 | this.emit("start", eventName); 38 | 39 | return this; 40 | } 41 | 42 | tick(eventName: string): this { 43 | if (!this.events.has(eventName)) { 44 | return this; 45 | } 46 | 47 | this.events.get(eventName)!.count++; 48 | this.emit("tick", eventName); 49 | 50 | return this; 51 | } 52 | 53 | count(eventName: string): number { 54 | return this.events.get(eventName)?.count ?? 0; 55 | } 56 | 57 | end(eventName: string): this { 58 | if (!this.events.has(eventName)) { 59 | return this; 60 | } 61 | 62 | const data = this.events.get(eventName)!; 63 | this.emit("end", eventName, { 64 | ...data, 65 | executionTime: performance.now() - data.startedAt 66 | }); 67 | 68 | return this; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /workspaces/scanner/src/extractors/index.ts: -------------------------------------------------------------------------------- 1 | // Import Internal Dependencies 2 | import { 3 | Payload, 4 | Callbacks, 5 | type ProbeExtractor, 6 | type PackumentProbeExtractor, 7 | type ManifestProbeExtractor, 8 | type PackumentProbeNextCallback, 9 | type ManifestProbeNextCallback 10 | } from "./payload.js"; 11 | 12 | import * as Probes from "./probes/index.js"; 13 | 14 | export const Extractors = { 15 | Payload, 16 | Callbacks, 17 | Probes 18 | } as const; 19 | 20 | export type { 21 | ProbeExtractor, 22 | PackumentProbeExtractor, 23 | ManifestProbeExtractor, 24 | PackumentProbeNextCallback, 25 | ManifestProbeNextCallback 26 | }; 27 | -------------------------------------------------------------------------------- /workspaces/scanner/src/extractors/probes/ContactExtractor.class.ts: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import type { Contact } from "@nodesecure/npm-types"; 3 | 4 | // Import Internal Dependencies 5 | import type { 6 | ManifestProbeExtractor, 7 | ProbeExtractorManifestParent 8 | } from "../payload.js"; 9 | import type { DependencyVersion } from "../../types.js"; 10 | 11 | export type ContactsResult = { 12 | contacts: Record; 13 | }; 14 | 15 | export class Contacts implements ManifestProbeExtractor { 16 | level = "manifest" as const; 17 | 18 | #contacts: ContactsResult["contacts"] = Object.create(null); 19 | #packages: Set = new Set(); 20 | 21 | #addContact( 22 | user: Contact | null 23 | ) { 24 | if (!user || !user.email) { 25 | return; 26 | } 27 | 28 | this.#contacts[user.email] = user.email in this.#contacts ? 29 | ++this.#contacts[user.email] : 1; 30 | } 31 | 32 | next( 33 | _: string, 34 | version: DependencyVersion, 35 | parent: ProbeExtractorManifestParent 36 | ) { 37 | const { author } = version; 38 | const { name, dependency } = parent; 39 | 40 | this.#addContact(author); 41 | if (!this.#packages.has(name)) { 42 | dependency.metadata.maintainers.forEach( 43 | (maintainer) => this.#addContact(maintainer) 44 | ); 45 | this.#packages.add(name); 46 | } 47 | } 48 | 49 | done() { 50 | return { 51 | contacts: this.#contacts 52 | }; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /workspaces/scanner/src/extractors/probes/FlagsExtractor.class.ts: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import FrequencySet from "frequency-set"; 3 | 4 | // Import Internal Dependencies 5 | import type { 6 | ManifestProbeExtractor 7 | } from "../payload.js"; 8 | import type { DependencyVersion } from "../../types.js"; 9 | 10 | export type FlagsResult = { 11 | flags: Record; 12 | }; 13 | 14 | export class Flags implements ManifestProbeExtractor { 15 | level = "manifest" as const; 16 | 17 | #flags = new FrequencySet(); 18 | 19 | next( 20 | _: string, 21 | version: DependencyVersion 22 | ) { 23 | const { flags } = version; 24 | 25 | flags.forEach((flagName) => this.#flags.add(flagName)); 26 | } 27 | 28 | done() { 29 | return { 30 | flags: Object.fromEntries(this.#flags) 31 | }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /workspaces/scanner/src/extractors/probes/LicensesExtractor.class.ts: -------------------------------------------------------------------------------- 1 | // Import Internal Dependencies 2 | import type { 3 | ManifestProbeExtractor 4 | } from "../payload.js"; 5 | import type { DependencyVersion } from "../../types.js"; 6 | 7 | export type LicensesResult = { 8 | licenses: Record; 9 | }; 10 | 11 | export class Licenses implements ManifestProbeExtractor { 12 | level = "manifest" as const; 13 | 14 | #licenses: LicensesResult["licenses"] = Object.create(null); 15 | 16 | next( 17 | _: string, 18 | version: DependencyVersion 19 | ) { 20 | const { uniqueLicenseIds } = version; 21 | 22 | for (const licenseName of uniqueLicenseIds) { 23 | this.#licenses[licenseName] = licenseName in this.#licenses ? 24 | ++this.#licenses[licenseName] : 1; 25 | } 26 | } 27 | 28 | done() { 29 | return { 30 | licenses: this.#licenses 31 | }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /workspaces/scanner/src/extractors/probes/SizeExtractor.class.ts: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import { formatBytes } from "@nodesecure/utils"; 3 | 4 | // Import Internal Dependencies 5 | import type { 6 | ManifestProbeExtractor, 7 | ProbeExtractorManifestParent 8 | } from "../payload.js"; 9 | import type { DependencyVersion } from "../../types.js"; 10 | 11 | export type SizeResult = { 12 | size: { 13 | all: string; 14 | internal: string; 15 | external: string; 16 | }; 17 | }; 18 | 19 | export interface SizeOptions { 20 | organizationPrefix?: string; 21 | } 22 | 23 | export class Size implements ManifestProbeExtractor { 24 | level = "manifest" as const; 25 | 26 | #size = { 27 | all: 0, 28 | internal: 0, 29 | external: 0 30 | }; 31 | #organizationPrefix: string | null = null; 32 | 33 | constructor( 34 | options: SizeOptions = {} 35 | ) { 36 | const { organizationPrefix = null } = options; 37 | 38 | this.#organizationPrefix = organizationPrefix; 39 | } 40 | 41 | next( 42 | _: string, 43 | version: DependencyVersion, 44 | parent: ProbeExtractorManifestParent 45 | ) { 46 | const { size } = version; 47 | 48 | const isExternal = this.#organizationPrefix === null ? 49 | true : 50 | !parent.name.startsWith(`${this.#organizationPrefix}/`); 51 | 52 | this.#size.all += size; 53 | this.#size[isExternal ? "external" : "internal"] += size; 54 | } 55 | 56 | done() { 57 | return { 58 | size: { 59 | all: formatBytes(this.#size.all), 60 | internal: formatBytes(this.#size.internal), 61 | external: formatBytes(this.#size.external) 62 | } 63 | }; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /workspaces/scanner/src/extractors/probes/VulnerabilitiesExtractor.class.ts: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import type { StandardVulnerability } from "@nodesecure/vulnera"; 3 | 4 | // Import Internal Dependencies 5 | import type { 6 | PackumentProbeExtractor 7 | } from "../payload.js"; 8 | import type { Dependency } from "../../types.js"; 9 | 10 | export type VulnerabilitiesResult = { 11 | vulnerabilities: StandardVulnerability[]; 12 | }; 13 | 14 | export class Vulnerabilities implements PackumentProbeExtractor { 15 | level = "packument" as const; 16 | 17 | #vulnerabilities: StandardVulnerability[] = []; 18 | 19 | next( 20 | _: string, 21 | dependency: Dependency 22 | ) { 23 | const { vulnerabilities = [] } = dependency; 24 | 25 | this.#vulnerabilities.push( 26 | ...vulnerabilities 27 | ); 28 | } 29 | 30 | done() { 31 | return { 32 | vulnerabilities: this.#vulnerabilities 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /workspaces/scanner/src/extractors/probes/WarningsExtractor.class.ts: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import type { 3 | WarningDefault, 4 | Warning, 5 | WarningName 6 | } from "@nodesecure/js-x-ray"; 7 | import FrequencySet from "frequency-set"; 8 | 9 | // Import Internal Dependencies 10 | import type { 11 | ManifestProbeExtractor, 12 | ProbeExtractorManifestParent 13 | } from "../payload.js"; 14 | import type { DependencyVersion } from "../../types.js"; 15 | 16 | export type WarningsResult = { 17 | warnings: { 18 | count: number; 19 | groups: Record[]>; 20 | uniqueKinds: Record; 21 | }; 22 | }; 23 | 24 | export interface WarningsOptions { 25 | /** 26 | * @default true 27 | */ 28 | useSpecAsKey?: boolean; 29 | } 30 | 31 | export class Warnings implements ManifestProbeExtractor { 32 | level = "manifest" as const; 33 | 34 | #warnings: Record[]> = Object.create(null); 35 | #uniqueKinds = new FrequencySet(); 36 | #count = 0; 37 | #useSpecAsKey: boolean; 38 | 39 | constructor( 40 | options: WarningsOptions = {} 41 | ) { 42 | this.#useSpecAsKey = options.useSpecAsKey ?? true; 43 | } 44 | 45 | next( 46 | version: string, 47 | depVersion: DependencyVersion, 48 | parent: ProbeExtractorManifestParent 49 | ) { 50 | const { warnings } = depVersion; 51 | if (warnings.length === 0) { 52 | return; 53 | } 54 | 55 | this.#count += warnings.length; 56 | const key = this.#useSpecAsKey ? 57 | `${parent.name}@${version}` : 58 | parent.name; 59 | 60 | warnings 61 | .map((warn) => warn.kind) 62 | .forEach((kind) => this.#uniqueKinds.add(kind)); 63 | 64 | if (key in this.#warnings) { 65 | this.#warnings[key].push(...warnings); 66 | } 67 | else { 68 | this.#warnings[key] = [...warnings]; 69 | } 70 | } 71 | 72 | done() { 73 | return { 74 | warnings: { 75 | count: this.#count, 76 | uniqueKinds: Object.fromEntries(this.#uniqueKinds) as any, 77 | groups: this.#warnings 78 | } 79 | }; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /workspaces/scanner/src/extractors/probes/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./SizeExtractor.class.js"; 2 | export * from "./LicensesExtractor.class.js"; 3 | export * from "./ContactExtractor.class.js"; 4 | export * from "./WarningsExtractor.class.js"; 5 | export * from "./VulnerabilitiesExtractor.class.js"; 6 | export * from "./FlagsExtractor.class.js"; 7 | -------------------------------------------------------------------------------- /workspaces/scanner/src/i18n/english.js: -------------------------------------------------------------------------------- 1 | const scanner = { 2 | disable_scarf: "This dependency could collect data against your consent so think to disable it with the env var: SCARF_ANALYTICS", 3 | keylogging: "This dependency can retrieve your keyboard and mouse inputs. It can be used for 'keylogging' attacks/malwares." 4 | }; 5 | 6 | export default { scanner }; 7 | -------------------------------------------------------------------------------- /workspaces/scanner/src/i18n/french.js: -------------------------------------------------------------------------------- 1 | const scanner = { 2 | disable_scarf: "Cette dépendance peut récolter des données contre votre volonté, pensez donc à la désactiver en fournissant la variable d'environnement SCARF_ANALYTICS", 3 | keylogging: "Cette dépendance peut obtenir vos entrées clavier ou de souris. Cette dépendance peut être utilisée en tant que 'keylogging' attacks/malwares." 4 | }; 5 | 6 | export default { scanner }; 7 | 8 | -------------------------------------------------------------------------------- /workspaces/scanner/src/utils/addMissingVersionFlags.ts: -------------------------------------------------------------------------------- 1 | // Import Internal Dependencies 2 | import type { Dependency } from "../types.js"; 3 | 4 | // TODO: add strict flags type 5 | export function* addMissingVersionFlags( 6 | flags: Set, 7 | dep: Dependency 8 | ): IterableIterator { 9 | const { metadata, vulnerabilities = [], versions } = dep; 10 | const semverVersions = Object.keys(versions); 11 | 12 | if (!metadata.hasReceivedUpdateInOneYear && flags.has("hasOutdatedDependency") && !flags.has("isDead")) { 13 | yield "isDead"; 14 | } 15 | if (metadata.hasManyPublishers && !flags.has("hasManyPublishers")) { 16 | yield "hasManyPublishers"; 17 | } 18 | if (metadata.hasChangedAuthor && !flags.has("hasChangedAuthor")) { 19 | yield "hasChangedAuthor"; 20 | } 21 | if (vulnerabilities.length > 0 && !flags.has("hasVulnerabilities")) { 22 | yield "hasVulnerabilities"; 23 | } 24 | if (semverVersions.length > 1 && !flags.has("hasDuplicate")) { 25 | yield "hasDuplicate"; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /workspaces/scanner/src/utils/dirname.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { fileURLToPath } from "node:url"; 3 | import { dirname } from "node:path"; 4 | 5 | export function getDirNameFromUrl(url: string | URL): string { 6 | const __filename = fileURLToPath(url); 7 | 8 | return dirname(__filename); 9 | } 10 | -------------------------------------------------------------------------------- /workspaces/scanner/src/utils/getLinks.ts: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import type { PackageJSON, PackumentVersion } from "@nodesecure/npm-types"; 3 | 4 | // CONSTANTS 5 | const kVCSHosts = new Set(["github.com", "gitlab.com"]); 6 | 7 | function getVCSRepositoryURL( 8 | link: string | null 9 | ): string | null { 10 | if (!link) { 11 | return null; 12 | } 13 | 14 | try { 15 | const url = new URL(link); 16 | const { hostname, pathname } = url; 17 | 18 | if (kVCSHosts.has(hostname) === false) { 19 | return null; 20 | } 21 | 22 | const [owner, repo] = pathname.split("/").filter(Boolean).map((curr) => curr.replace(".git", "")); 23 | 24 | return `https://${hostname}/${owner}/${repo}`; 25 | } 26 | catch { 27 | return null; 28 | } 29 | } 30 | 31 | export function getLinks( 32 | packumentVersion: PackumentVersion 33 | ) { 34 | const homepage = packumentVersion.homepage || null; 35 | const repositoryUrl = typeof packumentVersion.repository === "string" ? 36 | packumentVersion.repository : 37 | packumentVersion.repository?.url ?? null; 38 | 39 | return { 40 | npm: `https://www.npmjs.com/package/${packumentVersion.name}/v/${packumentVersion.version}`, 41 | homepage, 42 | repository: 43 | getVCSRepositoryURL(homepage) ?? 44 | getVCSRepositoryURL(repositoryUrl) 45 | }; 46 | } 47 | 48 | export function getManifestLinks(manifest: PackageJSON) { 49 | const homepage = manifest.homepage ?? null; 50 | const repositoryUrl = typeof manifest.repository === "string" ? 51 | manifest.repository : 52 | manifest.repository?.url ?? null; 53 | 54 | return { 55 | npm: null, 56 | homepage, 57 | repository: 58 | getVCSRepositoryURL(homepage) ?? 59 | getVCSRepositoryURL(repositoryUrl) 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /workspaces/scanner/src/utils/getUsedDeps.ts: -------------------------------------------------------------------------------- 1 | export function getUsedDeps( 2 | deps: Set<`${string}@${string}`> 3 | ): string[][] { 4 | return [...deps].map((name) => { 5 | const isScoped = name.startsWith("@"); 6 | if (isScoped) { 7 | const [nameChunk, version] = name.slice(1).split("@"); 8 | 9 | return [`@${nameChunk}`, version]; 10 | } 11 | 12 | return name.split("@"); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /workspaces/scanner/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./dirname.js"; 2 | export * from "./warnings.js"; 3 | export * from "./addMissingVersionFlags.js"; 4 | export * from "./getLinks.js"; 5 | export * from "./urlToString.js"; 6 | export * from "./getUsedDeps.js"; 7 | export * from "./manifestAuthor.js"; 8 | export * from "./isNodesecurePayload.js"; 9 | 10 | export const NPM_TOKEN = typeof process.env.NODE_SECURE_TOKEN === "string" ? 11 | { token: process.env.NODE_SECURE_TOKEN } : 12 | {}; 13 | -------------------------------------------------------------------------------- /workspaces/scanner/src/utils/isNodesecurePayload.ts: -------------------------------------------------------------------------------- 1 | // Import Internal Dependencies 2 | import type { Payload } from "../types.js"; 3 | 4 | export function isNodesecurePayload( 5 | data: Payload | Payload["dependencies"] 6 | ): data is Payload { 7 | return "dependencies" in data && "id" in data && "scannerVersion" in data; 8 | } 9 | -------------------------------------------------------------------------------- /workspaces/scanner/src/utils/manifestAuthor.ts: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import type { Contact } from "@nodesecure/npm-types"; 3 | 4 | export function manifestAuthor(author: string | Contact | undefined): Contact | null { 5 | if (author === void 0) { 6 | return null; 7 | } 8 | 9 | if (typeof author === "string") { 10 | if (author.trim() === "") { 11 | return null; 12 | } 13 | 14 | const authorRegexp = /^([^<(]+?)?[ \t]*(?:<([^>(]+?)>)?[ \t]*(?:\(([^)]+?)\)|$)/g; 15 | const [_, name, email, url] = authorRegexp.exec(author) ?? []; 16 | 17 | return { name, email, url }; 18 | } 19 | 20 | return author; 21 | } 22 | -------------------------------------------------------------------------------- /workspaces/scanner/src/utils/urlToString.ts: -------------------------------------------------------------------------------- 1 | export function urlToString( 2 | uri: string | URL 3 | ): string { 4 | return typeof uri === "string" ? 5 | new URL(uri).toString() : 6 | uri.toString(); 7 | } 8 | -------------------------------------------------------------------------------- /workspaces/scanner/src/utils/warnings.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import path from "node:path"; 3 | 4 | // Import Third-party Dependencies 5 | import * as i18n from "@nodesecure/i18n"; 6 | import * as RC from "@nodesecure/rc"; 7 | import { 8 | ContactExtractor, 9 | type IlluminatedContact, 10 | type ContactExtractorPackageMetadata 11 | } from "@nodesecure/contact"; 12 | import type { Contact } from "@nodesecure/npm-types"; 13 | 14 | // Import Internal Dependencies 15 | import { getDirNameFromUrl } from "./dirname.js"; 16 | import type { Dependency } from "../types.js"; 17 | 18 | await i18n.extendFromSystemPath( 19 | path.join(getDirNameFromUrl(import.meta.url), "..", "i18n") 20 | ); 21 | 22 | // CONSTANTS 23 | const kDetectedDep = i18n.taggedString`The dependency '${0}' has been detected in the dependency Tree.`; 24 | const kDefaultIlluminatedContacts: Contact[] = [ 25 | { 26 | name: "marak", 27 | email: "marak.squires@gmail.com" 28 | } 29 | ]; 30 | 31 | const kDependencyWarnMessage = { 32 | "@scarf/scarf": await i18n.getToken("scanner.disable_scarf"), 33 | iohook: await i18n.getToken("scanner.keylogging") 34 | } as const; 35 | 36 | export interface GetWarningsResult { 37 | warnings: string[]; 38 | illuminated: IlluminatedContact[]; 39 | } 40 | 41 | export async function getDependenciesWarnings( 42 | dependenciesMap: Map, 43 | highlightContacts: Contact[] = [] 44 | ): Promise { 45 | const vulnerableDependencyNames = Object.keys( 46 | kDependencyWarnMessage 47 | ) as unknown as (keyof typeof kDependencyWarnMessage)[]; 48 | 49 | const warnings = vulnerableDependencyNames 50 | .flatMap((name) => (dependenciesMap.has(name) ? `${kDetectedDep(name)} ${kDependencyWarnMessage[name]}` : [])); 51 | 52 | const dependencies: Record = Object.create(null); 53 | for (const [packageName, dependency] of dependenciesMap) { 54 | const { author, maintainers } = dependency.metadata; 55 | 56 | dependencies[packageName] = { 57 | maintainers, 58 | ...(author === null ? {} : { author }) 59 | }; 60 | } 61 | 62 | const memoizedConfig = RC.memoized(); 63 | const extractor = new ContactExtractor({ 64 | highlight: [ 65 | ...highlightContacts, 66 | ...(memoizedConfig === null ? 67 | [] : (memoizedConfig.scanner?.highlight?.contacts ?? []) 68 | ), 69 | ...kDefaultIlluminatedContacts 70 | ] 71 | }); 72 | const illuminated = extractor.fromDependencies( 73 | dependencies 74 | ); 75 | 76 | return { 77 | warnings, 78 | illuminated 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /workspaces/scanner/test/fixtures/depWalker/non-npm-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "non-npm-package", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "keywords": [], 9 | "description": "", 10 | "dependencies": { 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/NodeSecure/non-npm-package.git" 15 | }, 16 | "author": "NodeSecure", 17 | "homepage": "https://nodesecure.com" 18 | } 19 | -------------------------------------------------------------------------------- /workspaces/scanner/test/fixtures/depWalker/pkg.gitdeps.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pkg.gitdeps", 3 | "version": "0.1.0", 4 | "description": "Mock package with git related dependencies versions", 5 | "main": "index.js", 6 | "homepage": "https://github.com/username/pkg.gitdeps#readme", 7 | "devDependencies": { 8 | "ava": "^2.1.0", 9 | "cross-env": "^5.2.0", 10 | "eslint": "^5.13.0", 11 | "husky": "^2.4.0", 12 | "jsdoc": "^3.6.2" 13 | }, 14 | "dependencies": { 15 | "zen-observable": "^0.8.15", 16 | "nanoid": "github:ai/nanoid", 17 | "js-x-ray": "git://github.com/NodeSecure/js-x-ray.git", 18 | "nanodelay": "git+ssh://git@github.com:ai/nanodelay.git", 19 | "nanoevents": "git+https://github.com/ai/nanoevents.git" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /workspaces/scanner/test/fixtures/depWalker/slimio.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@slimio/config", 3 | "version": "0.16.0", 4 | "description": "SlimIO Reactive JSON Config loaded", 5 | "main": "index.js", 6 | "homepage": "https://github.com/SlimIO/Config#readme", 7 | "devDependencies": { 8 | "@commitlint/cli": "^8.3.5", 9 | "@commitlint/config-conventional": "^8.3.4", 10 | "@escommunity/minami": "^1.0.0", 11 | "@slimio/eslint-config": "^4.0.0", 12 | "@slimio/psp": "^0.11.0", 13 | "@types/lodash.clonedeep": "^4.5.6", 14 | "@types/lodash.get": "^4.4.6", 15 | "@types/lodash.set": "^4.3.6", 16 | "@types/zen-observable": "^0.8.0", 17 | "ava": "^3.2.0", 18 | "conventional-changelog-cli": "^2.0.31", 19 | "cross-env": "^7.0.0", 20 | "eslint": "^6.8.0", 21 | "husky": "^4.2.1", 22 | "jsdoc": "^3.6.3", 23 | "nyc": "^15.0.0", 24 | "pkg-ok": "^2.3.1" 25 | }, 26 | "dependencies": { 27 | "@iarna/toml": "^2.2.3", 28 | "@slimio/is": "^1.5.1", 29 | "ajv": "^6.11.0", 30 | "lodash.clonedeep": "^4.5.0", 31 | "lodash.get": "^4.4.2", 32 | "lodash.set": "^4.3.2", 33 | "node-watch": "^0.6.3", 34 | "zen-observable": "^0.8.15" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /workspaces/scanner/test/fixtures/depWalker/slimio.is.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@slimio/is", 3 | "version": "1.5.1", 4 | "description": "SlimIO is (JavaScript Primitives & Objects type checker)", 5 | "main": "index.js", 6 | "engines": { 7 | "node": ">=10" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/SlimIO/is.git" 12 | }, 13 | "author": "SlimIO", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/SlimIO/is/issues" 17 | }, 18 | "homepage": "https://github.com/SlimIO/is#readme", 19 | "devDependencies": { 20 | "@commitlint/cli": "^8.0.0", 21 | "@commitlint/config-conventional": "^8.0.0", 22 | "@escommunity/minami": "^1.0.0", 23 | "@slimio/eslint-config": "^2.0.4", 24 | "@slimio/psp": "^0.4.0", 25 | "ava": "^2.1.0", 26 | "cross-env": "^5.2.0", 27 | "eslint": "^5.13.0", 28 | "husky": "^2.4.0", 29 | "jsdoc": "^3.6.2", 30 | "nyc": "^14.1.1", 31 | "pkg-ok": "^2.3.1" 32 | }, 33 | "dependencies": {} 34 | } 35 | -------------------------------------------------------------------------------- /workspaces/scanner/test/fixtures/scannerPayloads/otherRootDependency.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "hjnfnJ", 3 | "rootDependencyName": "bar" 4 | } 5 | -------------------------------------------------------------------------------- /workspaces/scanner/test/fixtures/verifySemVer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "name": "scanner", 4 | "type": "module", 5 | "license": "MIT" 6 | } 7 | -------------------------------------------------------------------------------- /workspaces/scanner/test/integrityWarning.spec.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Internal Dependencies 6 | import { from } from "../src/index.js"; 7 | 8 | test("expect one warning from 'darcyclarke-manifest-pkg' with an integrity issue", async() => { 9 | const result = await from("darcyclarke-manifest-pkg", { 10 | maxDepth: 2 11 | }); 12 | 13 | assert.equal(result.warnings.length, 1); 14 | assert.match(result.warnings[0], /manifest & tarball integrity doesn't match/g); 15 | }); 16 | -------------------------------------------------------------------------------- /workspaces/scanner/test/utils/addMissingVersionFlags.spec.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Internal Dependencies 6 | import { addMissingVersionFlags } from "../../src/utils/index.js"; 7 | 8 | test("addMissingVersionFlags should return all missing flags", () => { 9 | const flags = new Set([ 10 | "hasOutdatedDependency" 11 | ]); 12 | const gen = addMissingVersionFlags(flags, { 13 | metadata: { 14 | hasReceivedUpdateInOneYear: false, 15 | hasManyPublishers: true, 16 | hasChangedAuthor: true 17 | }, 18 | vulnerabilities: [{}], 19 | versions: ["1.1.1", "1.5.0"] 20 | } as any); 21 | const resultFlags = [...gen]; 22 | assert.deepEqual(resultFlags, [ 23 | "isDead", "hasManyPublishers", "hasChangedAuthor", "hasVulnerabilities", "hasDuplicate" 24 | ]); 25 | }); 26 | 27 | test("addMissingVersionFlags should return an empty array", () => { 28 | const flags = new Set([ 29 | "hasOutdatedDependency", "isDead", "hasManyPublishers", "hasChangedAuthor", "hasVulnerabilities", "hasDuplicate" 30 | ]); 31 | const gen = addMissingVersionFlags(flags, { 32 | metadata: { 33 | hasReceivedUpdateInOneYear: false, 34 | hasManyPublishers: true, 35 | hasChangedAuthor: true 36 | }, 37 | vulnerabilities: [{}], 38 | versions: ["1.1.1", "1.5.0"] 39 | } as any); 40 | const resultFlags = [...gen]; 41 | assert.deepEqual(resultFlags, []); 42 | }); 43 | -------------------------------------------------------------------------------- /workspaces/scanner/test/utils/getLinks.spec.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import assert from "node:assert/strict"; 3 | import { describe, it } from "node:test"; 4 | 5 | // Import Third-party Dependencies 6 | import { type PackumentVersion } from "@nodesecure/npm-types"; 7 | 8 | // Import Internal Dependencies 9 | import * as utils from "../../src/utils/index.js"; 10 | 11 | describe("utils.getLinks", () => { 12 | it("should return all links", () => { 13 | assert.deepStrictEqual(utils.getLinks({ 14 | homepage: "https://github.com/foo/bar", 15 | repository: "git@github.com:foo/bar.git", 16 | name: "foo", 17 | version: "1.0.0" 18 | } as any as PackumentVersion), { 19 | npm: "https://www.npmjs.com/package/foo/v/1.0.0", 20 | homepage: "https://github.com/foo/bar", 21 | repository: "https://github.com/foo/bar" 22 | }); 23 | }); 24 | 25 | it("homepage should be null but repository should be parsed", () => { 26 | assert.deepStrictEqual(utils.getLinks({ 27 | homepage: null, 28 | repository: "https://github.com/foo/bar.git", 29 | name: "foo", 30 | version: "1.0.0" 31 | } as any), { 32 | npm: "https://www.npmjs.com/package/foo/v/1.0.0", 33 | homepage: null, 34 | repository: "https://github.com/foo/bar" 35 | }); 36 | }); 37 | 38 | it("should return repository.url", () => { 39 | assert.deepStrictEqual(utils.getLinks({ 40 | name: "foo", 41 | version: "1.0.0", 42 | homepage: "https://github.com/foo/bar", 43 | repository: { 44 | type: "git", 45 | url: "github.com/foo/bar" 46 | } 47 | } as any), { 48 | npm: "https://www.npmjs.com/package/foo/v/1.0.0", 49 | homepage: "https://github.com/foo/bar", 50 | repository: "https://github.com/foo/bar" 51 | }); 52 | }); 53 | }); 54 | 55 | describe("utils.getManifestLinks", () => { 56 | it("should return homepage and repository", () => { 57 | assert.deepStrictEqual(utils.getManifestLinks({ 58 | name: "@foo/bar", 59 | version: "1.0.0", 60 | homepage: "https://github.com/foo/bar", 61 | repository: "https://github.com/foo/bar" 62 | }), { 63 | npm: null, 64 | homepage: "https://github.com/foo/bar", 65 | repository: "https://github.com/foo/bar" 66 | }); 67 | }); 68 | 69 | it("should return repository only", () => { 70 | assert.deepStrictEqual(utils.getManifestLinks({ 71 | name: "@foo/bar", 72 | version: "1.0.0", 73 | homepage: void 0, 74 | repository: "https://github.com/foo/bar" 75 | }), { 76 | npm: null, 77 | homepage: null, 78 | repository: "https://github.com/foo/bar" 79 | }); 80 | }); 81 | 82 | it("should return repository.url", () => { 83 | assert.deepStrictEqual(utils.getManifestLinks({ 84 | name: "@foo/bar", 85 | version: "1.0.0", 86 | homepage: void 0, 87 | repository: { 88 | type: "git", 89 | url: "https://github.com/foo/bar" 90 | } 91 | }), { 92 | npm: null, 93 | homepage: null, 94 | repository: "https://github.com/foo/bar" 95 | }); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /workspaces/scanner/test/utils/getUsedDeps.spec.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Internal Dependencies 6 | import { getUsedDeps } from "../../src/utils/index.js"; 7 | 8 | test("getUsedDeps should handle scoped packages", () => { 9 | const deps = getUsedDeps(new Set([ 10 | "@slimio/is@latest" 11 | ])); 12 | 13 | assert.deepStrictEqual(deps, [["@slimio/is", "latest"]]); 14 | }); 15 | 16 | test("getUsedDeps should handle non-scoped packages", () => { 17 | const deps = getUsedDeps(new Set([ 18 | "is@latest" 19 | ])); 20 | 21 | assert.deepStrictEqual(deps, [["is", "latest"]]); 22 | }); 23 | -------------------------------------------------------------------------------- /workspaces/scanner/test/utils/manifestAuthor.spec.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import assert from "node:assert/strict"; 3 | import { describe, it } from "node:test"; 4 | 5 | // Import Internal Dependencies 6 | import * as utils from "../../src/utils/index.js"; 7 | 8 | describe("utils.manifestAuthor", () => { 9 | it("should return null when given undefined", () => { 10 | assert.strictEqual(utils.manifestAuthor(undefined), null); 11 | }); 12 | 13 | it("should return null when given empty string", () => { 14 | assert.strictEqual(utils.manifestAuthor(""), null); 15 | }); 16 | 17 | it("should return author object with only name", () => { 18 | assert.deepStrictEqual(utils.manifestAuthor("John Doe"), { 19 | name: "John Doe", 20 | email: void 0, 21 | url: void 0 22 | }); 23 | }); 24 | 25 | it("should return author object with name and email", () => { 26 | assert.deepStrictEqual(utils.manifestAuthor("John Doe "), { 27 | name: "John Doe", 28 | email: "john@doe.com", 29 | url: void 0 30 | }); 31 | }); 32 | 33 | it("should return author object with name, email and url", () => { 34 | assert.deepStrictEqual(utils.manifestAuthor("John Doe (john.com)"), { 35 | name: "John Doe", 36 | email: "john@doe.com", 37 | url: "john.com" 38 | }); 39 | }); 40 | 41 | it("should return given author object", () => { 42 | const author = { 43 | name: "John Doe", 44 | email: "john@doe.com", 45 | url: "john.com" 46 | }; 47 | 48 | assert.deepStrictEqual(utils.manifestAuthor(author), author); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /workspaces/scanner/test/utils/warnings.spec.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Third-party Dependencies 6 | import * as i18n from "@nodesecure/i18n"; 7 | 8 | // Import Internal Dependencies 9 | import { getDependenciesWarnings } from "../../src/utils/index.js"; 10 | 11 | function createDependency(maintainers = [], publishers = []) { 12 | return { 13 | metadata: { 14 | authors: { 15 | name: "John Doe", 16 | email: "john.doe@gmail.com" 17 | }, 18 | maintainers, 19 | publishers 20 | } 21 | }; 22 | } 23 | 24 | test("getDependenciesWarnings for '@scarf/scarf'", async() => { 25 | const deps = new Map([ 26 | ["@scarf/scarf", createDependency()] 27 | ]); 28 | 29 | const warnsArray = await getDependenciesWarnings(deps); 30 | assert.strictEqual(warnsArray.warnings.length, 1); 31 | 32 | const message = await i18n.getToken("scanner.disable_scarf"); 33 | assert.ok( 34 | warnsArray.warnings[0].includes(message) 35 | ); 36 | }); 37 | -------------------------------------------------------------------------------- /workspaces/scanner/test/verify.spec.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import path from "node:path"; 3 | import fs from "node:fs"; 4 | import assert from "node:assert"; 5 | import { fileURLToPath } from "node:url"; 6 | import { test } from "node:test"; 7 | 8 | // Import Internal Dependencies 9 | import { verify } from "../src/index.js"; 10 | 11 | // CONSTANTS 12 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 13 | const kFixturePath = path.join(__dirname, "fixtures", "verify"); 14 | 15 | test("verify 'express' package", async() => { 16 | const data = await verify("express@4.17.0"); 17 | data.files.extensions.sort(); 18 | 19 | assert.deepEqual(data.files, { 20 | list: [ 21 | "History.md", 22 | "LICENSE", 23 | "Readme.md", 24 | "index.js", 25 | "lib\\application.js", 26 | "lib\\express.js", 27 | "lib\\middleware\\init.js", 28 | "lib\\middleware\\query.js", 29 | "lib\\request.js", 30 | "lib\\response.js", 31 | "lib\\router\\index.js", 32 | "lib\\router\\layer.js", 33 | "lib\\router\\route.js", 34 | "lib\\utils.js", 35 | "lib\\view.js", 36 | "package.json" 37 | ].map((location) => location.replaceAll("\\", path.sep)), 38 | extensions: ["", ".md", ".js", ".json"].sort(), 39 | minified: [] 40 | }); 41 | assert.ok(data.directorySize > 0); 42 | 43 | // licenses 44 | assert.deepEqual(data.uniqueLicenseIds, ["MIT"]); 45 | assert.deepEqual(data.licenses, [ 46 | { 47 | licenses: { 48 | MIT: "https://spdx.org/licenses/MIT.html#licenseText" 49 | }, 50 | spdx: { 51 | osi: true, 52 | fsf: true, 53 | fsfAndOsi: true, 54 | includesDeprecated: false 55 | }, 56 | fileName: "package.json" 57 | }, 58 | { 59 | licenses: { 60 | MIT: "https://spdx.org/licenses/MIT.html#licenseText" 61 | }, 62 | spdx: { 63 | osi: true, 64 | fsf: true, 65 | fsfAndOsi: true, 66 | includesDeprecated: false 67 | }, 68 | fileName: "LICENSE" 69 | } 70 | ]); 71 | 72 | assert.ok(data.ast.warnings.length === 1); 73 | const warningName = data.ast.warnings.map((row) => row.kind); 74 | assert.deepEqual(warningName, ["unsafe-import"]); 75 | 76 | const expectedResult = JSON.parse( 77 | fs.readFileSync(path.join(kFixturePath, "express-result.json"), "utf-8") 78 | .replaceAll("\\", path.sep) 79 | .replaceAll("//", "/") 80 | ); 81 | assert.deepEqual(data.ast.dependencies, expectedResult); 82 | }); 83 | -------------------------------------------------------------------------------- /workspaces/scanner/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | }, 7 | "include": ["src"], 8 | "references": [ 9 | { 10 | "path": "../npm-types" 11 | }, 12 | { 13 | "path": "../conformance" 14 | }, 15 | { 16 | "path": "../mama" 17 | }, 18 | { 19 | "path": "../tarball" 20 | }, 21 | { 22 | "path": "../tree-walker" 23 | }, 24 | { 25 | "path": "../rc" 26 | }, 27 | { 28 | "path": "../i18n" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /workspaces/tarball/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @nodesecure/tarball 2 | 3 | ## 1.2.0 4 | 5 | ### Minor Changes 6 | 7 | - [#401](https://github.com/NodeSecure/scanner/pull/401) [`0137bc6`](https://github.com/NodeSecure/scanner/commit/0137bc6060fe56c673b1ab92214debe63ce35958) Thanks [@clemgbld](https://github.com/clemgbld)! - (Tarball) detect and flag with hasExternalCapacity when native fetch is used 8 | 9 | - [#404](https://github.com/NodeSecure/scanner/pull/404) [`40a9350`](https://github.com/NodeSecure/scanner/commit/40a93507e20e1002059f71a40539dfd058879257) Thanks [@fraxken](https://github.com/fraxken)! - Implement new DependencyVersion type to detect the kind of module (cjs/esm/dual..) 10 | 11 | ### Patch Changes 12 | 13 | - [#400](https://github.com/NodeSecure/scanner/pull/400) [`55af858`](https://github.com/NodeSecure/scanner/commit/55af858f993520bca6f0fc5b0dbddf0b329ab5e0) Thanks [@fraxken](https://github.com/fraxken)! - fix file import detection and avoid confusion with package with dots 14 | 15 | - Updated dependencies [[`3ee9a2e`](https://github.com/NodeSecure/scanner/commit/3ee9a2e17c877e7ea6fe23fc4ffc86578e6d0b72)]: 16 | - @nodesecure/mama@1.2.0 17 | -------------------------------------------------------------------------------- /workspaces/tarball/README.md: -------------------------------------------------------------------------------- 1 |

2 | @nodesecure/tarball 3 |

4 | 5 |

6 | Utilities to extract and deeply analyze NPM tarball 7 |

8 | 9 | ## Requirements 10 | - [Node.js](https://nodejs.org/en/) v20 or higher 11 | 12 | ## Getting Started 13 | 14 | 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). 15 | 16 | ```bash 17 | $ npm i @nodesecure/tarball 18 | # or 19 | $ yarn add @nodesecure/tarball 20 | ``` 21 | 22 | ## Usage example 23 | 24 | ```ts 25 | import * as tarball from "@nodesecure/tarball"; 26 | 27 | const scanResult = await tarball.scanPackage( 28 | process.cwd() 29 | ); 30 | console.log(scanResult); 31 | ``` 32 | 33 | > [!NOTE] 34 | > This package has been designed to be used by the Scanner package/workspace. 35 | 36 | ## API 37 | 38 | ### scanDirOrArchive 39 | 40 | Method created for Scanner (to be refactored soon) 41 | 42 | ```ts 43 | export interface scanDirOrArchiveOptions { 44 | ref: DependencyRef; 45 | location?: string; 46 | tmpLocation?: null | string; 47 | locker: Locker; 48 | registry: string; 49 | } 50 | ``` 51 | 52 | ### scanPackage(dest: string, packageName?: string): Promise< ScannedPackageResult > 53 | 54 | Scan a given tarball archive or a local project. 55 | 56 | ```ts 57 | interface ScannedPackageResult { 58 | files: { 59 | /** Complete list of files for the given package */ 60 | list: string[]; 61 | /** Complete list of extensions (.js, .md etc.) */ 62 | extensions: string[]; 63 | /** List of minified javascript files */ 64 | minified: string[]; 65 | }; 66 | /** Size of the directory in bytes */ 67 | directorySize: number; 68 | /** Unique license contained in the tarball (MIT, ISC ..) */ 69 | uniqueLicenseIds: string[]; 70 | /** All licenses with their SPDX */ 71 | licenses: ntlp.SpdxLicenseConformance[]; 72 | ast: { 73 | dependencies: Record>; 74 | warnings: Warning[]; 75 | }; 76 | } 77 | ``` 78 | 79 | ## License 80 | MIT 81 | -------------------------------------------------------------------------------- /workspaces/tarball/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nodesecure/tarball", 3 | "version": "1.2.0", 4 | "description": "NodeSecure tarball scanner", 5 | "type": "module", 6 | "exports": "./dist/index.js", 7 | "types": "./dist/index.d.ts", 8 | "scripts": { 9 | "build": "tsc -b", 10 | "prepublishOnly": "npm run build", 11 | "test-only": "tsx --test ./test/**/*.spec.ts", 12 | "test": "c8 -r html npm run test-only" 13 | }, 14 | "files": [ 15 | "dist" 16 | ], 17 | "keywords": [ 18 | "NodeSecure", 19 | "tarball" 20 | ], 21 | "author": "GENTILHOMME Thomas ", 22 | "license": "MIT", 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/NodeSecure/scanner.git" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/NodeSecure/scanner/issues" 29 | }, 30 | "homepage": "https://github.com/NodeSecure/tree/master/workspaces/tarball#readme", 31 | "dependencies": { 32 | "@nodesecure/conformance": "^1.0.0", 33 | "@nodesecure/fs-walk": "^2.0.0", 34 | "@nodesecure/js-x-ray": "^8.2.0", 35 | "@nodesecure/mama": "^1.2.0", 36 | "@nodesecure/npm-types": "^1.2.0", 37 | "@nodesecure/utils": "^2.1.0", 38 | "pacote": "^21.0.0" 39 | }, 40 | "devDependencies": { 41 | "get-folder-size": "^5.0.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /workspaces/tarball/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./tarball.js"; 2 | -------------------------------------------------------------------------------- /workspaces/tarball/src/sast/file.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import path from "node:path"; 3 | 4 | // Import Third-party Dependencies 5 | import { 6 | AstAnalyser, 7 | type WarningName, 8 | type WarningDefault 9 | } from "@nodesecure/js-x-ray"; 10 | 11 | // Import Internal Dependencies 12 | import { 13 | filterDependencyKind 14 | } from "../utils/index.js"; 15 | 16 | // CONSTANTS 17 | const kJsExtname = new Set([".js", ".mjs", ".cjs"]); 18 | 19 | export interface scanFileReport { 20 | file: string; 21 | warnings: (Omit, "value"> & { file: string; })[]; 22 | isMinified: boolean; 23 | tryDependencies: string[]; 24 | dependencies: string[]; 25 | filesDependencies: string[]; 26 | filesFlags: { 27 | hasExternalCapacity: boolean; 28 | }; 29 | } 30 | 31 | export async function scanFile( 32 | destination: string, 33 | file: string, 34 | packageName: string 35 | ): Promise { 36 | const result = await new AstAnalyser().analyseFile( 37 | path.join(destination, file), 38 | { 39 | packageName 40 | } 41 | ); 42 | 43 | const warnings = result.warnings.map((curr) => Object.assign({}, curr, { file })); 44 | if (result.ok) { 45 | const { packages, files } = filterDependencyKind( 46 | [...result.dependencies.keys()], 47 | path.dirname(file) 48 | ); 49 | 50 | const tryDependencies = [...result.dependencies.entries()] 51 | .flatMap(([name, dependency]) => (dependency.inTry ? [name] : [])); 52 | 53 | return { 54 | file, 55 | warnings, 56 | isMinified: result.isMinified, 57 | tryDependencies, 58 | dependencies: packages, 59 | filesDependencies: files, 60 | filesFlags: { 61 | hasExternalCapacity: result.flags.has("fetch") 62 | } 63 | }; 64 | } 65 | 66 | return { 67 | file, 68 | warnings, 69 | isMinified: false, 70 | tryDependencies: [], 71 | dependencies: [], 72 | filesDependencies: [], 73 | filesFlags: { 74 | hasExternalCapacity: false 75 | } 76 | }; 77 | } 78 | 79 | export async function scanManyFiles( 80 | files: string[], 81 | destination: string, 82 | packageName: string 83 | ): Promise { 84 | const scannedFiles = await Promise.allSettled( 85 | files 86 | .filter((fileName) => kJsExtname.has(path.extname(fileName))) 87 | .map((file) => scanFile(destination, file, packageName)) 88 | ); 89 | 90 | return scannedFiles 91 | .filter((result) => result.status === "fulfilled") 92 | .map((result) => result.value); 93 | } 94 | -------------------------------------------------------------------------------- /workspaces/tarball/src/sast/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./file.js"; 2 | -------------------------------------------------------------------------------- /workspaces/tarball/src/utils/booleanToFlags.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @example 3 | * console.log(...booleanToFlags({ hasScript: true })); // "hasScript" 4 | */ 5 | export function* booleanToFlags( 6 | flagsRecord: Record 7 | ): IterableIterator { 8 | for (const [flagName, boolValue] of Object.entries(flagsRecord)) { 9 | if (boolValue) { 10 | yield flagName; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /workspaces/tarball/src/utils/filterDependencyKind.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import path from "node:path"; 3 | 4 | // CONSTANTS 5 | const kRelativeImportPath = new Set([".", "..", "./", "../"]); 6 | 7 | /** 8 | * @see https://nodejs.org/docs/latest/api/modules.html#file-modules 9 | */ 10 | export function filterDependencyKind( 11 | dependencies: string[], 12 | relativeFileLocation: string 13 | ): { packages: string[]; files: string[]; } { 14 | const packages: string[] = []; 15 | const files: string[] = []; 16 | 17 | for (const moduleNameOrPath of dependencies) { 18 | const firstChar = moduleNameOrPath.charAt(0); 19 | 20 | /** 21 | * @example 22 | * require(".."); 23 | * require("/home/marco/foo.js"); 24 | */ 25 | if (firstChar === "." || firstChar === "/") { 26 | // Note: condition only possible for CJS 27 | if (kRelativeImportPath.has(moduleNameOrPath)) { 28 | files.push(path.join(moduleNameOrPath, "index.js")); 29 | } 30 | else { 31 | // Note: we are speculating that the extension is .js (but it could be .json or .node) 32 | const fixedFileName = path.extname(moduleNameOrPath) === "" ? 33 | `${moduleNameOrPath}.js` : moduleNameOrPath; 34 | 35 | files.push(path.join(relativeFileLocation, fixedFileName)); 36 | } 37 | } 38 | else { 39 | packages.push(moduleNameOrPath); 40 | } 41 | } 42 | 43 | return { packages, files }; 44 | } 45 | -------------------------------------------------------------------------------- /workspaces/tarball/src/utils/getPackageName.ts: -------------------------------------------------------------------------------- 1 | // CONSTANTS 2 | const kPackageSeparator = "/"; 3 | const kPackageOrgSymbol = "@"; 4 | 5 | /** 6 | * @see https://github.com/npm/validate-npm-package-name#naming-rules 7 | * @example 8 | * getPackageName("foo"); // foo 9 | * getPackageName("foo/bar"); // foo 10 | * getPackageName("@org/bar"); // @org/bar 11 | */ 12 | export function getPackageName( 13 | name: string 14 | ): string { 15 | const parts = name.split(kPackageSeparator); 16 | 17 | // Note: only scoped package are allowed to start with @ 18 | return name.startsWith(kPackageOrgSymbol) ? `${parts[0]}/${parts[1]}` : parts[0]!; 19 | } 20 | -------------------------------------------------------------------------------- /workspaces/tarball/src/utils/getTarballComposition.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { Stats, promises as fs } from "node:fs"; 3 | import path from "node:path"; 4 | 5 | // Import Third-party Dependencies 6 | import { walk } from "@nodesecure/fs-walk"; 7 | 8 | export interface TarballComposition { 9 | ext: Set; 10 | size: number; 11 | files: string[]; 12 | } 13 | 14 | export async function getTarballComposition( 15 | tarballDir: string 16 | ): Promise { 17 | const ext = new Set(); 18 | const files: string[] = []; 19 | const dirs: string[] = []; 20 | let { size } = await fs.stat(tarballDir); 21 | 22 | for await (const [dirent, file] of walk(tarballDir)) { 23 | if (dirent.isFile()) { 24 | ext.add(path.extname(file)); 25 | files.push(file); 26 | } 27 | else if (dirent.isDirectory()) { 28 | dirs.push(file); 29 | } 30 | } 31 | 32 | const sizeUnfilteredResult = await Promise.allSettled([ 33 | ...files.map((file) => fs.stat(file)), 34 | ...dirs.map((file) => fs.stat(file)) 35 | ]); 36 | const sizeAll = sizeUnfilteredResult 37 | .filter((promiseSettledResult) => promiseSettledResult.status === "fulfilled") 38 | .map((promiseSettledResult) => (promiseSettledResult as PromiseFulfilledResult).value); 39 | size += sizeAll.reduce((prev, curr) => prev + curr.size, 0); 40 | 41 | return { 42 | ext, 43 | size, 44 | files: files.map((fileLocation) => path.relative(tarballDir, fileLocation)).sort() 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /workspaces/tarball/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./analyzeDependencies.js"; 2 | export * from "./booleanToFlags.js"; 3 | export * from "./isSensitiveFile.js"; 4 | export * from "./getPackageName.js"; 5 | export * from "./getTarballComposition.js"; 6 | export * from "./filterDependencyKind.js"; 7 | -------------------------------------------------------------------------------- /workspaces/tarball/src/utils/isSensitiveFile.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import path from "node:path"; 3 | 4 | // CONSTANTS 5 | const kSensitiveFileName = new Set([".npmrc", ".env"]); 6 | const kSensitiveFileExtension = new Set([".key", ".pem"]); 7 | 8 | /** 9 | * @see https://github.com/jandre/safe-commit-hook/blob/master/git-deny-patterns.json 10 | */ 11 | export function isSensitiveFile( 12 | fileName: string 13 | ): boolean { 14 | return kSensitiveFileName.has(path.basename(fileName)) || 15 | kSensitiveFileExtension.has(path.extname(fileName)); 16 | } 17 | -------------------------------------------------------------------------------- /workspaces/tarball/src/warnings.ts: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import type { WarningDefault } from "@nodesecure/js-x-ray"; 3 | 4 | export function getSemVerWarning( 5 | value: string 6 | ): WarningDefault<"zero-semver"> { 7 | return { 8 | kind: "zero-semver", 9 | file: "package.json", 10 | value, 11 | location: null, 12 | i18n: "sast_warnings.zeroSemVer", 13 | severity: "Information", 14 | source: "Scanner", 15 | experimental: false 16 | }; 17 | } 18 | 19 | export function getEmptyPackageWarning(): WarningDefault<"empty-package"> { 20 | return { 21 | kind: "empty-package", 22 | file: "package.json", 23 | value: "package.json", 24 | location: null, 25 | i18n: "sast_warnings.emptyPackage", 26 | severity: "Critical", 27 | source: "Scanner", 28 | experimental: false 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /workspaces/tarball/test/fixtures/getTarballComposition/one/README: -------------------------------------------------------------------------------- 1 | HELLO 2 | -------------------------------------------------------------------------------- /workspaces/tarball/test/fixtures/getTarballComposition/two/empty.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NodeSecure/scanner/97a36b523aa9b22900cd4ad822aa6a083e254121/workspaces/tarball/test/fixtures/getTarballComposition/two/empty.txt -------------------------------------------------------------------------------- /workspaces/tarball/test/fixtures/getTarballComposition/two/package.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /workspaces/tarball/test/fixtures/getTarballComposition/two/two-deep/test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | console.log("hello world"); 4 | -------------------------------------------------------------------------------- /workspaces/tarball/test/fixtures/scanJavascriptFile/fetch.js: -------------------------------------------------------------------------------- 1 | const apiService = async (url) => fetch(url); 2 | 3 | export default apiService; -------------------------------------------------------------------------------- /workspaces/tarball/test/fixtures/scanJavascriptFile/one.js: -------------------------------------------------------------------------------- 1 | require("./src/foo.js"); 2 | require("http"); 3 | require("mocha"); 4 | require("/home/marco"); 5 | require("yolo"); 6 | 7 | -------------------------------------------------------------------------------- /workspaces/tarball/test/fixtures/scanJavascriptFile/onelineStmt.min.js: -------------------------------------------------------------------------------- 1 | require("./foobar.js"); 2 | -------------------------------------------------------------------------------- /workspaces/tarball/test/fixtures/scanJavascriptFile/parsingError.js: -------------------------------------------------------------------------------- 1 | cqd,;s ù 2 | 3 | LBLM. SDF 4 | £ M SMV 5 | £LM? SV 6 | 7 | ?P?V 8 | -------------------------------------------------------------------------------- /workspaces/tarball/test/fixtures/scanJavascriptFile/two.min.js: -------------------------------------------------------------------------------- 1 | try { 2 | require("http"); 3 | } 4 | catch { 5 | // ignore 6 | } 7 | 8 | require("fs"); 9 | -------------------------------------------------------------------------------- /workspaces/tarball/test/fixtures/scanPackage/caseone/.gitignore: -------------------------------------------------------------------------------- 1 | ololo 2 | -------------------------------------------------------------------------------- /workspaces/tarball/test/fixtures/scanPackage/caseone/foobar.txt: -------------------------------------------------------------------------------- 1 | hello world 2 | -------------------------------------------------------------------------------- /workspaces/tarball/test/fixtures/scanPackage/caseone/index.js: -------------------------------------------------------------------------------- 1 | require("./src/deps.js"); 2 | 3 | try { 4 | const fs = require("fs"); 5 | } 6 | catch { 7 | // do nothing 8 | } 9 | const kleur = require("kleur"); 10 | -------------------------------------------------------------------------------- /workspaces/tarball/test/fixtures/scanPackage/caseone/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "caseone", 3 | "type": "commonjs", 4 | "license": "MIT" 5 | } 6 | -------------------------------------------------------------------------------- /workspaces/tarball/test/fixtures/scanPackage/caseone/src/deps.js: -------------------------------------------------------------------------------- 1 | const http = require("http"); 2 | 3 | const VER = "1.0.0"; 4 | module.exports = VER; 5 | -------------------------------------------------------------------------------- /workspaces/tarball/test/fixtures/scanPackage/caseone/src/other.min.js: -------------------------------------------------------------------------------- 1 | require("kleur"); 2 | require("http"); 3 | 4 | const foo = "hello world"; 5 | -------------------------------------------------------------------------------- /workspaces/tarball/test/sast/scanFile.spec.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import path from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | import { test } from "node:test"; 5 | import assert from "node:assert"; 6 | 7 | // Import Internal Dependencies 8 | import { scanFile } from "../../src/sast/index.js"; 9 | 10 | // CONSTANTS 11 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 12 | const kFixturePath = path.join(__dirname, "..", "fixtures", "scanJavascriptFile"); 13 | 14 | test("scanFile (fixture one.js)", async() => { 15 | const result = await scanFile(kFixturePath, "one.js", "yolo"); 16 | assert.deepEqual(result, { 17 | file: "one.js", 18 | warnings: [], 19 | isMinified: false, 20 | tryDependencies: [], 21 | dependencies: ["http", "mocha"], 22 | filesDependencies: ["src\\foo.js", "home\\marco.js"].map((location) => location.replaceAll("\\", path.sep)), 23 | filesFlags: { 24 | hasExternalCapacity: false 25 | } 26 | }); 27 | }); 28 | 29 | test("scanFile (fixture two.min.js)", async() => { 30 | const result = await scanFile(kFixturePath, "two.min.js", "yolo"); 31 | assert.deepEqual(result, { 32 | file: "two.min.js", 33 | warnings: [], 34 | isMinified: true, 35 | tryDependencies: ["http"], 36 | dependencies: ["http", "fs"], 37 | filesDependencies: [], 38 | filesFlags: { 39 | hasExternalCapacity: false 40 | } 41 | }); 42 | }); 43 | 44 | test("scanFile (fixture onelineStmt.min.js)", async() => { 45 | const result = await scanFile(kFixturePath, "onelineStmt.min.js", "yolo"); 46 | assert.deepEqual(result, { 47 | file: "onelineStmt.min.js", 48 | warnings: [], 49 | isMinified: false, 50 | tryDependencies: [], 51 | dependencies: [], 52 | filesDependencies: ["foobar.js"], 53 | filesFlags: { 54 | hasExternalCapacity: false 55 | } 56 | }); 57 | }); 58 | 59 | test("scanFile (fixture parsingError.js)", async() => { 60 | const result = await scanFile(kFixturePath, "parsingError.js", "yolo"); 61 | 62 | assert.deepEqual(result, { 63 | file: "parsingError.js", 64 | warnings: [ 65 | { 66 | kind: "parsing-error", 67 | value: "[1:4-1:5]: Unexpected token: ';'", 68 | location: [[0, 0], [0, 0]], 69 | file: "parsingError.js" 70 | } 71 | ], 72 | isMinified: false, 73 | tryDependencies: [], 74 | dependencies: [], 75 | filesDependencies: [], 76 | filesFlags: { 77 | hasExternalCapacity: false 78 | } 79 | }); 80 | }); 81 | 82 | test("scanFile (fixture fetch.js)", async() => { 83 | const result = await scanFile(kFixturePath, "fetch.js", "yolo"); 84 | assert.deepEqual(result, { 85 | file: "fetch.js", 86 | warnings: [], 87 | isMinified: false, 88 | tryDependencies: [], 89 | dependencies: [], 90 | filesDependencies: [], 91 | filesFlags: { 92 | hasExternalCapacity: true 93 | } 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /workspaces/tarball/test/tarball/scanPackage.spec.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import path from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | import { test } from "node:test"; 5 | import assert from "node:assert"; 6 | 7 | // Import Internal Dependencies 8 | import { scanPackage } from "../../src/index.js"; 9 | 10 | // CONSTANTS 11 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 12 | const kFixturePath = path.join(__dirname, "..", "fixtures", "scanPackage"); 13 | 14 | test("scanPackage (caseone)", async() => { 15 | const result = await scanPackage( 16 | path.join(kFixturePath, "caseone") 17 | ); 18 | result.files.extensions.sort(); 19 | 20 | assert.deepEqual(result.files, { 21 | list: [ 22 | ".gitignore", 23 | "foobar.txt", 24 | "index.js", 25 | "package.json", 26 | "src\\deps.js", 27 | "src\\other.min.js" 28 | ].map((location) => location.replace(/\\/g, path.sep)), 29 | extensions: [ 30 | "", 31 | ".txt", 32 | ".js", 33 | ".json" 34 | ].sort(), 35 | minified: [ 36 | "src\\other.min.js" 37 | ].map((location) => location.replace(/\\/g, path.sep)) 38 | }); 39 | 40 | assert.ok(typeof result.directorySize === "number", "directorySize should be a number"); 41 | assert.ok(result.directorySize > 0, "directorySize has a size different of zero"); 42 | 43 | assert.deepEqual(result.uniqueLicenseIds, ["MIT"], "Unique license ID should only contain MIT"); 44 | assert.deepEqual(result.licenses, [ 45 | { 46 | fileName: "package.json", 47 | licenses: { 48 | MIT: "https://spdx.org/licenses/MIT.html#licenseText" 49 | }, 50 | spdx: { 51 | osi: true, 52 | fsf: true, 53 | fsfAndOsi: true, 54 | includesDeprecated: false 55 | } 56 | } 57 | ]); 58 | 59 | assert.ok(result.ast.warnings.length === 0); 60 | assert.deepEqual(Object.keys(result.ast.dependencies), [ 61 | "index.js", 62 | "src\\deps.js", 63 | "src\\other.min.js" 64 | ].map((location) => location.replace(/\\/g, path.sep))); 65 | assert.deepEqual(Object.keys(result.ast.dependencies["index.js"]), [ 66 | "./src/deps.js", 67 | "fs", 68 | "kleur" 69 | ]); 70 | assert.ok(result.ast.dependencies["index.js"].fs.inTry); 71 | }); 72 | -------------------------------------------------------------------------------- /workspaces/tarball/test/utils/booleanToFlags.spec.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Internal Dependencies 6 | import { booleanToFlags } from "../../src/utils/index.js"; 7 | 8 | test("booleanToFlags should transform the Record in flag list where value are true", () => { 9 | const flags = booleanToFlags({ hasScript: true, foo: false, bar: true }); 10 | assert.deepEqual([...flags], ["hasScript", "bar"]); 11 | }); 12 | -------------------------------------------------------------------------------- /workspaces/tarball/test/utils/filterDependencyKind.spec.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import path from "node:path"; 3 | import { test } from "node:test"; 4 | import assert from "node:assert"; 5 | 6 | // Import Internal Dependencies 7 | import { filterDependencyKind } from "../../src/utils/index.js"; 8 | 9 | test("filterDependencyKind should be able to split files and packages", () => { 10 | const result = filterDependencyKind(["mocha", "."], process.cwd()); 11 | assert.deepEqual(result.files, ["index.js"]); 12 | assert.deepEqual(result.packages, ["mocha"]); 13 | }); 14 | 15 | test("filterDependencyKind should be able to match all relative import path", () => { 16 | const result = filterDependencyKind([".", "./", "..", "../"], process.cwd()); 17 | assert.deepEqual(result.files, [ 18 | "index.js", 19 | "index.js", 20 | "..\\index.js", 21 | "..\\index.js" 22 | ].map((location) => location.replaceAll("\\", path.sep))); 23 | assert.deepEqual(result.packages, []); 24 | }); 25 | 26 | test("filterDependencyKind should be able to match a file and join with the relative path", () => { 27 | const result = filterDependencyKind(["./foobar.js"], process.cwd()); 28 | assert.deepEqual(result.files, [ 29 | path.join(process.cwd(), "foobar.js") 30 | ]); 31 | assert.deepEqual(result.packages, []); 32 | }); 33 | 34 | test("filterDependencyKind should be able to automatically append the '.js' extension", () => { 35 | const result = filterDependencyKind(["./foobar"], process.cwd()); 36 | assert.deepEqual(result.files, [ 37 | path.join(process.cwd(), "foobar.js") 38 | ]); 39 | assert.deepEqual(result.packages, []); 40 | }); 41 | -------------------------------------------------------------------------------- /workspaces/tarball/test/utils/getPackageName.spec.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Internal Dependencies 6 | import { getPackageName } from "../../src/utils/index.js"; 7 | 8 | test("getPackageName should return the package name (if there is not slash char at all)", () => { 9 | assert.deepStrictEqual(getPackageName("mocha"), "mocha"); 10 | }); 11 | 12 | test("getPackageName should return the package name (first part before '/' character)", () => { 13 | assert.deepStrictEqual(getPackageName("foo/bar"), "foo"); 14 | }); 15 | 16 | test("getPackageName should return the package name with organization namespace", () => { 17 | assert.deepStrictEqual(getPackageName("@slimio/is/test"), "@slimio/is"); 18 | }); 19 | -------------------------------------------------------------------------------- /workspaces/tarball/test/utils/getTarballComposition.spec.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import path from "node:path"; 3 | import assert from "node:assert"; 4 | import { fileURLToPath } from "node:url"; 5 | import { test } from "node:test"; 6 | 7 | // Import Third-party Dependencies 8 | import getSize from "get-folder-size"; 9 | 10 | // Import Internal Dependencies 11 | import { getTarballComposition } from "../../src/utils/index.js"; 12 | 13 | // CONSTANTS 14 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 15 | const kFixturePath = path.join(__dirname, "..", "fixtures", "getTarballComposition"); 16 | 17 | test("should return the composition of a directory", async() => { 18 | const composition = await getTarballComposition(kFixturePath); 19 | const size = await getSize.loose(kFixturePath); 20 | 21 | assert.deepEqual(composition, { 22 | ext: new Set(["", ".js", ".json", ".txt"]), 23 | size, 24 | files: ["one\\README", "two\\empty.txt", "two\\package.json", "two\\two-deep\\test.js"] 25 | .map((location) => location.replaceAll("\\", path.sep)) 26 | }); 27 | assert.strictEqual(composition.files.length, 4); 28 | assert.match(composition.files[0], /one(\/|\\)README/); 29 | }); 30 | -------------------------------------------------------------------------------- /workspaces/tarball/test/utils/isSensitiveFile.spec.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Internal Dependencies 6 | import { isSensitiveFile } from "../../src/utils/index.js"; 7 | 8 | test("isSensitiveFile should return true for sensitive files", () => { 9 | assert.ok(isSensitiveFile(".npmrc")); 10 | assert.ok(isSensitiveFile(".env")); 11 | }); 12 | 13 | test("isSensitiveFile should return true for sensitive extensions", () => { 14 | assert.ok(isSensitiveFile("lol.key"), ".key extension is sensible"); 15 | assert.ok(isSensitiveFile("bar.pem"), ".pem extension is sensible"); 16 | }); 17 | 18 | test("isSensitiveFile should return false for classical extension or file name", () => { 19 | assert.ok(!isSensitiveFile("test.js")); 20 | assert.ok(!isSensitiveFile(".eslintrc")); 21 | }); 22 | -------------------------------------------------------------------------------- /workspaces/tarball/test/warnings.spec.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import assert from "node:assert"; 3 | import { test } from "node:test"; 4 | 5 | // Import Internal Dependencies 6 | import { getSemVerWarning } from "../src/warnings.js"; 7 | 8 | // CONSTANTS 9 | const kDefaultWarning = { 10 | kind: "zero-semver", 11 | file: "package.json", 12 | location: null, 13 | i18n: "sast_warnings.zeroSemVer", 14 | severity: "Information", 15 | source: "Scanner", 16 | experimental: false 17 | }; 18 | 19 | test("getSemVerWarning should return a warning for any SemVer starting with 0.x", () => { 20 | assert.deepEqual(getSemVerWarning("0"), { 21 | value: "0", ...kDefaultWarning 22 | }); 23 | 24 | assert.deepEqual(getSemVerWarning("0.0"), { 25 | value: "0.0", ...kDefaultWarning 26 | }); 27 | 28 | assert.deepEqual(getSemVerWarning("0.0.0"), { 29 | value: "0.0.0", ...kDefaultWarning 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /workspaces/tarball/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | }, 7 | "include": ["src"], 8 | "references": [ 9 | { 10 | "path": "../npm-types" 11 | }, 12 | { 13 | "path": "../conformance" 14 | }, 15 | { 16 | "path": "../mama" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /workspaces/tree-walker/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @nodesecure/tree-walker 2 | 3 | ## 1.3.0 4 | 5 | ### Minor Changes 6 | 7 | - [#404](https://github.com/NodeSecure/scanner/pull/404) [`40a9350`](https://github.com/NodeSecure/scanner/commit/40a93507e20e1002059f71a40539dfd058879257) Thanks [@fraxken](https://github.com/fraxken)! - Implement new DependencyVersion type to detect the kind of module (cjs/esm/dual..) 8 | -------------------------------------------------------------------------------- /workspaces/tree-walker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nodesecure/tree-walker", 3 | "version": "1.3.0", 4 | "description": "NodeSecure tree walker", 5 | "type": "module", 6 | "exports": "./dist/index.js", 7 | "types": "./dist/index.d.ts", 8 | "scripts": { 9 | "build": "tsc -b", 10 | "prepublishOnly": "npm run build", 11 | "test-only": "tsx --test ./test/**/*.spec.ts", 12 | "test": "c8 -r html npm run test-only" 13 | }, 14 | "files": [ 15 | "dist" 16 | ], 17 | "keywords": [ 18 | "NodeSecure", 19 | "tree", 20 | "walker" 21 | ], 22 | "author": "GENTILHOMME Thomas ", 23 | "license": "MIT", 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/NodeSecure/scanner.git" 27 | }, 28 | "bugs": { 29 | "url": "https://github.com/NodeSecure/scanner/issues" 30 | }, 31 | "homepage": "https://github.com/NodeSecure/tree/master/workspaces/tree-walker#readme", 32 | "dependencies": { 33 | "@nodesecure/js-x-ray": "^8.1.0", 34 | "@nodesecure/npm-registry-sdk": "^3.0.0", 35 | "@nodesecure/npm-types": "^1.1.0", 36 | "@npmcli/arborist": "^9.0.2", 37 | "combine-async-iterators": "^3.0.0", 38 | "itertools": "^2.3.1", 39 | "npm-pick-manifest": "^10.0.0", 40 | "pacote": "^21.0.0", 41 | "semver": "^7.6.0" 42 | }, 43 | "devDependencies": { 44 | "@types/npmcli__arborist": "^6.3.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /workspaces/tree-walker/src/Dependency.class.ts: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import type { Warning, WarningDefault } from "@nodesecure/js-x-ray"; 3 | import type { PackageModuleType } from "@nodesecure/mama"; 4 | 5 | export type NpmSpec = `${string}@${string}`; 6 | 7 | export interface DependencyJSON { 8 | id: number; 9 | type: PackageModuleType; 10 | name: string; 11 | version: string; 12 | usedBy: Record; 13 | isDevDependency: boolean; 14 | existOnRemoteRegistry: boolean; 15 | flags: string[]; 16 | warnings: Warning[]; 17 | alias: Record; 18 | dependencyCount: number; 19 | gitUrl: string | null; 20 | } 21 | 22 | export type DependencyOptions = { 23 | parent?: Dependency; 24 | } & Partial>; 25 | 26 | export class Dependency { 27 | static currentId = 1; 28 | 29 | public name: string; 30 | public version: string; 31 | public dev = false; 32 | public existOnRemoteRegistry = true; 33 | public dependencyCount = 0; 34 | public gitUrl: null | string = null; 35 | public warnings: Warning[] = []; 36 | public alias: Record = {}; 37 | 38 | #flags = new Set(); 39 | #parent: null | Dependency = null; 40 | 41 | constructor( 42 | name: string, 43 | version: string, 44 | options: DependencyOptions = {} 45 | ) { 46 | this.name = name; 47 | this.version = version; 48 | const { parent = null, ...props } = options; 49 | 50 | if (parent !== null) { 51 | parent.addChildren(); 52 | } 53 | this.#parent = parent; 54 | 55 | Object.assign(this, props); 56 | } 57 | 58 | addChildren() { 59 | this.dependencyCount += 1; 60 | } 61 | 62 | get spec(): NpmSpec { 63 | return `${this.name}@${this.version}`; 64 | } 65 | 66 | get flags() { 67 | return [...this.#flags]; 68 | } 69 | 70 | get parent() { 71 | return this.#parent === null ? {} : { [this.#parent.name]: this.#parent.version }; 72 | } 73 | 74 | addFlag(flagName: string, predicate = true) { 75 | if (typeof flagName !== "string") { 76 | throw new TypeError("flagName argument must be typeof string"); 77 | } 78 | 79 | if (predicate) { 80 | if (flagName === "hasDependencies" && this.#parent !== null) { 81 | this.#parent.addFlag("hasIndirectDependencies"); 82 | } 83 | 84 | this.#flags.add(flagName); 85 | } 86 | } 87 | 88 | isGit(url?: string) { 89 | this.#flags.add("isGit"); 90 | if (typeof url === "string") { 91 | this.gitUrl = url; 92 | } 93 | 94 | return this; 95 | } 96 | 97 | exportAsPlainObject(customId?: number): DependencyJSON { 98 | if (this.warnings.length > 0) { 99 | this.addFlag("hasWarnings"); 100 | } 101 | 102 | return { 103 | id: typeof customId === "number" ? customId : Dependency.currentId++, 104 | type: "cjs", 105 | name: this.name, 106 | version: this.version, 107 | usedBy: this.parent, 108 | isDevDependency: this.dev, 109 | existOnRemoteRegistry: this.existOnRemoteRegistry, 110 | flags: this.flags, 111 | warnings: this.warnings, 112 | dependencyCount: this.dependencyCount, 113 | gitUrl: this.gitUrl, 114 | alias: this.alias 115 | }; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /workspaces/tree-walker/src/git/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NodeSecure/scanner/97a36b523aa9b22900cd4ad822aa6a083e254121/workspaces/tree-walker/src/git/.gitkeep -------------------------------------------------------------------------------- /workspaces/tree-walker/src/index.ts: -------------------------------------------------------------------------------- 1 | export * as npm from "./npm/walker.js"; 2 | export * from "./Dependency.class.js"; 3 | -------------------------------------------------------------------------------- /workspaces/tree-walker/src/npm/LocalDependencyTreeLoader.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 Arborist from "@npmcli/arborist"; 7 | 8 | // Import Internal Dependencies 9 | import * as utils from "../utils/index.js"; 10 | 11 | export interface LocalDependencyTreeLoaderProvider { 12 | load( 13 | location: string, 14 | registry?: string 15 | ): Promise; 16 | } 17 | 18 | export class LocalDependencyTreeLoader implements LocalDependencyTreeLoaderProvider { 19 | async load( 20 | location: string, 21 | registry?: string 22 | ): Promise { 23 | const arb = new Arborist({ 24 | ...utils.NPM_TOKEN, 25 | path: location, 26 | registry 27 | }); 28 | 29 | try { 30 | await fs.access( 31 | path.join(location, "node_modules") 32 | ); 33 | 34 | await arb.loadActual(); 35 | 36 | return arb.buildIdealTree(); 37 | } 38 | catch { 39 | return arb.loadVirtual(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /workspaces/tree-walker/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | // CONSTANTS 2 | export const NPM_TOKEN = typeof process.env.NODE_SECURE_TOKEN === "string" ? 3 | { token: process.env.NODE_SECURE_TOKEN } : 4 | {}; 5 | 6 | export * from "./mergeDependencies.js"; 7 | export * from "./isGitDependency.js"; 8 | export * from "./semver.js"; 9 | -------------------------------------------------------------------------------- /workspaces/tree-walker/src/utils/isGitDependency.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @example isGitDependency("github:NodeSecure/scanner") // => true 3 | * @example isGitDependency("git+ssh://git@github.com:npm/cli#semver:^5.0") // => true 4 | * @example isGitDependency(">=1.0.2 <2.1.2") // => false 5 | * @example isGitDependency("http://asdf.com/asdf.tar.gz") // => false 6 | * @param {string} version 7 | * @returns {boolean} 8 | */ 9 | export function isGitDependency(version: string): boolean { 10 | return /^git(:|\+|hub:)/.test(version); 11 | } 12 | -------------------------------------------------------------------------------- /workspaces/tree-walker/src/utils/mergeDependencies.ts: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import type { PackageJSON } from "@nodesecure/npm-types"; 3 | 4 | export type NpmDependency = 5 | "dependencies" | 6 | "devDependencies" | 7 | "optionalDependencies" | 8 | "peerDependencies" | 9 | "bundleDependencies" | 10 | "bundledDependencies"; 11 | 12 | export function mergeDependencies( 13 | manifest: Partial, 14 | types: NpmDependency[] = ["dependencies"] as const 15 | ) { 16 | const dependencies = new Map(); 17 | const customResolvers = new Map(); 18 | const alias = new Map(); 19 | 20 | for (const fieldName of types) { 21 | if (!(fieldName in manifest)) { 22 | continue; 23 | } 24 | 25 | const dep = manifest[fieldName] as Record; 26 | 27 | for (const [name, version] of Object.entries(dep)) { 28 | /** 29 | * Version can be file:, github:, git:, git+, ./... 30 | * @see https://docs.npmjs.com/cli/v7/configuring-npm/package-json#dependencies 31 | */ 32 | if (/^([a-zA-Z]+:|git\+|\.\\)/.test(version)) { 33 | customResolvers.set(name, version); 34 | if (!version.startsWith("npm:")) { 35 | continue; 36 | } 37 | alias.set(name, version.slice(4)); 38 | } 39 | 40 | dependencies.set(name, version); 41 | } 42 | } 43 | 44 | return { dependencies, customResolvers, alias }; 45 | } 46 | -------------------------------------------------------------------------------- /workspaces/tree-walker/src/utils/semver.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @example 3 | * cleanRange(">=1.5.0"); // 1.5.0 4 | * cleanRange("^2.0.0"); // 2.0.0 5 | */ 6 | export function cleanRange( 7 | version: string 8 | ): string { 9 | // TODO: how do we handle complicated range like pkg-name@1 || 2 or pkg-name@2.1.2 < 3 10 | const firstChar = version.charAt(0); 11 | if (firstChar === "^" || firstChar === "<" || firstChar === ">" || firstChar === "=" || firstChar === "~") { 12 | return version.slice(version.charAt(1) === "=" ? 2 : 1); 13 | } 14 | 15 | return version; 16 | } 17 | -------------------------------------------------------------------------------- /workspaces/tree-walker/test/Dependency.spec.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Third-party Dependencies 6 | import is from "@slimio/is"; 7 | 8 | // Import Internal Dependencies 9 | import { Dependency } from "../src/Dependency.class.js"; 10 | 11 | test("Dependency class should act as expected by assertions", () => { 12 | assert.ok(is.classObject(Dependency)); 13 | 14 | const dep = new Dependency("semver", "1.0.0"); 15 | assert.deepEqual(dep.parent, {}); 16 | assert.strictEqual(dep.name, "semver"); 17 | assert.strictEqual(dep.version, "1.0.0"); 18 | assert.strictEqual(dep.spec, "semver@1.0.0"); 19 | assert.strictEqual(dep.dev, false); 20 | assert.strictEqual(dep.dependencyCount, 0); 21 | assert.strictEqual(dep.existOnRemoteRegistry, true); 22 | assert.deepEqual(dep.warnings, []); 23 | assert.deepEqual(dep.alias, {}); 24 | assert.strictEqual(dep.gitUrl, null); 25 | assert.strictEqual(Reflect.ownKeys(dep).length, 8); 26 | 27 | const flagOne = dep.flags; 28 | const flagTwo = dep.flags; 29 | assert.deepEqual(flagOne, flagTwo); 30 | assert.ok(flagOne !== flagTwo); 31 | }); 32 | 33 | test("Dependency children should write his parent as usedBy when exported", () => { 34 | const semverDep = new Dependency("semver", "1.0.0"); 35 | const testDep = new Dependency("test", "1.0.0", { 36 | parent: semverDep 37 | }); 38 | 39 | assert.strictEqual(semverDep.dependencyCount, 1); 40 | assert.deepEqual(testDep.parent, { 41 | [semverDep.name]: semverDep.version 42 | }); 43 | 44 | const flatDep = testDep.exportAsPlainObject(void 0); 45 | assert.strictEqual(flatDep.name, "test"); 46 | assert.deepEqual(flatDep.usedBy, { 47 | [semverDep.name]: semverDep.version 48 | }); 49 | }); 50 | 51 | test("Create a dependency with one warning", () => { 52 | const semverDep = new Dependency("semver", "1.0.0"); 53 | const fakeWarning = { foo: "bar" }; 54 | semverDep.warnings.push(fakeWarning as any); 55 | 56 | const flatDep = semverDep.exportAsPlainObject(void 0); 57 | assert.deepEqual(flatDep.flags, ["hasWarnings"]); 58 | assert.strictEqual(flatDep.warnings[0], fakeWarning); 59 | }); 60 | 61 | test("Create a GIT Dependency (flags.isGit must be set to true)", () => { 62 | const semverDep = new Dependency("semver", "1.0.0").isGit(); 63 | assert.deepStrictEqual(semverDep.gitUrl, null); 64 | 65 | const flatSemver = semverDep.exportAsPlainObject(void 0); 66 | assert.ok(flatSemver.flags.includes("isGit")); 67 | 68 | const mochaDep = new Dependency("mocha", "1.0.0").isGit("https://github.com/mochajs/mocha"); 69 | assert.strictEqual(mochaDep.gitUrl, "https://github.com/mochajs/mocha"); 70 | 71 | const flatMocha = mochaDep.exportAsPlainObject(void 0); 72 | assert.ok(flatMocha.flags.includes("isGit")); 73 | }); 74 | 75 | test("Dependency.addFlag should throw a TypeError if flagName is not string", () => { 76 | const semverDep = new Dependency("semver", "1.0.0"); 77 | assert.throws( 78 | () => semverDep.addFlag(10 as any), 79 | { 80 | name: "TypeError", 81 | message: "flagName argument must be typeof string" 82 | } 83 | ); 84 | }); 85 | 86 | test("Dependency.addChildren must increment dependencyCount one by one", () => { 87 | const semverDep = new Dependency("semver", "1.0.0"); 88 | assert.equal(semverDep.dependencyCount, 0); 89 | 90 | semverDep.addChildren(); 91 | assert.equal(semverDep.dependencyCount, 1); 92 | }); 93 | -------------------------------------------------------------------------------- /workspaces/tree-walker/test/fixtures/mergeDependencies/one.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "semver": "^0.1.0", 4 | "test": "~0.5.0" 5 | }, 6 | "devDependencies": { 7 | "ava": "^1.0.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /workspaces/tree-walker/test/fixtures/mergeDependencies/three.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /workspaces/tree-walker/test/fixtures/mergeDependencies/two.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@slimio/is": "^1.4.0", 4 | "custom": "file:\\file.js" 5 | }, 6 | "devDependencies": { 7 | "japa": "~0.1.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /workspaces/tree-walker/test/utils/cleanRange.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 { cleanRange } from "../../src/utils/index.js"; 7 | 8 | describe("cleanRange", () => { 9 | it("should return cleaned SemVer range", () => { 10 | const r1 = cleanRange("0.1.0"); 11 | const r2 = cleanRange("^1.0.0"); 12 | const r3 = cleanRange(">=2.0.0"); 13 | 14 | assert.strictEqual(r1, "0.1.0"); 15 | assert.strictEqual(r2, "1.0.0"); 16 | assert.strictEqual(r3, "2.0.0"); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /workspaces/tree-walker/test/utils/isGitDependency.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 { isGitDependency } from "../../src/utils/index.js"; 7 | 8 | describe("isGitDependency", () => { 9 | it("should return true for git related package versions", () => { 10 | assert.ok(isGitDependency("git+ssh://git@github.com:npm/cli.git#v1.0.27")); 11 | assert.ok(isGitDependency("git+ssh://git@github.com:npm/cli#semver:^5.0")); 12 | assert.ok(isGitDependency("git+https://isaacs@github.com/npm/cli.git")); 13 | assert.ok(isGitDependency("git://github.com/npm/cli.git#v1.0.27")); 14 | assert.ok(isGitDependency("github:NodeSecure/scanner")); 15 | }); 16 | 17 | it("should return false for non git related package versions", () => { 18 | assert.ok(!isGitDependency(">=1.0.2 <2.1.2")); 19 | assert.ok(!isGitDependency("1.0.0 - 2.9999.9999")); 20 | assert.ok(!isGitDependency(">1.0.2 <=2.3.4")); 21 | assert.ok(!isGitDependency("2.0.1")); 22 | assert.ok(!isGitDependency("<1.0.0 || >=2.3.1 <2.4.5 || >=2.5.2 <3.0.0")); 23 | assert.ok(!isGitDependency("http://asdf.com/asdf.tar.gz")); 24 | assert.ok(!isGitDependency("~1.2")); 25 | assert.ok(!isGitDependency("~1.2.3")); 26 | assert.ok(!isGitDependency("2.x")); 27 | assert.ok(!isGitDependency("3.3.x")); 28 | assert.ok(!isGitDependency("latest")); 29 | assert.ok(!isGitDependency("file:../dyl")); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /workspaces/tree-walker/test/utils/mergeDependencies.spec.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { dirname, join } from "node:path"; 3 | import { readFileSync } from "node:fs"; 4 | import { test } from "node:test"; 5 | import { fileURLToPath } from "node:url"; 6 | import assert from "node:assert"; 7 | 8 | // Import Third-party Dependencies 9 | import is from "@slimio/is"; 10 | 11 | // Import Internal Dependencies 12 | import { mergeDependencies } from "../../src/utils/index.js"; 13 | 14 | // CONSTANTS 15 | const __dirname = dirname(fileURLToPath(import.meta.url)); 16 | const kFixturePath = join(__dirname, "..", "fixtures/mergeDependencies"); 17 | 18 | // JSON PAYLOADS 19 | const one = JSON.parse(readFileSync(join(kFixturePath, "one.json"), "utf-8")); 20 | const two = JSON.parse(readFileSync(join(kFixturePath, "two.json"), "utf-8")); 21 | const three = JSON.parse(readFileSync(join(kFixturePath, "three.json"), "utf-8")); 22 | 23 | test("should return the one.json field 'dependencies' merged", () => { 24 | const result = mergeDependencies(one); 25 | assert.ok(is.plainObject(result), "result value of mergeDependencies must be a plainObject."); 26 | 27 | const expected = new Map([ 28 | ["semver", "^0.1.0"], 29 | ["test", "~0.5.0"] 30 | ]); 31 | assert.deepEqual(result.dependencies, expected); 32 | assert.deepEqual(result.customResolvers, new Map()); 33 | }); 34 | 35 | test("should return the one.json field 'dependencies' & 'devDependencies' merged", () => { 36 | const result = mergeDependencies(one, ["dependencies", "devDependencies"]); 37 | assert.ok(is.plainObject(result), "result value of mergeDependencies must be a plainObject."); 38 | 39 | const expected = new Map([ 40 | ["semver", "^0.1.0"], 41 | ["test", "~0.5.0"], 42 | ["ava", "^1.0.0"] 43 | ]); 44 | assert.deepEqual(result.dependencies, expected); 45 | assert.deepEqual(result.customResolvers, new Map()); 46 | }); 47 | 48 | test("should return two.json 'dependencies' & 'devDependencies' merged (with a custom Resolvers)", () => { 49 | const result = mergeDependencies(two, ["dependencies", "devDependencies"]); 50 | assert.ok(is.plainObject(result), "result value of mergeDependencies must be a plainObject."); 51 | 52 | const expected = new Map([ 53 | ["@slimio/is", "^1.4.0"], 54 | ["japa", "~0.1.0"] 55 | ]); 56 | assert.deepEqual(result.dependencies, expected); 57 | 58 | const resolvers = new Map([ 59 | ["custom", "file:\\file.js"] 60 | ]); 61 | assert.deepEqual(result.customResolvers, resolvers); 62 | }); 63 | 64 | test("should return no dependencies/customResolvers for three.json", () => { 65 | const result = mergeDependencies(three, ["dependencies", "devDependencies"]); 66 | assert.ok(is.plainObject(result), "result value of mergeDependencies must be a plainObject."); 67 | 68 | assert.strictEqual(result.dependencies.size, 0); 69 | assert.strictEqual(result.customResolvers.size, 0); 70 | }); 71 | 72 | test("should detect NPM alias using custom resolvers npm: (but still count it as normal dependency)", () => { 73 | const result = mergeDependencies({ 74 | dependencies: { 75 | test: "npm:fastify@^4.7.0" 76 | } 77 | }, ["dependencies", "devDependencies"]); 78 | assert.ok(is.plainObject(result), "result value of mergeDependencies must be a plainObject."); 79 | 80 | assert.strictEqual(result.dependencies.size, 1); 81 | assert.strictEqual(result.customResolvers.size, 1); 82 | assert.ok(result.alias.has("test")); 83 | assert.strictEqual(result.alias.get("test"), "fastify@^4.7.0"); 84 | }); 85 | -------------------------------------------------------------------------------- /workspaces/tree-walker/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | }, 7 | "include": ["src"], 8 | "references": [ 9 | { 10 | "path": "../npm-types" 11 | } 12 | ] 13 | } 14 | --------------------------------------------------------------------------------