├── .eslintignore ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── 1-bug-report.md │ ├── 2-feature-request.md │ └── 3-help.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── main.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── Contributor-Agreement.md ├── LICENSE ├── README.md ├── SECURITY.md ├── __tests__ ├── __fixtures__ │ ├── commit-with-broken-package-json.zip │ ├── simple-project-existing-package-name.zip │ ├── simple-project.zip │ └── small-project.zip ├── __snapshots__ │ └── app.test.js.snap └── app.test.js ├── babel.config.json ├── bin └── snync.js ├── jsdoc.json ├── package-lock.json ├── package.json └── src ├── Parser.js ├── RegistryClient.js ├── RepoManager.js └── main.js /.eslintignore: -------------------------------------------------------------------------------- 1 | __tests__/__fixtures__/tmp -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @lirantal 2 | * @snyk/devrel 3 | * @Kirill89 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | - **Library Version**: 8 | - **OS**: 9 | - **Node.js Version**: 10 | 11 | 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug report" 3 | about: Create a bug report 4 | --- 5 | 6 | 7 | 8 | ## Expected Behavior 9 | 10 | 11 | 12 | 13 | ## Current Behavior 14 | 15 | 16 | 17 | 18 | ## Possible Solution 19 | 20 | 21 | 22 | 23 | ## Steps to Reproduce (for bugs) 24 | 25 | 26 | 27 | 28 | 1. 29 | 2. 30 | 3. 31 | 4. 32 | 33 | ## Context 34 | 35 | 36 | 37 | 38 | ## Your Environment 39 | 40 | 41 | 42 | - Library Version used: 43 | - Node.js version (e.g. Node.js 5.4): 44 | - Operating System and version (desktop or mobile): 45 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F680 Feature request" 3 | about: Suggest an idea for this project 4 | --- 5 | 6 | 11 | 12 | **Is your feature request related to a problem? Please describe.** 13 | Please describe the problem you are trying to solve. 14 | 15 | **Describe the solution you'd like** 16 | Please describe the desired behavior. 17 | 18 | **Describe alternatives you've considered** 19 | Please describe alternative solutions or features you have considered. 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3-help.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '⁉️ Need help?' 3 | about: Please describe the problem. 4 | --- 5 | 6 | 9 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | 7 | ## Types of changes 8 | 9 | 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 14 | 15 | ## Related Issue 16 | 17 | 18 | 19 | 20 | 21 | 22 | ## Motivation and Context 23 | 24 | 25 | 26 | ## How Has This Been Tested? 27 | 28 | 29 | 30 | 31 | 32 | 33 | ## Screenshots (if appropriate): 34 | 35 | ## Checklist: 36 | 37 | 38 | 39 | 40 | - [ ] I have updated the documentation (if required). 41 | - [ ] I have read the **CONTRIBUTING** document. 42 | - [ ] I have added tests to cover my changes. 43 | - [ ] All new and existing tests passed. 44 | - [ ] I added a picture of a cute animal cause it's fun 45 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | name: Lint 8 | runs-on: 'ubuntu-latest' 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-node@v2 12 | with: 13 | node-version: '14' 14 | - name: install dependencies 15 | run: yarn install --frozen-lockfile 16 | - name: lint code 17 | run: npm run lint 18 | 19 | build: 20 | strategy: 21 | matrix: 22 | platform: [ubuntu-latest] 23 | node: ['14', '16'] 24 | name: Tests - Node ${{ matrix.node }} (${{ matrix.platform }}) 25 | runs-on: ${{ matrix.platform }} 26 | steps: 27 | - uses: actions/checkout@v2 28 | - uses: actions/setup-node@v2 29 | with: 30 | node-version: ${{ matrix.node }} 31 | - name: install dependencies 32 | run: yarn install --frozen-lockfile 33 | - name: run tests 34 | run: npm run test 35 | 36 | release: 37 | name: do semantic release 38 | runs-on: 'ubuntu-latest' 39 | needs: build 40 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' 41 | steps: 42 | - uses: actions/checkout@v2 43 | - uses: actions/setup-node@v2 44 | with: 45 | node-version: '14' 46 | - name: install dependencies 47 | run: npm ci --only=production --ignore-engines 48 | - name: release 49 | run: npx semantic-release 50 | env: 51 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 52 | NPM_TOKEN: ${{secrets.NPM_TOKEN}} 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore the extracted fixtures directory 2 | __tests__/__fixtures__/tmp 3 | 4 | # ignore dccache 5 | .dccache 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # TypeScript v1 declaration files 46 | typings/ 47 | 48 | # Optional npm cache directory 49 | .npm 50 | 51 | # Optional eslint cache 52 | .eslintcache 53 | 54 | # Optional REPL history 55 | .node_repl_history 56 | 57 | # Output of 'npm pack' 58 | *.tgz 59 | 60 | # Yarn Integrity file 61 | .yarn-integrity 62 | 63 | # dotenv environment variables file 64 | .env 65 | .env.test 66 | 67 | # parcel-bundler cache (https://parceljs.org/) 68 | .cache 69 | 70 | # next.js build output 71 | .next 72 | 73 | # nuxt.js build output 74 | .nuxt 75 | 76 | # vuepress build output 77 | .vuepress/dist 78 | 79 | # Serverless directories 80 | .serverless/ 81 | 82 | # FuseBox cache 83 | .fusebox/ 84 | 85 | # DynamoDB Local files 86 | .dynamodb/ 87 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | __tests__/__fixtures__ -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "semi": false, 6 | "trailingComma": "none", 7 | "useTabs": false, 8 | "bracketSpacing": true 9 | } 10 | -------------------------------------------------------------------------------- /Contributor-Agreement.md: -------------------------------------------------------------------------------- 1 | This Contributor Licence Agreement (“Agreement”) sets out the terms under which contributions are made to open source projects of Snyk Ltd (“Snyk”) by or on behalf of the Contributor. This Agreement is legally binding on the Contributor. 2 | 3 | 4 | Who the “Contributor” is depends on whether the person submitting the contribution is a private individual acting on their own behalf, or is acting on behalf of someone else (for example, their employer). The “Contributor” in this Agreement is therefore either: (i) if the individual who Submits a Contribution does so on behalf of their employer or another Legal Entity, any Legal Entity on behalf of whom a Contribution has been received by Snyk; or in all other cases (ii) the individual who Submits a Contribution to Snyk. "Legal Entity" means an entity which is not a natural person (for example, a limited company or corporation). 5 | 6 | 7 | ** 1. Interpretation** 8 | 9 | 10 | The following definitions and rules of interpretation apply in this Agreement. 11 | 12 | 13 | 1.1 Definitions: 14 | 15 | **Affiliates**: means, in respect of a Legal Entity, any other Legal Entities that control, are controlled by, or under common control with that Legal Entity. 16 | 17 | 18 | **Contribution**: means any software or code that is Submitted by the Contributor to Snyk for inclusion in a Project. 19 | 20 | 21 | **Copyright**: all copyright and rights in the nature of copyright subsisting in the Contribution in any part of the world, to which the Contributor is, or may become, entitled. 22 | 23 | 24 | **Effective Date**: the earlier of the date on which the Contributor Submits the Contribution, or the date of the Contributor’s acceptance of this Agreement. 25 | 26 | 27 | **Patent Rights**: any patent claims which the Contributor or its Affiliates owns, controls or has the right to grant, now or in the future, to the extent infringed by the exercise of the rights licensed under this Agreement. 28 | 29 | 30 | **Project**: a software project to which the Contribution is Submitted. 31 | 32 | 33 | **Submit**: means to submit or send to Snyk or its representatives by any form of electronic, verbal, or written communication, for example, by means of code repositories or control systems, and issue tracking systems, that are managed by or on behalf of Snyk. 34 | 35 | 36 | **2. Licence Grant** 37 | 38 | 39 | 2.1 Copyright: The Contributor grants to Snyk a perpetual, irrevocable, worldwide, transferable, fully sublicenseable through multiple tiers, fee-free, non-exclusive licence under the Copyright to do the following acts, subject to, and in accordance with, the terms of this Agreement: to reproduce, prepare derivative works of, publicly display, publicly perform, communicate to the public, and distribute by any means Contributions and such derivative works. 40 | 41 | 42 | 2.2. Patent Rights: The Contributor grants to Snyk a perpetual, irrevocable, worldwide, transferable, fully sublicenseable through multiple tiers, fee-free, non-exclusive licence under the Patent Rights to do the following acts, subject to, and in accordance with, the terms of this Agreement: to make, have made, use, sell, offer for sale, import and otherwise transfer the Contribution and the Contribution in combination with a Project (and portions of such combination). 43 | 44 | 45 | 2.3 The Contributor acknowledges that Snyk is not obliged to include the Contribution in any Project. 46 | 47 | 48 | 2.4 If Snyk includes the Contribution in a Project, Snyk may license the Contribution under any licence terms, including copyleft, permissive, commercial, or proprietary licenses, provided that it shall also license the Contribution under the terms of any licenses which are approved by the Open Source Initiative on or after the Effective Date, including both permissive and copyleft licenses, whether or not such licenses are subsequently disapproved (including any right to adopt any future version of a license if permitted). 49 | 50 | 51 | 2.5 In the event that any moral rights apply in respect of the Contribution, the Contributor, being the sole author of the Contribution, waives all moral rights in respect of the use to be made of the Contribution under this Agreement to which the Contributor may now or at any future time be entitled. 52 | 53 | 54 | **3. Warranties and Disclaimers** 55 | 56 | 57 | 3.1 The Contributor warrants and represents that: 58 | 59 | 60 | (a) it is the sole owner of the Copyright and any Patent Rights and legally entitled to grant the licence in section 2; 61 | 62 | (b) the Contribution is its own original creation; 63 | 64 | 65 | (c) the licence in section 2 does not conflict with or infringe any rights granted by the Contributor or (if applicable) its Affiliates; and 66 | 67 | 68 | (d) it is not aware of any claims, suits, or actions in respect of the Contribution. 69 | 70 | 71 | 3.2 All other conditions, warranties or other terms which might have effect between the parties in respect of the Contribution or be implied or incorporated into this Agreement are excluded. 72 | 73 | 3.3 The Contributor is not required to provide any support for the Contribution. 74 | 75 | 76 | **4. Other important terms** 77 | 78 | 79 | 4.1 Assignment/Transfer: Snyk may assign and transfer all of its rights and obligations under this Agreement to any person. 80 | 81 | 82 | 4.2 Further Assurance: The Contributor shall at Snyk’s expense execute and deliver such documents and perform such acts as may reasonably be required by Snyk for the purpose of giving full effect to this Agreement. 83 | 84 | 85 | 4.3 Agreement: This Agreement constitutes the entire Agreement between the parties and supersedes and extinguishes all previous Agreements, promises, assurances, warranties, representations and understandings between them, whether written or oral, relating to its subject matter. 86 | 87 | 88 | 4.4 Governing law: This Agreement and any dispute or claim (including non-contractual disputes or claims) arising out of or in connection with it or its subject matter or formation shall be governed by and construed in accordance with the law of England and Wales. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Snyk Ltd. 2021 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | snync 4 |

