diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index 0178b70d104c2..05afdfde23f45 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -127,6 +127,7 @@ graph LR; npm-->npmcli-mock-registry["@npmcli/mock-registry"]; npm-->npmcli-package-json["@npmcli/package-json"]; npm-->npmcli-promise-spawn["@npmcli/promise-spawn"]; + npm-->npmcli-redact["@npmcli/redact"]; npm-->npmcli-run-script["@npmcli/run-script"]; npm-->npmcli-smoke-tests["@npmcli/smoke-tests"]; npm-->npmcli-template-oss["@npmcli/template-oss"]; @@ -541,6 +542,7 @@ graph LR; npm-->npmcli-mock-registry["@npmcli/mock-registry"]; npm-->npmcli-package-json["@npmcli/package-json"]; npm-->npmcli-promise-spawn["@npmcli/promise-spawn"]; + npm-->npmcli-redact["@npmcli/redact"]; npm-->npmcli-run-script["@npmcli/run-script"]; npm-->npmcli-smoke-tests["@npmcli/smoke-tests"]; npm-->npmcli-template-oss["@npmcli/template-oss"]; @@ -831,4 +833,4 @@ packages higher up the chain. - @npmcli/git, make-fetch-happen, @npmcli/config - @npmcli/installed-package-contents, @npmcli/map-workspaces, cacache, npm-pick-manifest, read-package-json, promzard - @npmcli/docs, @npmcli/fs, npm-bundled, read-package-json-fast, unique-filename, npm-install-checks, npm-package-arg, normalize-package-data, npm-packlist, bin-links, nopt, npmlog, parse-conflict-json, @npmcli/mock-globals, read - - @npmcli/eslint-config, @npmcli/template-oss, ignore-walk, semver, npm-normalize-package-bin, @npmcli/name-from-folder, json-parse-even-better-errors, fs-minipass, ssri, unique-slug, @npmcli/promise-spawn, hosted-git-info, proc-log, validate-npm-package-name, @npmcli/node-gyp, @npmcli/agent, minipass-fetch, @npmcli/query, cmd-shim, read-cmd-shim, write-file-atomic, abbrev, are-we-there-yet, gauge, minify-registry-metadata, ini, @npmcli/disparity-colors, mute-stream, npm-audit-report, npm-user-validate + - @npmcli/eslint-config, @npmcli/template-oss, ignore-walk, semver, npm-normalize-package-bin, @npmcli/name-from-folder, json-parse-even-better-errors, fs-minipass, ssri, unique-slug, @npmcli/promise-spawn, hosted-git-info, proc-log, validate-npm-package-name, @npmcli/node-gyp, @npmcli/agent, minipass-fetch, @npmcli/query, cmd-shim, read-cmd-shim, write-file-atomic, abbrev, are-we-there-yet, gauge, minify-registry-metadata, ini, @npmcli/disparity-colors, mute-stream, @npmcli/redact, npm-audit-report, npm-user-validate diff --git a/lib/commands/adduser.js b/lib/commands/adduser.js index cd4cba60511cb..a69ef366fbf32 100644 --- a/lib/commands/adduser.js +++ b/lib/commands/adduser.js @@ -1,5 +1,5 @@ const log = require('../utils/log-shim.js') -const replaceInfo = require('../utils/replace-info.js') +const { redactLog: replaceInfo } = require('@npmcli/redact') const auth = require('../utils/auth.js') const BaseCommand = require('../base-command.js') diff --git a/lib/commands/login.js b/lib/commands/login.js index dc4ed8a67acd9..b498a3bf2ecd8 100644 --- a/lib/commands/login.js +++ b/lib/commands/login.js @@ -1,5 +1,5 @@ const log = require('../utils/log-shim.js') -const replaceInfo = require('../utils/replace-info.js') +const { redactLog: replaceInfo } = require('@npmcli/redact') const auth = require('../utils/auth.js') const BaseCommand = require('../base-command.js') diff --git a/lib/commands/publish.js b/lib/commands/publish.js index 63abc50b4745f..0456fd7e8320e 100644 --- a/lib/commands/publish.js +++ b/lib/commands/publish.js @@ -6,7 +6,7 @@ const runScript = require('@npmcli/run-script') const pacote = require('pacote') const npa = require('npm-package-arg') const npmFetch = require('npm-registry-fetch') -const replaceInfo = require('../utils/replace-info.js') +const { redactLog: replaceInfo } = require('@npmcli/redact') const otplease = require('../utils/otplease.js') const { getContents, logTar } = require('../utils/tar.js') diff --git a/lib/npm.js b/lib/npm.js index 0a023f4ac8a30..d05b74ac74b83 100644 --- a/lib/npm.js +++ b/lib/npm.js @@ -12,7 +12,7 @@ const LogFile = require('./utils/log-file.js') const Timers = require('./utils/timers.js') const Display = require('./utils/display.js') const log = require('./utils/log-shim') -const replaceInfo = require('./utils/replace-info.js') +const { redactLog: replaceInfo } = require('@npmcli/redact') const updateNotifier = require('./utils/update-notifier.js') const pkg = require('../package.json') const { deref } = require('./utils/cmd-list.js') diff --git a/lib/utils/audit-error.js b/lib/utils/audit-error.js index aaf35566fc030..f9850d718b198 100644 --- a/lib/utils/audit-error.js +++ b/lib/utils/audit-error.js @@ -1,5 +1,5 @@ const log = require('./log-shim') -const replaceInfo = require('./replace-info.js') +const { redactLog: replaceInfo } = require('@npmcli/redact') // print an error or just nothing if the audit report has an error // this is called by the audit command, and by the reify-output util diff --git a/lib/utils/error-message.js b/lib/utils/error-message.js index e3d6c3526936f..fc7be8301662e 100644 --- a/lib/utils/error-message.js +++ b/lib/utils/error-message.js @@ -1,7 +1,7 @@ const { format } = require('util') const { resolve } = require('path') const nameValidator = require('validate-npm-package-name') -const replaceInfo = require('./replace-info.js') +const { redactLog: replaceInfo } = require('@npmcli/redact') const { report } = require('./explain-eresolve.js') const log = require('./log-shim') diff --git a/lib/utils/exit-handler.js b/lib/utils/exit-handler.js index 25cecd170a584..8b4ab45c4d474 100644 --- a/lib/utils/exit-handler.js +++ b/lib/utils/exit-handler.js @@ -3,7 +3,7 @@ const fs = require('fs') const log = require('./log-shim.js') const errorMessage = require('./error-message.js') -const replaceInfo = require('./replace-info.js') +const { redactLog: replaceInfo } = require('@npmcli/redact') let npm = null // set by the cli let exitHandlerCalled = false diff --git a/lib/utils/replace-info.js b/lib/utils/replace-info.js deleted file mode 100644 index b9ce61935ffb7..0000000000000 --- a/lib/utils/replace-info.js +++ /dev/null @@ -1,31 +0,0 @@ -const { cleanUrl } = require('npm-registry-fetch') -const isString = (v) => typeof v === 'string' - -// split on \s|= similar to how nopt parses options -const splitAndReplace = (str) => { - // stateful regex, don't move out of this scope - const splitChars = /[\s=]/g - - let match = null - let result = '' - let index = 0 - while (match = splitChars.exec(str)) { - result += cleanUrl(str.slice(index, match.index)) + match[0] - index = splitChars.lastIndex - } - - return result + cleanUrl(str.slice(index)) -} - -// replaces auth info in an array of arguments or in a strings -function replaceInfo (arg) { - if (isString(arg)) { - return splitAndReplace(arg) - } else if (Array.isArray(arg)) { - return arg.map((a) => isString(a) ? splitAndReplace(a) : a) - } - - return arg -} - -module.exports = replaceInfo diff --git a/node_modules/.gitignore b/node_modules/.gitignore index a8aebf722351e..04c142522b2fe 100644 --- a/node_modules/.gitignore +++ b/node_modules/.gitignore @@ -33,6 +33,7 @@ !/@npmcli/package-json !/@npmcli/promise-spawn !/@npmcli/query +!/@npmcli/redact !/@npmcli/run-script !/@pkgjs/ /@pkgjs/* diff --git a/node_modules/@npmcli/redact/LICENSE b/node_modules/@npmcli/redact/LICENSE new file mode 100644 index 0000000000000..c21644115c85d --- /dev/null +++ b/node_modules/@npmcli/redact/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 npm + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/node_modules/@npmcli/redact/lib/index.js b/node_modules/@npmcli/redact/lib/index.js new file mode 100644 index 0000000000000..e5b5e74157c2a --- /dev/null +++ b/node_modules/@npmcli/redact/lib/index.js @@ -0,0 +1,59 @@ +const { URL } = require('url') + +const REPLACE = '***' +const TOKEN_REGEX = /\bnpm_[a-zA-Z0-9]{36}\b/g +const GUID_REGEX = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/g + +const redact = (value) => { + if (typeof value !== 'string' || !value) { + return value + } + + let urlValue + try { + urlValue = new URL(value) + } catch { + // If it's not a URL then we can ignore all errors + } + + if (urlValue?.password) { + urlValue.password = REPLACE + value = urlValue.toString() + } + + return value + .replace(TOKEN_REGEX, `npm_${REPLACE}`) + .replace(GUID_REGEX, REPLACE) +} + +// split on \s|= similar to how nopt parses options +const splitAndRedact = (str) => { + // stateful regex, don't move out of this scope + const splitChars = /[\s=]/g + + let match = null + let result = '' + let index = 0 + while (match = splitChars.exec(str)) { + result += redact(str.slice(index, match.index)) + match[0] + index = splitChars.lastIndex + } + + return result + redact(str.slice(index)) +} + +// replaces auth info in an array of arguments or in a strings +const redactLog = (arg) => { + if (typeof arg === 'string') { + return splitAndRedact(arg) + } else if (Array.isArray(arg)) { + return arg.map((a) => typeof a === 'string' ? splitAndRedact(a) : a) + } + + return arg +} + +module.exports = { + redact, + redactLog, +} diff --git a/node_modules/@npmcli/redact/package.json b/node_modules/@npmcli/redact/package.json new file mode 100644 index 0000000000000..1fc64a4c02f28 --- /dev/null +++ b/node_modules/@npmcli/redact/package.json @@ -0,0 +1,45 @@ +{ + "name": "@npmcli/redact", + "version": "1.1.0", + "description": "Redact sensitive npm information from output", + "main": "lib/index.js", + "scripts": { + "test": "tap", + "lint": "eslint \"**/*.{js,cjs,ts,mjs,jsx,tsx}\"", + "postlint": "template-oss-check", + "template-oss-apply": "template-oss-apply --force", + "lintfix": "npm run lint -- --fix", + "snap": "tap", + "posttest": "npm run lint" + }, + "keywords": [], + "author": "GitHub Inc.", + "license": "ISC", + "files": [ + "bin/", + "lib/" + ], + "repository": { + "type": "git", + "url": "https://github.com/npm/redact.git" + }, + "templateOSS": { + "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", + "version": "4.21.3", + "publish": true + }, + "tap": { + "nyc-arg": [ + "--exclude", + "tap-snapshots/**" + ] + }, + "devDependencies": { + "@npmcli/eslint-config": "^4.0.2", + "@npmcli/template-oss": "4.21.3", + "tap": "^16.3.10" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } +} diff --git a/package-lock.json b/package-lock.json index 1b181ce94ffd9..324c60c2a27e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@npmcli/map-workspaces", "@npmcli/package-json", "@npmcli/promise-spawn", + "@npmcli/redact", "@npmcli/run-script", "@sigstore/tuf", "abbrev", @@ -95,6 +96,7 @@ "@npmcli/map-workspaces": "^3.0.4", "@npmcli/package-json": "^5.0.0", "@npmcli/promise-spawn": "^7.0.1", + "@npmcli/redact": "^1.1.0", "@npmcli/run-script": "^7.0.4", "@sigstore/tuf": "^2.3.2", "abbrev": "^2.0.0", @@ -1904,6 +1906,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/@npmcli/redact": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-1.1.0.tgz", + "integrity": "sha512-PfnWuOkQgu7gCbnSsAisaX7hKOdZ4wSAhAzH3/ph5dSGau52kCRrMMGbiSQLwyTZpgldkZ49b0brkOr1AzGBHQ==", + "inBundle": true, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, "node_modules/@npmcli/run-script": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-7.0.4.tgz", diff --git a/package.json b/package.json index 4986f7b89db70..247df83d014d0 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "@npmcli/map-workspaces": "^3.0.4", "@npmcli/package-json": "^5.0.0", "@npmcli/promise-spawn": "^7.0.1", + "@npmcli/redact": "^1.1.0", "@npmcli/run-script": "^7.0.4", "@sigstore/tuf": "^2.3.2", "abbrev": "^2.0.0", @@ -130,6 +131,7 @@ "@npmcli/map-workspaces", "@npmcli/package-json", "@npmcli/promise-spawn", + "@npmcli/redact", "@npmcli/run-script", "@sigstore/tuf", "abbrev", diff --git a/test/lib/utils/replace-info.js b/test/lib/utils/replace-info.js deleted file mode 100644 index c1e77e943b491..0000000000000 --- a/test/lib/utils/replace-info.js +++ /dev/null @@ -1,116 +0,0 @@ -const t = require('tap') -const replaceInfo = require('../../../lib/utils/replace-info.js') - -t.equal( - replaceInfo(), - undefined, - 'should return undefined item' -) - -t.equal( - replaceInfo(null), - null, - 'should return null' -) - -t.equal( - replaceInfo(1234), - 1234, - 'should return numbers' -) - -t.equal( - replaceInfo(' == = = '), - ' == = = ', - 'should return same string with only separators' -) - -t.equal( - replaceInfo(''), - '', - 'should return empty string' -) - -t.equal( - replaceInfo('https://user:pass@registry.npmjs.org/'), - 'https://user:***@registry.npmjs.org/', - 'should replace single item' -) - -t.equal( - replaceInfo(`https://registry.npmjs.org/path/npm_${'a'.repeat('36')}`), - 'https://registry.npmjs.org/path/npm_***', - 'should replace single item token' -) - -t.equal( - replaceInfo('https://example.npmjs.org'), - 'https://example.npmjs.org', - 'should not replace single item with no password' -) - -t.equal( - replaceInfo('foo bar https://example.npmjs.org lorem ipsum'), - 'foo bar https://example.npmjs.org lorem ipsum', - 'should not replace single item with no password with multiple items' -) - -t.equal( - replaceInfo('https://user:pass@registry.npmjs.org/ http://a:b@reg.github.com'), - 'https://user:***@registry.npmjs.org/ http://a:***@reg.github.com/', - 'should replace multiple items on a string' -) - -t.equal( - replaceInfo('Something https://user:pass@registry.npmjs.org/ foo bar'), - 'Something https://user:***@registry.npmjs.org/ foo bar', - 'should replace single item within a phrase' -) - -t.equal( - replaceInfo('Something --x=https://user:pass@registry.npmjs.org/ foo bar'), - 'Something --x=https://user:***@registry.npmjs.org/ foo bar', - 'should replace single item within a phrase separated by =' -) - -t.same( - replaceInfo([ - 'Something https://user:pass@registry.npmjs.org/ foo bar', - 'http://foo:bar@registry.npmjs.org', - 'http://example.npmjs.org', - ]), - [ - 'Something https://user:***@registry.npmjs.org/ foo bar', - 'http://foo:***@registry.npmjs.org/', - 'http://example.npmjs.org', - ], - 'should replace items in an array' -) - -t.same( - replaceInfo([ - 'Something --x=https://user:pass@registry.npmjs.org/ foo bar', - '--url=http://foo:bar@registry.npmjs.org', - '--url=http://example.npmjs.org', - ]), - [ - 'Something --x=https://user:***@registry.npmjs.org/ foo bar', - '--url=http://foo:***@registry.npmjs.org/', - '--url=http://example.npmjs.org', - ], - 'should replace items in an array with equals' -) - -t.same( - replaceInfo([ - 'Something https://user:pass@registry.npmjs.org/ foo bar', - null, - [], - ]), - [ - 'Something https://user:***@registry.npmjs.org/ foo bar', - null, - [], - ], - 'should ignore invalid items of array' -)