diff --git a/common/changes/@microsoft/rush/fix-disallow-sha1-error-with-pnpm9_2025-12-29-09-14.json b/common/changes/@microsoft/rush/fix-disallow-sha1-error-with-pnpm9_2025-12-29-09-14.json new file mode 100644 index 00000000000..c3e1099e41a --- /dev/null +++ b/common/changes/@microsoft/rush/fix-disallow-sha1-error-with-pnpm9_2025-12-29-09-14.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Fix an issue where packages listed in the `pnpmLockfilePolicies.disallowInsecureSha1.exemptPackageVersions` `common/config/rush/pnpm-config.json` config file are not exempted in PNPM 9.", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts b/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts index ede98dae323..97068b557ec 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts @@ -605,13 +605,30 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { * Example: "/@typescript-eslint/experimental-utils/5.9.1_eslint@8.6.0+typescript@4.4.4" --> "/@typescript-eslint/experimental-utils/5.9.1" */ private _parseDependencyPath(packagePath: string): string { - let depPath: string = packagePath; - if (this.shrinkwrapFileMajorVersion >= ShrinkwrapFileMajorVersion.V6) { - depPath = this._convertLockfileV6DepPathToV5DepPath(packagePath); + let name: string | undefined; + let version: string | undefined; + + /** + * For PNPM lockfile version 9 and above, use pnpmKitV9 to parse the dependency path. + * Example: "@some/pkg@1.0.0" --> "@some/pkg@1.0.0" + * Example: "@some/pkg@1.0.0(peer@2.0.0)" --> "@some/pkg@1.0.0" + * Example: "pkg@1.0.0(patch_hash)" --> "pkg@1.0.0" + */ + if (this.shrinkwrapFileMajorVersion >= ShrinkwrapFileMajorVersion.V9) { + ({ name, version } = pnpmKitV9.dependencyPath.parse(packagePath)); + } else { + if (this.shrinkwrapFileMajorVersion >= ShrinkwrapFileMajorVersion.V6) { + packagePath = this._convertLockfileV6DepPathToV5DepPath(packagePath); + } + + ({ name, version } = pnpmKitV8.dependencyPath.parse(packagePath)); } - const pkgInfo: ReturnType = - pnpmKitV8.dependencyPath.parse(depPath); - return this._getPackageId(pkgInfo.name as string, pkgInfo.version as string); + + if (!name || !version) { + throw new InternalError(`Unable to parse package path: ${packagePath}`); + } + + return this._getPackageId(name, version); } /** @override */ diff --git a/libraries/rush-lib/src/logic/pnpm/test/PnpmShrinkwrapFile.test.ts b/libraries/rush-lib/src/logic/pnpm/test/PnpmShrinkwrapFile.test.ts index 16f68c967b6..adc4ca5bd11 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmShrinkwrapFile.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmShrinkwrapFile.test.ts @@ -6,6 +6,9 @@ import { PnpmShrinkwrapFile, parsePnpm9DependencyKey, parsePnpmDependencyKey } f import { RushConfiguration } from '../../../api/RushConfiguration'; import type { RushConfigurationProject } from '../../../api/RushConfigurationProject'; import type { Subspace } from '../../../api/Subspace'; +import { StringBufferTerminalProvider, Terminal } from '@rushstack/terminal'; +import { PnpmOptionsConfiguration } from '../PnpmOptionsConfiguration'; +import { AlreadyReportedError } from '@rushstack/node-core-library'; const DEPENDENCY_NAME: string = 'dependency_name'; const SCOPED_DEPENDENCY_NAME: string = '@scope/dependency_name'; @@ -483,6 +486,74 @@ snapshots: ) ).resolves.toBe(false); }); + + it('sha1 integrity can be handled when disallowInsecureSha1', async () => { + const project = getMockRushProject(); + const pnpmShrinkwrapFile = getPnpmShrinkwrapFileFromFile( + `${__dirname}/yamlFiles/pnpm-lock-v9/sha1-integrity.yaml`, + project.rushConfiguration.defaultSubspace + ); + + const defaultSubspace = project.rushConfiguration.defaultSubspace; + + const mockPnpmOptions = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( + `${__dirname}/jsonFiles/pnpm-config-disallow-sha1.json`, + defaultSubspace.getSubspaceTempFolderPath() + ); + + jest.spyOn(defaultSubspace, 'getPnpmOptions').mockReturnValue(mockPnpmOptions); + + const spyTerminalWrite = jest.fn(); + const terminal = new Terminal({ + eolCharacter: '\n', + supportsColor: false, + write: spyTerminalWrite + }); + + expect(() => + pnpmShrinkwrapFile.validateShrinkwrapAfterUpdate( + project.rushConfiguration, + project.rushConfiguration.defaultSubspace, + terminal + ) + ).not.toThrow(); + expect(spyTerminalWrite).not.toHaveBeenCalled(); + }); + + it('sha1 integrity can be handled when disallowInsecureSha1', async () => { + const project = getMockRushProject(); + const pnpmShrinkwrapFile = getPnpmShrinkwrapFileFromFile( + `${__dirname}/yamlFiles/pnpm-lock-v9/sha1-integrity-non-exempted-package.yaml`, + project.rushConfiguration.defaultSubspace + ); + + const defaultSubspace = project.rushConfiguration.defaultSubspace; + + const mockPnpmOptions = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( + `${__dirname}/jsonFiles/pnpm-config-disallow-sha1.json`, + defaultSubspace.getSubspaceTempFolderPath() + ); + + jest.spyOn(defaultSubspace, 'getPnpmOptions').mockReturnValue(mockPnpmOptions); + + const terminalProvider: StringBufferTerminalProvider = new StringBufferTerminalProvider(); + const terminal = new Terminal(terminalProvider); + + expect(() => + pnpmShrinkwrapFile.validateShrinkwrapAfterUpdate( + project.rushConfiguration, + project.rushConfiguration.defaultSubspace, + terminal + ) + ).toThrowError(AlreadyReportedError); + expect({ + log: terminalProvider.getOutput(), + warning: terminalProvider.getWarningOutput(), + error: terminalProvider.getErrorOutput(), + verbose: terminalProvider.getVerboseOutput(), + debug: terminalProvider.getDebugOutput() + }).toMatchSnapshot(); + }); }); }); }); diff --git a/libraries/rush-lib/src/logic/pnpm/test/__snapshots__/PnpmShrinkwrapFile.test.ts.snap b/libraries/rush-lib/src/logic/pnpm/test/__snapshots__/PnpmShrinkwrapFile.test.ts.snap new file mode 100644 index 00000000000..9135d73d008 --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/__snapshots__/PnpmShrinkwrapFile.test.ts.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PnpmShrinkwrapFile Check is workspace project modified pnpm lockfile major version 9 sha1 integrity can be handled when disallowInsecureSha1 1`] = ` +Object { + "debug": "", + "error": "Error: An integrity field with \\"sha1\\" was detected in the pnpm-lock.yaml file located in subspace default; this conflicts with the \\"disallowInsecureSha1\\" policy from pnpm-config.json.[n][n]", + "log": "", + "verbose": "", + "warning": "", +} +`; diff --git a/libraries/rush-lib/src/logic/pnpm/test/jsonFiles/pnpm-config-disallow-sha1.json b/libraries/rush-lib/src/logic/pnpm/test/jsonFiles/pnpm-config-disallow-sha1.json new file mode 100644 index 00000000000..a07b8d4cab9 --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/jsonFiles/pnpm-config-disallow-sha1.json @@ -0,0 +1,14 @@ +{ + "pnpmLockfilePolicies": { + "disallowInsecureSha1": { + "enabled": true, + "exemptPackageVersions": { + "@some/sha1-pkg": ["1.4.3"], + "other-sha1-pkg": ["2.0.0"], + "fake-with-patch": ["1.0.0"], + "fake-with-peer": ["1.0.0"], + "fake": ["7.8.1"] + } + } + } +} diff --git a/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v9/sha1-integrity-non-exempted-package.yaml b/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v9/sha1-integrity-non-exempted-package.yaml new file mode 100644 index 00000000000..30a5c5526eb --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v9/sha1-integrity-non-exempted-package.yaml @@ -0,0 +1,66 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +patchedDependencies: + fake-with-patch@1.0.0: + hash: tta6uuavpnftppsegagihriayy + path: patches/fake-with-patch@1.0.0.patch + +importers: + .: + dependencies: + '@some/sha1-pkg': + specifier: ^1.0.0 + version: 1.4.3 + other-sha1-pkg: + specifier: ~2.0.0 + version: 2.0.0 + fake-with-patch: + specifier: 1.0.0 + version: 1.0.0 + fake-with-peer: + specifier: 1.0.0 + version: 1.0.0 + fake-with-npm: + specifier: npm:fake@7.8.1 + version: fake@7.8.1 + fake-non-exempted: + specifier: 1.0.0 + version: 1.0.0 + +packages: + '@some/sha1-pkg@1.4.3': + resolution: { integrity: sha1-KQzv7h3EqVCA2u7BOFut5Vqbl5o= } + + other-sha1-pkg@2.0.0: + resolution: { integrity: sha1-+5v5y5gkJ6x2+X1K3pZ5p8W7m4o= } + + fake-with-patch@1.0.0: + resolution: { integrity: sha1-FAKEPATCHINTEGRITY1234567890= } + + fake-with-peer@1.0.0: + resolution: { integrity: sha1-FAKEPEERINTEGRITY0987654321= } + + fake@7.8.1: + resolution: { integrity: sha1-FAKEPEERINTEGRITY0987654321= } + + fake-non-exempted@1.0.0: + resolution: { integrity: sha1-FAKEPEERINTEGRITY0987654321= } + +snapshots: + '@some/sha1-pkg@1.4.3': {} + + other-sha1-pkg@2.0.0: {} + + fake-with-patch@1.0.0(patch_hash=tta6uuavpnftppsegagihriayy): {} + + fake-with-peer@1.0.0(@some/sha1-pkg@1.4.3): + transitivePeerDependencies: + - '@some/sha1-pkg' + + fake@7.8.1: {} + + fake-non-exempted@1.0.0: {} diff --git a/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v9/sha1-integrity.yaml b/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v9/sha1-integrity.yaml new file mode 100644 index 00000000000..b2dd4e49386 --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v9/sha1-integrity.yaml @@ -0,0 +1,58 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +patchedDependencies: + fake-with-patch@1.0.0: + hash: tta6uuavpnftppsegagihriayy + path: patches/fake-with-patch@1.0.0.patch + +importers: + .: + dependencies: + '@some/sha1-pkg': + specifier: ^1.0.0 + version: 1.4.3 + other-sha1-pkg: + specifier: ~2.0.0 + version: 2.0.0 + fake-with-patch: + specifier: 1.0.0 + version: 1.0.0 + fake-with-peer: + specifier: 1.0.0 + version: 1.0.0 + fake-with-npm: + specifier: npm:fake@7.8.1 + version: fake@7.8.1 + +packages: + '@some/sha1-pkg@1.4.3': + resolution: { integrity: sha1-KQzv7h3EqVCA2u7BOFut5Vqbl5o= } + + other-sha1-pkg@2.0.0: + resolution: { integrity: sha1-+5v5y5gkJ6x2+X1K3pZ5p8W7m4o= } + + fake-with-patch@1.0.0: + resolution: { integrity: sha1-FAKEPATCHINTEGRITY1234567890= } + + fake-with-peer@1.0.0: + resolution: { integrity: sha1-FAKEPEERINTEGRITY0987654321= } + + fake@7.8.1: + resolution: { integrity: sha1-FAKEPEERINTEGRITY0987654321= } + +snapshots: + '@some/sha1-pkg@1.4.3': {} + + other-sha1-pkg@2.0.0: {} + + fake-with-patch@1.0.0(patch_hash=tta6uuavpnftppsegagihriayy): {} + + fake-with-peer@1.0.0(@some/sha1-pkg@1.4.3): + transitivePeerDependencies: + - '@some/sha1-pkg' + + fake@7.8.1: {}