diff --git a/review/npm.md b/review/npm.md index 4711fd4..8341da7 100644 --- a/review/npm.md +++ b/review/npm.md @@ -14,14 +14,17 @@ alternative. - [npm Best Practices Guide](#npm-best-practices-guide) * [TOC](#toc) + * [CI configuration](#ci-configuration) * [Dependency management](#dependency-management) + [Intake](#intake) + [Declaration](#declaration) + + [Project types](#project-types) + [Reproducible installation](#reproducible-installation) - [Vendoring dependencies](#vendoring-dependencies) - [Use a Lockfile](#use-a-lockfile) * [Package-lock.json](#package-lockjson) - * [Shrinkwrap.json](#shrinkwrapjson) + * [npm-shrinkwrap.json](#shrinkwrapjson) + - [Lockfiles and commands](#lockfiles-and-commands) + [Maintenance](#maintenance) * [Release](#release) + [Account](#account) @@ -31,6 +34,18 @@ alternative. + [Scopes](#scopes) + [Private registry configurations](#private-registry-configurations) +## CI configuration + +Follow the [principle of least privilege](https://www.cisa.gov/uscert/bsi/articles/knowledge/principles/least-privilege) +for your CI configuration. + +If you run CI via GitHub Actions, a non-privileged environment is a workflow **without** access to +GitHub secrets and with non-write permissions defined, such as `permissions: read-all`, `permissions:`, +`contents: none`, `contents: read`. For more information about permissions, refer to the [official documentation](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token). + +You may install the [OpenSSF Scorecard Action](https://github.com/ossf/scorecard-action) +to flag non-read permissions on your project. + ## Dependency management ### Intake @@ -94,6 +109,25 @@ commitish are allowed. **Note**: The manifest file ***does not*** list transitive dependencies; it lists only direct project dependencies. +### Project types + +In the rest of this document, we will refer to three types of projects: + +- **Libraries**: These are projects published on the npm registry and consumed +by other projects in the form of API calls. (Their manifest file +typically contains a `main`, `exports`, `browser`, `module`, and/or `types` entry). + +- **Standalone CLIs**: These are projects published on the npm registry +and consumed by end-users in the form of locally installed programs that +are **always** run stand alone via `npx` or via a global install. +An example would be [clipboard-cli](https://github.com/sindresorhus/clipboard-cli). +(Their manifest file contains a `bin` entry). + +- **Application projects**: These are projects that teams collaborate on in +development and deploy, such as web sites and/or web applications. +An example would be a React web app for a company's user-facing SaaS. +(Their manifest file typically contains `"private": true`). + ### Reproducible installation A reproducible installation is one that guarantees the exact same copy the @@ -121,6 +155,11 @@ benefits, including: proxy packages from public registries. Note: Versions are immutable in principle, but immutability is enforced by the registry. +- Improve the accuracy of automated tools such as GitHub's security alerts. + +- Let maintainers test updates before accepting them in the default branch, + e.g., via renovatebot's [stabilityDays](https://docs.renovatebot.com/configuration-options/#stabilitydays). + There are two ways to reliably achieve a reproducible installation: vendoring dependencies and pinning by hash. @@ -172,7 +211,7 @@ cryptographic hash of their content: } ``` -The package-lock.json file is a ***snapshot of an installation*** that allows +The `package-lock.json` file is a ***snapshot of an installation*** that allows later reproduction of the same installation. As such, the lock file is generated or updated via the various commands that install packages, e.g., `npm install`. If some dependencies are missing or not pinned by hash (e.g., @@ -183,80 +222,133 @@ The lock file ***cannot*** be uploaded to a registry, which means that consumers who locally install the package via `npm install` may [see different dependency versions](https://dev.to/saurabhdaware/but-what-the-hell-is-package-lock-json-b04) -than the repo owners used during testing. Using package-lock.json is akin to +than the repo owners used during testing. Using `package-lock.json` is akin to dynamic linking for low-level programming languages: the loader will resolve dependencies at load time using libraries available on the system. Using this lock file leaves the task of deciding dependencies' versions to use to the package consumer. -##### Shrinkwrap.json +##### npm-shrinkwrap.json -[Shrinkwrap.json](https://docs.npmjs.com/cli/v7/configuring-npm/package-lock-json#package-lockjson-vs-npm-shrinkwrapjson) +[npm-shrinkwrap.json](https://docs.npmjs.com/cli/v7/configuring-npm/package-lock-json#package-lockjson-vs-npm-shrinkwrapjson) is another lock file supported by npm. The main difference is that this lock file ***may*** be uploaded to a registry along with the package. This ensures that consumers of the dependency will obtain the same dependencies' hashes as the -repo owners intended. With shrinkwrap.json, the package producer takes +repo owners intended. With `npm-shrinkwrap.json`, the package producer takes responsibility for updating the dependencies on behalf of their consumers. It's akin to static linking for low-level programming languages: everything is declared at packaging time. -To generate the shrinkwrap.json, an existing package-lock.json must be present +To generate the `npm-shrinkwrap.json`, an existing `package-lock.json` must be present and the command [`npm shrinkwrap`](https://docs.npmjs.com/cli/v8/commands/npm-shrinkwrap) must be run. +#### Lockfiles and commands + +Certain `npm` commmands treat the lockfiles as read-only, while others do not. + +The following commands treat the lock file as read-only: + +1. [`npm ci`](https://docs.npmjs.com/cli/v8/commands/npm-ci), used to + install a project and its dependencies, + +1. [`npm install-ci-test`](https://docs.npmjs.com/cli/v8/commands/npm-install-ci-test), + used to install a project and run tests. + +The following commands ***do not*** treat the lock file as read-only, may fetch / install +unpinned dependencies and update the lockfiles: + +1. `npm install`, `npm i`, `npm install -g` + +1. `npm update` + +1. `npm install-test` + +1. `npm pkg set` and `npm pkg delete` + +1. `npm exec`, `npx` + +1. `npm set-script` + **Recommendations:** 1. Developers should declare and commit a manifest file for ***all*** their projects. Use the official [Creating a package.json file](https://docs.npmjs.com/creating-a-package-json-file) documentation to - create the manifest file. + create the manifest file. 1. To add a dependency to a manifest file, ***locally*** run [`npm - install --save `](https://docs.npmjs.com/cli/v8/commands/npm-install). + install --save `](https://docs.npmjs.com/cli/v8/commands/npm-install) + and commit the updated manifest to the repository. + +1. If you need to run a standalone CLI package from the registry, ensure the package is a part of + the dependencies defined in your project via the `package.json` file, prior to + being installed at build-time in your CI or otherwise automated environment. + +1. Developers should declare and commit a lockfile for ***all*** their + projects. The reasoning is that this lockfile will provide the benefits of + [Reproducible installation](#reproducible-installation) + by default for privileged environments (project developers' machines, + CI, production or other environments with access to sensitive data, + such as secrets, PII, write/push permission to the repository, etc). + + When running tests locally, developers should use commands that treat a lockfile + as read-only (see [Lockfiles and commands](#lockfiles-and-commands)), unless they + are intentionally adding / removing a dependency. -1. In automated environments (e.g., in CI or production) or when building - artifacts for end-users (e.g., container images, etc): + Below we explain the type of lockfile acceptable by project type. - 1. Always generate a package-lock.json ***locally*** and commit it to the - repository. +1. If a project is a library: - 1. Never run commands that may update the lock files or fetch unpinned - dependencies: + 1. An `npm-shrinkwrap.json` ***should not*** be published. + The reasoning is that version resolution should be left to the package consumer. + Allow all versions from the minimum to the latest you support, e.g., + `^m.n.o` to pin to a major range; `~m.n.o` to pin to a minor range. Avoid versions + with critical vulnerabilities as much as possible. Visit the [semver + calculator](https://semver.npmjs.com/) to help you define the ranges. - 1. `npm install`, `npm i`, `npm install -g` + 1. The lockfile `package-lock.json` ***should*** be ignored for tests running in CI + (e.g. via `npm install --no-package-lock`). The reasoning is that CI tests should + exercise a wide range of dependency versions in order to discover / fix problems + before the library users do, so tests need to pull the latest versions of packages. - 1. `npm update` + 1. Locally, developers should only run npm commands that treat the lockfile as + read-only (see [Lockfiles and commands](#lockfiles-and-commands)), except + when intentionally adding /removing a dependency. - 1. `npm install-test` + 1. Follow the principle of least privilege in your [CI configuration](#ci-configuration). + This is particularly important since the lockfile is ignored. - 1. `npm pkg set` and `npm pkg delete` +1. If a project is a standalone CLI: - 1. `npm exec`, `npx` + 1. Developers may publish an `npm-shrinkwrap.json`. + Remember that, by declaring an `npm-shrinkwrap.json`, you take responsibility + for rapidly and consistently updating all the dependencies. Your users will not be able + to update or deduplicate them. If you expect your CLI to be used by other projects and defined + in their `package.json` or lockfile, do **not** use `npm-shrinkwrap.json` because it will + hinder dependency resolution for your consumers: follow the recommendations as if your project + was a library. - 1. `npm set-script` + 1. In CI, only run npm commands that treat the lockfile as + read-only (see [Lockfiles and commands](#lockfiles-and-commands)). - 1. Only run commands that treat the lock file as read-only: + 1. Locally, developers should only run npm commands that treat the lockfile as + read-only (see [Lockfiles and commands](#lockfiles-and-commands)), except + when intentionally adding /removing a dependency. - 1. To install a project and its dependencies, use [`npm - ci`](https://docs.npmjs.com/cli/v8/commands/npm-ci). + 1. Follow the principle of least privilege in your [CI configuration](#ci-configuration). - 1. To run tests, run [`npm - install-ci-test`](https://docs.npmjs.com/cli/v8/commands/npm-install-ci-test). +1. If a project is an application: - 1. If you need to run a CLI package from the registry, ensure the package is a part of - the dependencies defined in your project via the `package.json` file, prior to being installed at build-time in your CI or otherwise automated environment. + 1. Developers should declare and commit a lockfile to their repository. -1. If a project is a CLI application (`main` entry in the manifest file), - developers may publish a shrinkwrap.json. + 1. In CI, only run npm commands that treat the lockfile as + read-only (see [Lockfiles and commands](#lockfiles-and-commands)). -1. If a project is a library (`lib` entry in the manifest file), a - shrinkwrap.json should ***not*** be published. The reasoning is that version - resolution should be left to the package consumer. Allow all versions from - the minimum to the latest you support, e.g., `^m.n.o` to pin to a major - range; `~m.n.o` to pin to a minor range. Avoid versions with critical - vulnerabilities as much as possible. Visit the [semver - calculator](https://semver.npmjs.com/) to help you define the ranges. + 1. Locally, developers should only run npm commands that treat the lockfile as + read-only (see [Lockfiles and commands](#lockfiles-and-commands)), except + when intentionally adding /removing a dependency. ### Maintenance