5 | 6 |

7 | Mitigate security concerns of Dependency Confusion supply chain security risks 8 |

9 | 10 |

11 | npm version 12 | license 13 | downloads 14 | build 15 | Known Vulnerabilities 16 | Responsible Disclosure Policy 17 |

18 | 19 | # About 20 | 21 | Prevent and detect if you're vulnerable to Dependency Confusion supply chain security attacks 22 | 23 | # Intro 24 | 25 | When you manage private open source packages, for reasons such as keeping intellectual property private, then these packages will be hosted and served via private registries, or require authorization. By definition, these packages won't exist on public registries. However, when a package name is used without a reserved namespace (also known as a scope in npm, for example), they are often free to be registered by any other user on the Internet and create a potential Dependency Confusion attack vector. The attack manifests due to a mix of user misconfiguration, and bad design of package managers, which will cause the download of the package from the public registry, instead of the private registry. 26 | 27 | # How does it work? 28 | 29 | This tool detects two types of potential Dependency Confusion compromises: 30 | 1. Vulnerable 31 | 2. Suspicious 32 | 33 | ## Vulnerable 34 | 35 | A case of actual **vulnerable** package is when a package name is detected to be used in a project, but the same package name is not registered on the public registry. 36 | 37 | You can easily simulate a real world example of this case in an npm project: 38 | 1. Edit the package.json file 39 | 2. Add to the `dependencies` key a new entry: `"snyk-private-internal-logic": "1.0.0"` and save the file (this assumes the package name `snyk-private-internal-logic` is not registered on npmjs.org). 40 | 3. Commit the file to the repository 41 | 4. Run `snync` to detect it. 42 | 43 | When a package is detected as **vulnerable**, it is our recommendation to immediately reserve the name on the public registry. 44 | 45 | ## Suspicious 46 | 47 | What happens if the private package name that you use is already registered on the public registry as a functional and unrelated package by someone else? In this case, you don't own the public package, but someone else does. Theoretically, this might not look as a problem because in a dependency confusion case the worst thing that can happen is the wrong package to be installed. However, that diminishes the potential threat model where a package can be hijacked and replaced by malicious versions of it, especially in cases of low-downloaded and unmaintained packages. 48 | 49 | We've seen cases of package hijacking and maintainer accounts compromises in past supply chain security incidents such as `event-stream`, `mailparser`, and `eslint-config` as some examples of highly downloaded packages, and very active maintainers, yet still resulting in package compromises. 50 | 51 | When a pakcage is detected as **suspicious**, it is our recommendation to immediately move to a new package naming and reserve that new name on the public registry. 52 | 53 | ## Logic flow 54 | 55 | How does snync work from decision tree perspective? 56 | 57 | ``` 58 | 1. Get "all dependencies" in `package.json` (note, refers to direct dependencies only, not transitive) 59 | ['dependencies'] and ['devDependencies] 60 | 2. If a package includes a scope (such as prefixed with a `@snyk/`) 61 | then remove it from the "all dependencies" list and save it for later, to warn the user to ensure they own that scope 62 | 3. Foreach of the "all dependencies" gathered, get the time it was introduced to the source-code (i.e. the time it was added to `package.json`) 63 | 4. Foreach of the "all dependencies" gathered, get the time it was created in the npmjs registry 64 | 5. Compare the two timestamps 65 | 5.1. if a package is not found in the registry then signal an error to let them know that this public namespace is not taken, and is vulnerable for someone to employ a Dependency Confusion on them. 66 | 5.2. if a package is found in the registry, and it was created after the time it was introduced to source-code, then signal a warning that there is potentially an attack in progress and to warn the user to review the premise and legitimacy of that package that exits in the public registry. 67 | ``` 68 | 69 | ## Supported ecosystems 70 | 71 | | Ecosystem | Supported 72 | | ------------- | ------------- 73 | | npm | ✅ 74 | | pypi | 75 | 76 | # Install 77 | 78 | ## Prerequisite 79 | 80 | To use this tool, it is expected that you have the following available in your environment: 81 | 1. Node.js and npm in stable and recent versions 82 | 2. The Git binary available in your path 83 | 84 | ```sh 85 | npm install -g snync 86 | ``` 87 | 88 | # Usage 89 | 90 | To scan a project's dependencies and test if you're vulnerable to Dependency Confusion security issues, where the project's git repository is cloned at `/home/user/my-app`: 91 | 92 | ```sh 93 | npx snync --directory /home/user/my-app 94 | ``` 95 | 96 | # Implementation details 97 | 98 | To get a list of dependencies we parse a project's manifest (`package.json`) from the root of the directory you specify as a first argument. 99 | 100 | Then we fetch from the public NPM registry to check when each dependency was created. At this point we can check if dependency is **vulnerable** – if it is not in the public NPM registry. 101 | 102 | To check if dependency is **suspicious** we compare date it was first introduced to a project's manifest and date it was published. To understand when you added a dependency to a manifest we scan git commits history. 103 | 104 | # FAQ 105 | 106 | Q. Why is it called _snync_? 107 | A. _snync_ is abbreviation for _So Now You're Not Confused_, which is a play on Snyk's _So Now You Know_. 108 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Responsible disclosure security policy 4 | 5 | A responsible disclosure policy helps protect users of the project from publicly disclosed security vulnerabilities without a fix by employing a process where vulnerabilities are first triaged in a private manner, and only publicly disclosed after a reasonable time period that allows patching the vulnerability and provides an upgrade path for users. 6 | 7 | When contacting us directly via email, we will do our best efforts to respond in a reasonable time to resolve the issue. When contacting a security program their disclosure policy will provide details on time-frame, processes and paid bounties. 8 | 9 | We kindly ask you to refrain from malicious acts that put our users, the project, or any of the project’s team members at risk. 10 | 11 | ## Reporting a security issue 12 | 13 | We consider the security of our systems a top priority. But no matter how much effort we put into system security, there can still be vulnerabilities present. 14 | 15 | If you discover a security vulnerability, please use one of the following means of communications to report it to us: 16 | 17 | - Report the security issue to [Snyk Security Team](https://snyk.io/vulnerability-disclosure). They will help triage the security issue and work with all involved parties to remediate and release a fix. 18 | 19 | Note that time-frame and processes are subject to each program’s own policy. 20 | 21 | - Report the security issue to the project maintainers directly. 22 | 23 | Your efforts to responsibly disclose your findings are sincerely appreciated and will be taken into account to acknowledge your contributions. 24 | -------------------------------------------------------------------------------- /__tests__/__fixtures__/commit-with-broken-package-json.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snyk-labs/snync/8361c23556d126d7bd369ac107173176ed379cec/__tests__/__fixtures__/commit-with-broken-package-json.zip -------------------------------------------------------------------------------- /__tests__/__fixtures__/simple-project-existing-package-name.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snyk-labs/snync/8361c23556d126d7bd369ac107173176ed379cec/__tests__/__fixtures__/simple-project-existing-package-name.zip -------------------------------------------------------------------------------- /__tests__/__fixtures__/simple-project.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snyk-labs/snync/8361c23556d126d7bd369ac107173176ed379cec/__tests__/__fixtures__/simple-project.zip -------------------------------------------------------------------------------- /__tests__/__fixtures__/small-project.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snyk-labs/snync/8361c23556d126d7bd369ac107173176ed379cec/__tests__/__fixtures__/small-project.zip -------------------------------------------------------------------------------- /__tests__/__snapshots__/app.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Commit with broken manifest should be ignored 1`] = ` 4 | " 5 | Reviewing your dependencies... 6 | 7 | Checking dependency: lodash 8 | -> introduced via commit sha: cba680064122389350203e90b2cbc8705de23b63 9 | Checking dependency: async 10 | -> introduced via commit sha: 456e6f36b5494ff6c2437347ac3ec220248e09c8 11 | " 12 | `; 13 | 14 | exports[`Debug information prints commit SHA 1`] = ` 15 | " 16 | Reviewing your dependencies... 17 | 18 | Checking dependency: asunc 19 | -> ❌ suspicious 20 | -> introduced via commit sha: b335c1d5b4089833d63c95e846fb1916e6425447 21 | Checking dependency: a-package-in-private-registry-0 22 | -> ⚠️ vulnerable 23 | -> introduced via commit sha: 07b2f1c6a5bde9d73e056d67d51ea9bbc7be6ee7 24 | Checking dependency: lodash 25 | -> introduced via commit sha: 74f0d1666052fd03fc1220351ee087545dba2ec0 26 | Checking dependency: moment 27 | " 28 | `; 29 | 30 | exports[`Sanity test - simple project 1`] = ` 31 | " 32 | Reviewing your dependencies... 33 | 34 | Checking dependency: asunc 35 | -> ❌ suspicious 36 | Checking dependency: a-package-in-private-registry-0 37 | -> ⚠️ vulnerable 38 | Checking dependency: lodash 39 | Checking dependency: moment 40 | " 41 | `; 42 | 43 | exports[`Sanity test - small project 1`] = ` 44 | " 45 | Reviewing your dependencies... 46 | 47 | Checking dependency: isomorphic-textencoder 48 | -> introduced via commit sha: 55c258ce353630cd7ccc0ce999d941311f8c4655 49 | Checking dependency: just-debounce-it 50 | -> introduced via commit sha: 55c258ce353630cd7ccc0ce999d941311f8c4655 51 | Checking dependency: just-once 52 | -> introduced via commit sha: 55c258ce353630cd7ccc0ce999d941311f8c4655 53 | Checking dependency: very-private-package-of-mine 54 | -> ⚠️ vulnerable 55 | -> introduced via commit sha: ab259e06ddc4bdacaf459c8df98c65ca34c93ecc 56 | Checking dependency: karma 57 | -> introduced via commit sha: 55c258ce353630cd7ccc0ce999d941311f8c4655 58 | Checking dependency: karma-browserstack-launcher 59 | -> introduced via commit sha: ac659289d1e3f9eb1e81db4f5ea821fe688b6bbd 60 | Checking dependency: karma-chrome-launcher 61 | -> introduced via commit sha: 55c258ce353630cd7ccc0ce999d941311f8c4655 62 | Checking dependency: karma-edge-launcher 63 | -> introduced via commit sha: 55c258ce353630cd7ccc0ce999d941311f8c4655 64 | Checking dependency: karma-fail-fast-reporter 65 | -> introduced via commit sha: ac659289d1e3f9eb1e81db4f5ea821fe688b6bbd 66 | Checking dependency: karma-firefox-launcher 67 | -> introduced via commit sha: 55c258ce353630cd7ccc0ce999d941311f8c4655 68 | Checking dependency: karma-ie-launcher 69 | -> introduced via commit sha: ac659289d1e3f9eb1e81db4f5ea821fe688b6bbd 70 | Checking dependency: karma-jasmine 71 | -> introduced via commit sha: 55c258ce353630cd7ccc0ce999d941311f8c4655 72 | Checking dependency: karma-junit-reporter 73 | -> introduced via commit sha: 55c258ce353630cd7ccc0ce999d941311f8c4655 74 | Checking dependency: karma-safari-launcher 75 | -> introduced via commit sha: 55c258ce353630cd7ccc0ce999d941311f8c4655 76 | Checking dependency: karma-sauce-launcher 77 | -> introduced via commit sha: 55c258ce353630cd7ccc0ce999d941311f8c4655 78 | Checking dependency: karma-verbose-reporter 79 | -> introduced via commit sha: 55c258ce353630cd7ccc0ce999d941311f8c4655 80 | Checking dependency: karma-webpack 81 | -> introduced via commit sha: 55c258ce353630cd7ccc0ce999d941311f8c4655 82 | Checking dependency: prettier 83 | -> introduced via commit sha: 55c258ce353630cd7ccc0ce999d941311f8c4655 84 | Checking dependency: puppeteer 85 | -> introduced via commit sha: 55c258ce353630cd7ccc0ce999d941311f8c4655 86 | Checking dependency: semantic-release 87 | -> introduced via commit sha: f083b9680cc3f588c8d24a4c212064372d04de2e 88 | Checking dependency: webpack 89 | -> introduced via commit sha: 55c258ce353630cd7ccc0ce999d941311f8c4655 90 | Checking dependency: webpack-cli 91 | -> introduced via commit sha: 61c5d8034927693cc72064c44b8a7f3f63b3ea50 92 | " 93 | `; 94 | 95 | exports[`Test case of private package that exists already on npm 1`] = ` 96 | " 97 | Reviewing your dependencies... 98 | 99 | Checking dependency: eslint-plugin-vue 100 | -> ❌ suspicious 101 | -> introduced via commit sha: 9e9dab770d4e412babfce0f2dc66d8b04a6c0d28 102 | " 103 | `; 104 | -------------------------------------------------------------------------------- /__tests__/app.test.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import { fileURLToPath } from 'url' 4 | import { testProject } from '../src/main.js' 5 | import decompress from 'decompress' 6 | import { jest } from '@jest/globals' 7 | 8 | jest.setTimeout(30000) 9 | const __dirname = path.dirname(fileURLToPath(import.meta.url)) 10 | 11 | const projectFixtures = [ 12 | 'simple-project.zip', 13 | 'small-project.zip', 14 | 'commit-with-broken-package-json.zip', 15 | 'simple-project-existing-package-name.zip' 16 | ] 17 | 18 | const destinationFixtures = path.resolve(path.join(__dirname, '__fixtures__', 'tmp')) 19 | 20 | beforeAll(async () => { 21 | if (!fs.existsSync(destinationFixtures)) { 22 | fs.mkdirSync(destinationFixtures) 23 | } 24 | 25 | for (const fixtureName of projectFixtures) { 26 | const fixtureProjectPath = path.resolve(path.join(__dirname, '__fixtures__', fixtureName)) 27 | const fixtureDirectoryName = path.basename(fixtureName, '.zip') 28 | if (fs.existsSync(path.resolve(path.join(destinationFixtures, fixtureDirectoryName)))) { 29 | continue 30 | } else { 31 | await decompress(fixtureProjectPath, destinationFixtures) 32 | } 33 | } 34 | }) 35 | 36 | test('Sanity test - simple project', async () => { 37 | const projectPath = path.resolve(path.join(destinationFixtures, 'simple-project')) 38 | 39 | let out = '' 40 | await testProject({ projectPath, log: (...args) => (out += `${args.join(' ')}\n`) }) 41 | expect(out).toMatchSnapshot() 42 | }) 43 | 44 | test('Sanity test - small project', async () => { 45 | const projectPath = path.resolve(path.join(destinationFixtures, 'small-project')) 46 | 47 | let out = '' 48 | await testProject({ 49 | projectPath, 50 | log: (...args) => (out += `${args.join(' ')}\n`), 51 | debugMode: true 52 | }) 53 | expect(out).toMatchSnapshot() 54 | }) 55 | 56 | test('Debug information prints commit SHA', async () => { 57 | const projectPath = path.resolve(path.join(destinationFixtures, 'simple-project')) 58 | 59 | let out = '' 60 | await testProject({ 61 | projectPath, 62 | log: (...args) => (out += `${args.join(' ')}\n`), 63 | debugMode: true 64 | }) 65 | expect(out).toMatchSnapshot() 66 | }) 67 | 68 | // Check the case when commit contains invalid package.json file, 69 | // for example with extra comma. 70 | // commit 456e6f36b5494ff6c2437347ac3ec220248e09c8 fix previous commit 71 | // commit 6b2a52e3177c8b1fad572b10d3090d1e9822945f add async 72 | // --> This commit introduces extra comma after the last dependency 73 | // commit cba680064122389350203e90b2cbc8705de23b63 add lodash 74 | test('Commit with broken manifest should be ignored', async () => { 75 | const projectPath = path.resolve( 76 | path.join(destinationFixtures, 'commit-with-broken-package-json') 77 | ) 78 | 79 | let out = '' 80 | await testProject({ 81 | projectPath, 82 | log: (...args) => (out += `${args.join(' ')}\n`), 83 | debugMode: true 84 | }) 85 | expect(out).toMatchSnapshot() 86 | }) 87 | 88 | test('Test case of private package that exists already on npm', async () => { 89 | const projectPath = path.resolve( 90 | path.join(destinationFixtures, 'simple-project-existing-package-name') 91 | ) 92 | 93 | let out = '' 94 | await testProject({ 95 | projectPath, 96 | log: (...args) => (out += `${args.join(' ')}\n`), 97 | debugMode: true, 98 | privatePackagesList: ['eslint-plugin-vue'] 99 | }) 100 | expect(out).toMatchSnapshot() 101 | }) 102 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ] 11 | ] 12 | } -------------------------------------------------------------------------------- /bin/snync.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | 4 | import path from 'path' 5 | import { testProject } from '../src/main.js' 6 | import meow from 'meow' 7 | 8 | const cli = meow( 9 | ` 10 | Usage 11 | $ snync --directory ~/projects/my-app [options] 12 | 13 | Options 14 | --directory -d Path to a project's source-code directory with a package.json 15 | --private -p Specify name of private packages (repeat as needed) 16 | --debug -x Enable debugging when printing data 17 | 18 | Examples 19 | $ snync --directory ~/projects/my-app -p "my-private-package-name" -p "my-other-private-package" 20 | `, 21 | { 22 | importMeta: import.meta, 23 | flags: { 24 | directory: { 25 | type: 'string', 26 | alias: 'd' 27 | }, 28 | private: { 29 | type: 'string', 30 | alias: 'p' 31 | }, 32 | debug: { 33 | type: 'boolean', 34 | alias: 'x' 35 | } 36 | } 37 | } 38 | ) 39 | 40 | main() 41 | 42 | async function main() { 43 | let projectPath 44 | 45 | if (cli.flags.directory) { 46 | projectPath = path.resolve(cli.flags.directory) 47 | console.log(`Testing project at: ${projectPath}`) 48 | } else { 49 | console.error(`error: please provide a directory path to the git project to test.`) 50 | console.error(cli.help) 51 | process.exit(-1) 52 | } 53 | 54 | await testProject({ 55 | projectPath, 56 | log: console.log, 57 | debug: cli.flags.debug, 58 | privatePackagesList: cli.flags.private 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "allowUnknownTags": true, 4 | "dictionaries": ["jsdoc", "closure"] 5 | }, 6 | "source": { 7 | "include": ["./index.js"], 8 | "exclude": ["./node_modules", "./.nyc_output", "./coverage", "./out", "./tests"], 9 | "includePattern": ".+\\.js(doc|x)?$", 10 | "excludePattern": "(^|\\/|\\\\)_" 11 | }, 12 | "plugins": [], 13 | "templates": {}, 14 | "opts": { 15 | "destination": "./out", 16 | "recurse": true, 17 | "private": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "snync", 3 | "version": "0.0.0-development", 4 | "description": "Mitigate security concerns of Dependency Confusion supply chain security risks", 5 | "type": "module", 6 | "engines": { 7 | "node": ">=12" 8 | }, 9 | "bin": { 10 | "snync": "./bin/snync.js" 11 | }, 12 | "scripts": { 13 | "lint": "eslint . && npm run lint:lockfile", 14 | "lint:fix": "eslint . --fix", 15 | "format": "prettier --config .prettierrc.json --write '**/*.js'", 16 | "test": "NODE_OPTIONS=--experimental-vm-modules jest", 17 | "test:watch": "jest --watch", 18 | "coverage:view": "open-cli coverage/lcov-report/index.html", 19 | "lint:lockfile": "lockfile-lint --path package-lock.json --validate-https --allowed-hosts npm yarn" 20 | }, 21 | "author": { 22 | "name": "Liran Tal", 23 | "email": "liran.tal@gmail.com", 24 | "url": "https://github.com/lirantal" 25 | }, 26 | "license": "Apache-2.0", 27 | "keywords": [ 28 | "dependency confusion", 29 | "supply chain", 30 | "supply chain security", 31 | "dazed and confused", 32 | "security" 33 | ], 34 | "homepage": "https://github.com/snyk-labs/snync", 35 | "bugs": { 36 | "url": "https://github.com/snyk-labs/snync/issues" 37 | }, 38 | "repository": { 39 | "type": "git", 40 | "url": "https://github.com/snyk-labs/snync.git" 41 | }, 42 | "dependencies": { 43 | "debug": "^4.3.1", 44 | "meow": "^10.0.1", 45 | "undici": "^5.10.0", 46 | "validate-npm-package-name": "^3.0.0" 47 | }, 48 | "devDependencies": { 49 | "@babel/core": "^7.14.6", 50 | "@babel/preset-env": "^7.14.7", 51 | "@commitlint/cli": "^9.1.2", 52 | "@commitlint/config-conventional": "^7.1.2", 53 | "@semantic-release/changelog": "^3.0.4", 54 | "@semantic-release/commit-analyzer": "^6.2.0", 55 | "@semantic-release/git": "^7.0.16", 56 | "@semantic-release/github": "^5.4.2", 57 | "@semantic-release/npm": "^7.1.0", 58 | "@semantic-release/release-notes-generator": "^7.2.1", 59 | "babel-eslint": "^10.0.1", 60 | "babel-jest": "^27.0.6", 61 | "babel-plugin-syntax-async-functions": "^6.13.0", 62 | "babel-plugin-transform-regenerator": "^6.26.0", 63 | "decompress": "^4.2.1", 64 | "eslint": "^6.0.1", 65 | "eslint-config-standard": "^13.0.1", 66 | "eslint-plugin-import": "^2.18.0", 67 | "eslint-plugin-jest": "^22.7.2", 68 | "eslint-plugin-node": "^9.1.0", 69 | "eslint-plugin-promise": "^4.2.1", 70 | "eslint-plugin-security": "^1.4.0", 71 | "eslint-plugin-standard": "^4.0.0", 72 | "husky": "^3.0.0", 73 | "jest": "^29.0.2", 74 | "lint-staged": "^9.2.0", 75 | "lockfile-lint": "^2.0.1", 76 | "node-notifier": "^10.0.0", 77 | "open-cli": "^6.0.0", 78 | "prettier": "^1.18.2" 79 | }, 80 | "jest": { 81 | "testEnvironment": "node", 82 | "verbose": true, 83 | "notify": true, 84 | "collectCoverage": true, 85 | "coverageThreshold": { 86 | "global": { 87 | "branches": 80, 88 | "functions": 80, 89 | "lines": 80, 90 | "statements": 80 91 | } 92 | }, 93 | "testPathIgnorePatterns": [ 94 | "/__tests__/.*/__fixtures__/.*" 95 | ], 96 | "collectCoverageFrom": [ 97 | "index.js", 98 | "src/**/*.{js,ts}" 99 | ], 100 | "testMatch": [ 101 | "**/*.test.js" 102 | ] 103 | }, 104 | "husky": { 105 | "hooks": { 106 | "commit-msg": "commitlint --env HUSKY_GIT_PARAMS", 107 | "pre-commit": "lint-staged", 108 | "post-merge": "npm install", 109 | "pre-push": "npm run lint && npm run test" 110 | } 111 | }, 112 | "lint-staged": { 113 | "**/*.js": [ 114 | "npm run format", 115 | "git add" 116 | ] 117 | }, 118 | "commitlint": { 119 | "extends": [ 120 | "@commitlint/config-conventional" 121 | ] 122 | }, 123 | "standard": { 124 | "env": [ 125 | "jest" 126 | ], 127 | "parser": "babel-eslint", 128 | "ignore": [ 129 | "**/out/" 130 | ] 131 | }, 132 | "eslintIgnore": [ 133 | "coverage/**" 134 | ], 135 | "eslintConfig": { 136 | "env": { 137 | "node": true, 138 | "es6": true, 139 | "jest": true 140 | }, 141 | "plugins": [ 142 | "import", 143 | "standard", 144 | "node", 145 | "security", 146 | "jest" 147 | ], 148 | "extends": [ 149 | "standard", 150 | "plugin:node/recommended" 151 | ], 152 | "rules": { 153 | "node/no-extraneous-import": "warn", 154 | "no-process-exit": "warn", 155 | "jest/no-disabled-tests": "error", 156 | "jest/no-focused-tests": "error", 157 | "jest/no-identical-title": "error", 158 | "node/no-unsupported-features": "off", 159 | "node/no-unpublished-require": "off", 160 | "security/detect-non-literal-fs-filename": "off", 161 | "security/detect-unsafe-regex": "error", 162 | "security/detect-buffer-noassert": "error", 163 | "security/detect-child-process": "off", 164 | "security/detect-disable-mustache-escape": "error", 165 | "security/detect-eval-with-expression": "error", 166 | "security/detect-no-csrf-before-method-override": "error", 167 | "security/detect-non-literal-regexp": "error", 168 | "security/detect-object-injection": "warn", 169 | "security/detect-possible-timing-attacks": "error", 170 | "security/detect-pseudoRandomBytes": "error", 171 | "space-before-function-paren": "off", 172 | "object-curly-spacing": "off" 173 | }, 174 | "parserOptions": { 175 | "ecmaVersion": 2020, 176 | "ecmaFeatures": { 177 | "impliedStrict": true 178 | } 179 | } 180 | }, 181 | "release": { 182 | "branches": [ 183 | "main" 184 | ], 185 | "analyzeCommits": { 186 | "preset": "angular", 187 | "releaseRules": [ 188 | { 189 | "type": "docs", 190 | "release": "patch" 191 | }, 192 | { 193 | "type": "refactor", 194 | "release": "patch" 195 | }, 196 | { 197 | "type": "style", 198 | "release": "patch" 199 | } 200 | ] 201 | } 202 | }, 203 | "plugins": [ 204 | "@semantic-release/commit-analyzer", 205 | "@semantic-release/release-notes-generator", 206 | [ 207 | "@semantic-release/changelog", 208 | { 209 | "changelogFile": "CHANGELOG.md" 210 | } 211 | ], 212 | "@semantic-release/npm", 213 | [ 214 | "@semantic-release/git", 215 | { 216 | "assets": [ 217 | "CHANGELOG.md" 218 | ] 219 | } 220 | ], 221 | "@semantic-release/github" 222 | ], 223 | "files": [ 224 | "src", 225 | "bin" 226 | ] 227 | } 228 | -------------------------------------------------------------------------------- /src/Parser.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import path from 'path' 4 | import fs from 'fs' 5 | import validatePackageName from 'validate-npm-package-name' 6 | 7 | class Parser { 8 | constructor({ directoryPath, manifestType } = {}) { 9 | this.directoryPath = directoryPath 10 | this.manifestType = manifestType 11 | } 12 | 13 | getEarliestSnapshotPerDependency({ snapshots }) { 14 | const result = {} 15 | 16 | if (this.manifestType === 'npm') { 17 | for (const snapshot of snapshots) { 18 | let manifest 19 | 20 | try { 21 | manifest = JSON.parse(snapshot.content) 22 | } catch (_) { 23 | // Skip broken snapshots 24 | continue 25 | } 26 | 27 | const dependencies = this.getDependencies({ manifest }) 28 | 29 | for (const dependency of dependencies) { 30 | if (!result[dependency]) { 31 | result[dependency] = snapshot 32 | } 33 | } 34 | } 35 | } 36 | 37 | return result 38 | } 39 | 40 | getDependenciesFromManifest() { 41 | let projectManifest 42 | 43 | if (this.manifestType === 'npm') { 44 | projectManifest = JSON.parse( 45 | fs.readFileSync(path.resolve(path.join(this.directoryPath, 'package.json'))) 46 | ) 47 | } 48 | 49 | return this.getDependencies({ manifest: projectManifest }) 50 | } 51 | 52 | getDependencies({ manifest }) { 53 | let allDependencies 54 | 55 | if (this.manifestType === 'npm') { 56 | // @TODO need to also add here other sources for deps like peerDeps, etc 57 | const prodDependencies = Object.keys(manifest.dependencies || {}) 58 | const devDependencies = Object.keys(manifest.devDependencies || {}) 59 | 60 | allDependencies = [].concat(prodDependencies, devDependencies) 61 | } 62 | 63 | return allDependencies 64 | } 65 | 66 | classifyScopedDependencies(dependencies) { 67 | const scopedDependencies = [] 68 | const nonScopedDependencies = [] 69 | let scopedOrgs = [] 70 | 71 | for (const dependency of dependencies) { 72 | const packageNameStructure = dependency.match(validatePackageName.scopedPackagePattern) 73 | if (packageNameStructure && packageNameStructure[1]) { 74 | scopedDependencies.push(dependency) 75 | scopedOrgs.push(packageNameStructure[1]) 76 | } else { 77 | nonScopedDependencies.push(dependency) 78 | } 79 | } 80 | 81 | scopedOrgs = [...new Set(scopedOrgs)] 82 | 83 | return { 84 | scopedOrgs, 85 | scopedDependencies, 86 | nonScopedDependencies 87 | } 88 | } 89 | } 90 | 91 | export default Parser 92 | -------------------------------------------------------------------------------- /src/RegistryClient.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import undici from 'undici' 4 | 5 | class RegistryClient { 6 | // @TODO should we instantiate with some configuration 7 | // such as supporting proxy settings and others? 8 | // constructor() {} 9 | 10 | async getPackageMetadataFromRegistry({ packageName }) { 11 | const { body } = await undici.request( 12 | `https://registry.npmjs.org/${encodeURIComponent(packageName)}`, 13 | { 14 | method: 'GET', 15 | headers: { 16 | 'content-type': 'application/json' 17 | } 18 | } 19 | ) 20 | 21 | const dataBuffer = [] 22 | for await (const data of body) { 23 | dataBuffer.push(data) 24 | } 25 | 26 | const packageMetadata = Buffer.concat(dataBuffer).toString('utf8') 27 | const packageMetadataObject = JSON.parse(Buffer.from(packageMetadata).toString('utf8')) 28 | 29 | return packageMetadataObject 30 | } 31 | } 32 | 33 | export default RegistryClient 34 | -------------------------------------------------------------------------------- /src/RepoManager.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import childProcess from 'child_process' 4 | 5 | class RepoManager { 6 | constructor({ directoryPath }) { 7 | this.directoryPath = directoryPath 8 | } 9 | 10 | getFileSnapshots({ filepath }) { 11 | const { stdout } = childProcess.spawnSync( 12 | 'git', 13 | ['log', '--pretty=format:%ct %H', '--first-parent', '--', filepath], 14 | { cwd: this.directoryPath } 15 | ) 16 | const commitsString = Buffer.from(stdout).toString('utf8') 17 | 18 | return ( 19 | commitsString 20 | .split('\n') 21 | .map(line => { 22 | const [unixTs, hash] = line.split(' ') 23 | 24 | return { 25 | ts: unixTs * 1000, 26 | hash, 27 | content: this.getFileForCommit({ 28 | hash, 29 | filepath 30 | }) 31 | } 32 | }) 33 | // Order snapshots from oldest to newest 34 | .reverse() 35 | ) 36 | } 37 | 38 | getFileForCommit({ hash, filepath }) { 39 | const { stdout } = childProcess.spawnSync('git', ['show', `${hash}:${filepath}`], { 40 | cwd: this.directoryPath 41 | }) 42 | return Buffer.from(stdout).toString('utf8') 43 | } 44 | } 45 | 46 | export default RepoManager 47 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import RepoManager from './RepoManager.js' 4 | import Parser from './Parser.js' 5 | import RegistryClient from './RegistryClient.js' 6 | 7 | async function testProject({ projectPath, log, debugMode, privatePackagesList = [] }) { 8 | const registryClient = new RegistryClient() 9 | const repoManager = new RepoManager({ directoryPath: projectPath }) 10 | 11 | const parser = new Parser({ 12 | directoryPath: projectPath, 13 | manifestType: 'npm' 14 | }) 15 | 16 | const allDependencies = parser.getDependenciesFromManifest() 17 | const { nonScopedDependencies } = parser.classifyScopedDependencies(allDependencies) 18 | // @TODO warn the user about `scopedDeps` and `scopedDependencies` to make sure they own it 19 | 20 | log() 21 | log('Reviewing your dependencies...') 22 | log() 23 | 24 | const snapshots = repoManager.getFileSnapshots({ filepath: 'package.json' }) 25 | const earliestSnapshotPerDependency = parser.getEarliestSnapshotPerDependency({ snapshots }) 26 | 27 | // Make all requests in parallel and await later when it needed 28 | const packagesMetadataRequests = nonScopedDependencies.reduce((acc, dependency) => { 29 | acc[dependency] = registryClient.getPackageMetadataFromRegistry({ 30 | packageName: dependency 31 | }) 32 | return acc 33 | }, {}) 34 | 35 | for (const dependency of nonScopedDependencies) { 36 | log(`Checking dependency: ${dependency}`) 37 | 38 | const packageMetadataFromRegistry = await packagesMetadataRequests[dependency] 39 | const timestampOfPackageInSource = earliestSnapshotPerDependency[dependency] 40 | ? earliestSnapshotPerDependency[dependency].ts 41 | : Date.now() // If a dependency is not in the git history (just added in the working copy) 42 | 43 | let timestampOfPackageInRegistry 44 | if (packageMetadataFromRegistry && packageMetadataFromRegistry.error === 'Not found') { 45 | timestampOfPackageInRegistry = null 46 | } else { 47 | // npmjs keeps time.created always in UTC, it's a ISO 8601 time format string 48 | timestampOfPackageInRegistry = new Date(packageMetadataFromRegistry.time.created).getTime() 49 | } 50 | 51 | const isPrivatePackage = privatePackagesList.includes(dependency) 52 | 53 | // @TODO add debug for: 54 | // console.log('package in source UTC: ', timestampInSource) 55 | // console.log('package in registry: ', timestampOfPackageInRegistry) 56 | 57 | const status = resolveDependencyConfusionStatus({ 58 | isPrivatePackage, 59 | timestampOfPackageInSource, 60 | timestampOfPackageInRegistry 61 | }) 62 | 63 | if (status) { 64 | log(' -> ', status) 65 | } 66 | 67 | if (debugMode && earliestSnapshotPerDependency[dependency]) { 68 | log(' -> introduced via commit sha: ', earliestSnapshotPerDependency[dependency].hash) 69 | } 70 | } 71 | } 72 | 73 | function resolveDependencyConfusionStatus({ 74 | isPrivatePackage, 75 | timestampOfPackageInSource, 76 | timestampOfPackageInRegistry 77 | }) { 78 | let status = null 79 | 80 | // if timestampOfPackageInRegistry exists and has 81 | // numeric values then the package exists in the registry 82 | if (timestampOfPackageInRegistry > 0) { 83 | const timeDiff = timestampOfPackageInSource - timestampOfPackageInRegistry 84 | if (timeDiff < 0) { 85 | // this means that the package was first introduced to source code 86 | // and now there's also a package of this name in a public registry 87 | status = '❌ suspicious' 88 | } else { 89 | if (isPrivatePackage) { 90 | status = '❌ suspicious' 91 | } 92 | } 93 | } else { 94 | status = '⚠️ vulnerable' 95 | } 96 | 97 | return status 98 | } 99 | 100 | export { testProject } 101 | --------------------------------------------------------------------------------