Skip to content

Commit 855b736

Browse files
MaxKlessFrozenPandaz
authored andcommitted
fix(core): check nx packages for provenance config before running nx migrate (#32557)
(cherry picked from commit 1ff26dd)
1 parent 416bb04 commit 855b736

File tree

13 files changed

+276
-58
lines changed

13 files changed

+276
-58
lines changed

packages/devkit/src/utils/package-json.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -503,16 +503,22 @@ export function ensurePackage<T extends any = any>(
503503
windowsHide: false,
504504
});
505505
}
506-
let addCommand = getPackageManagerCommand(packageManager).addDev;
506+
const pmCommands = getPackageManagerCommand(packageManager);
507+
let addCommand = pmCommands.addDev;
507508
if (packageManager === 'pnpm') {
508509
addCommand = 'pnpm add -D'; // we need to ensure that we are not using workspace command
509510
}
510511

511-
execSync(`${addCommand} ${pkg}@${requiredVersion}`, {
512-
cwd: tempDir,
513-
stdio: isVerbose ? 'inherit' : 'ignore',
514-
windowsHide: false,
515-
});
512+
execSync(
513+
`${addCommand} ${pkg}@${requiredVersion} ${
514+
pmCommands.ignoreScriptsFlag ?? ''
515+
}`,
516+
{
517+
cwd: tempDir,
518+
stdio: isVerbose ? 'inherit' : 'ignore',
519+
windowsHide: false,
520+
}
521+
);
516522

517523
addToNodePath(join(workspaceRoot, 'node_modules'));
518524
addToNodePath(join(tempDir, 'node_modules'));

packages/nuxt/src/plugins/__snapshots__/plugin.spec.ts.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Jest Snapshot v1, https://goo.gl/fbAQLP
1+
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
22

33
exports[`@nx/nuxt/plugin not root project should create nodes 1`] = `
44
{

packages/nx/src/command-line/migrate/command-object.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export const yargsMigrateCommand: CommandModule = {
1010
builder: (yargs) =>
1111
linkToNxDevAndExamples(withMigrationOptions(yargs), 'migrate'),
1212
handler: async () => {
13-
(await import('./migrate')).runMigration();
13+
await (await import('./migrate')).runMigration();
1414
process.exit(0);
1515
},
1616
};

packages/nx/src/command-line/migrate/migrate.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ import {
7979
readProjectsConfigurationFromProjectGraph,
8080
} from '../../project-graph/project-graph';
8181
import { formatFilesWithPrettierIfAvailable } from '../../generators/internal-utils/format-changed-files-with-prettier-if-available';
82+
import {
83+
ensurePackageHasProvenance,
84+
getNxPackageGroup,
85+
} from '../../utils/provenance';
8286

8387
export interface ResolvedMigrationConfiguration extends MigrationsJson {
8488
packageGroup?: ArrayPackageGroup;
@@ -990,6 +994,9 @@ async function getPackageMigrationsUsingRegistry(
990994
packageName: string,
991995
packageVersion: string
992996
): Promise<ResolvedMigrationConfiguration> {
997+
if (getNxPackageGroup().includes(packageName)) {
998+
await ensurePackageHasProvenance(packageName, packageVersion);
999+
}
9931000
// check if there are migrations in the packages by looking at the
9941001
// registry directly
9951002
const migrationsConfig = await getPackageMigrationsConfigFromRegistry(
@@ -1103,6 +1110,10 @@ async function getPackageMigrationsUsingInstall(
11031110

11041111
let result: ResolvedMigrationConfiguration;
11051112

1113+
if (getNxPackageGroup().includes(packageName)) {
1114+
await ensurePackageHasProvenance(packageName, packageVersion);
1115+
}
1116+
11061117
try {
11071118
const pmc = getPackageManagerCommand(detectPackageManager(dir), dir);
11081119

@@ -1464,10 +1475,13 @@ function runInstall(nxWorkspaceRoot?: string) {
14641475
if (packageManager ?? detectPackageManager() === 'npm') {
14651476
process.env.npm_config_legacy_peer_deps ??= 'true';
14661477
}
1478+
const installCommand = `${pmCommands.install} ${
1479+
pmCommands.ignoreScriptsFlag ?? ''
1480+
}`;
14671481
output.log({
1468-
title: `Running '${pmCommands.install}' to make sure necessary packages are installed`,
1482+
title: `Running '${installCommand}' to make sure necessary packages are installed`,
14691483
});
1470-
execSync(pmCommands.install, {
1484+
execSync(installCommand, {
14711485
stdio: [0, 1, 2],
14721486
windowsHide: false,
14731487
cwd: nxWorkspaceRoot ?? process.cwd(),
@@ -1792,14 +1806,14 @@ export async function migrate(
17921806
});
17931807
}
17941808

1795-
export function runMigration() {
1809+
export async function runMigration() {
17961810
const runLocalMigrate = () => {
17971811
runNxSync(`_migrate ${process.argv.slice(3).join(' ')}`, {
17981812
stdio: ['inherit', 'inherit', 'inherit'],
17991813
});
18001814
};
18011815
if (process.env.NX_MIGRATE_USE_LOCAL === undefined) {
1802-
const p = nxCliPath();
1816+
const p = await nxCliPath();
18031817
if (p === null) {
18041818
runLocalMigrate();
18051819
} else {
@@ -1867,10 +1881,12 @@ export function getImplementationPath(
18671881
return { path: implPath, fnSymbol };
18681882
}
18691883

1870-
export function nxCliPath(nxWorkspaceRoot?: string) {
1884+
export async function nxCliPath(nxWorkspaceRoot?: string) {
18711885
const version = process.env.NX_MIGRATE_CLI_VERSION || 'latest';
18721886
const isVerbose = process.env.NX_VERBOSE_LOGGING === 'true';
18731887

1888+
await ensurePackageHasProvenance('nx', version);
1889+
18741890
try {
18751891
const packageManager = detectPackageManager();
18761892
const pmc = getPackageManagerCommand(packageManager);
@@ -1913,7 +1929,7 @@ export function nxCliPath(nxWorkspaceRoot?: string) {
19131929
}
19141930
}
19151931

1916-
execSync(pmc.install, {
1932+
execSync(`${pmc.install} ${pmc.ignoreScriptsFlag ?? ''}`, {
19171933
cwd: tmpDir,
19181934
stdio,
19191935
windowsHide: false,

packages/nx/src/utils/package-manager.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ export interface PackageManagerCommands {
5151
registryConfigKey: string,
5252
tag: string
5353
) => string;
54+
// yarn berry doesn't support ignoring scripts via flag
55+
ignoreScriptsFlag?: string;
5456
}
5557

5658
/**
@@ -128,6 +130,7 @@ export function getPackageManagerCommand(
128130
useBerry = true;
129131
}
130132

133+
// new versions of yarn only support ignoring scripts via .yarnrc.yml
131134
return {
132135
preInstall: `yarn set version ${yarnVersion}`,
133136
install: 'yarn',
@@ -141,7 +144,7 @@ export function getPackageManagerCommand(
141144
addDev: useBerry ? 'yarn add -D' : 'yarn add -D -W',
142145
rm: 'yarn remove',
143146
exec: 'yarn',
144-
dlx: useBerry ? 'yarn dlx' : 'yarn',
147+
dlx: useBerry ? 'yarn dlx' : 'npx',
145148
run: (script: string, args?: string) =>
146149
`yarn ${script}${args ? ` ${args}` : ''}`,
147150
list: useBerry ? 'yarn info --name-only' : 'yarn list',
@@ -150,6 +153,7 @@ export function getPackageManagerCommand(
150153
: 'yarn config get registry',
151154
publish: (packageRoot, registry, registryConfigKey, tag) =>
152155
`npm publish "${packageRoot}" --json --"${registryConfigKey}=${registry}" --tag=${tag}`,
156+
ignoreScriptsFlag: useBerry ? undefined : `--ignore-scripts`,
153157
};
154158
},
155159
pnpm: () => {
@@ -196,6 +200,7 @@ export function getPackageManagerCommand(
196200
`pnpm publish "${packageRoot}" --json --"${
197201
allowRegistryConfigKey ? registryConfigKey : 'registry'
198202
}=${registry}" --tag=${tag} --no-git-checks`,
203+
ignoreScriptsFlag: '--ignore-scripts',
199204
};
200205
},
201206
npm: () => {
@@ -217,6 +222,7 @@ export function getPackageManagerCommand(
217222
getRegistryUrl: 'npm config get registry',
218223
publish: (packageRoot, registry, registryConfigKey, tag) =>
219224
`npm publish "${packageRoot}" --json --"${registryConfigKey}=${registry}" --tag=${tag}`,
225+
ignoreScriptsFlag: '--ignore-scripts',
220226
};
221227
},
222228
bun: () => {
@@ -235,6 +241,7 @@ export function getPackageManagerCommand(
235241
// Unlike npm, bun publish does not support a custom registryConfigKey option
236242
publish: (packageRoot, registry, registryConfigKey, tag) =>
237243
`bun publish --cwd="${packageRoot}" --json --registry="${registry}" --tag=${tag}`,
244+
ignoreScriptsFlag: '--ignore-scripts',
238245
};
239246
},
240247
};
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import { execFile } from 'child_process';
2+
import { join } from 'path';
3+
import { promisify } from 'util';
4+
import { readJsonFile } from './fileutils';
5+
6+
/*
7+
* Verifies that the given npm package has provenance attestations
8+
* generated by the GitHub Actions workflow at .github/workflows/publish.yml
9+
* in the nrwl/nx repository.
10+
*
11+
* Will throw if the package does not have valid provenance.
12+
*/
13+
export async function ensurePackageHasProvenance(
14+
packageName: string,
15+
packageVersion: string
16+
): Promise<void> {
17+
// this is used for locally released versions without provenance
18+
// do not set this for other reasons or you might be exposed to security risks
19+
if (process.env.NX_SKIP_PROVENANCE_CHECK) {
20+
return;
21+
}
22+
23+
const execFileAsync = promisify(execFile);
24+
25+
const npmViewResult = JSON.parse(
26+
(
27+
await execFileAsync(
28+
'npm',
29+
['view', `${packageName}@${packageVersion}`, '--json', '--silent'],
30+
{
31+
timeout: 20000,
32+
}
33+
)
34+
).stdout.trim()
35+
);
36+
37+
const attURL = npmViewResult.dist?.attestations?.url;
38+
39+
if (!attURL)
40+
throw noProvenanceError(
41+
packageName,
42+
packageVersion,
43+
'No attestation URL found'
44+
);
45+
46+
const attestations = (await (await fetch(attURL)).json()) as {
47+
attestations: Attestation[];
48+
};
49+
50+
const provenanceAttestation = attestations?.attestations?.find(
51+
(a) => a.predicateType === 'https://slsa.dev/provenance/v1'
52+
);
53+
if (!provenanceAttestation)
54+
throw noProvenanceError(
55+
packageName,
56+
packageVersion,
57+
'No provenance attestation found'
58+
);
59+
60+
const dsseEnvelopePayload = JSON.parse(
61+
Buffer.from(
62+
provenanceAttestation.bundle.dsseEnvelope.payload,
63+
'base64'
64+
).toString()
65+
);
66+
67+
const workflowParameters =
68+
dsseEnvelopePayload?.predicate?.buildDefinition?.externalParameters
69+
?.workflow;
70+
71+
// verify that provenance was actually generated from the right publishing workflow
72+
if (workflowParameters?.repository !== 'https://github.com/nrwl/nx') {
73+
throw noProvenanceError(
74+
packageName,
75+
packageVersion,
76+
'Repository does not match nrwl/nx'
77+
);
78+
}
79+
if (workflowParameters?.path !== '.github/workflows/publish.yml') {
80+
throw noProvenanceError(
81+
packageName,
82+
packageVersion,
83+
'Publishing workflow does not match .github/workflows/publish.yml'
84+
);
85+
}
86+
if (workflowParameters?.ref !== `refs/tags/${npmViewResult.version}`) {
87+
throw noProvenanceError(
88+
packageName,
89+
packageVersion,
90+
`Version ref does not match refs/tags/${npmViewResult.version}`
91+
);
92+
}
93+
94+
// verify that provenance was generated from the exact same artifact as the one we are installing
95+
const distSha = Buffer.from(
96+
npmViewResult.dist.integrity.replace('sha512-', ''),
97+
'base64'
98+
).toString('hex');
99+
const attestationSha = dsseEnvelopePayload?.subject[0]?.digest.sha512;
100+
if (distSha !== attestationSha) {
101+
throw noProvenanceError(
102+
packageName,
103+
packageVersion,
104+
'Integrity hash does not match attestation hash'
105+
);
106+
}
107+
return;
108+
}
109+
110+
export const noProvenanceError = (
111+
packageName: string,
112+
packageVersion: string,
113+
error?: string
114+
) =>
115+
`An error occurred while checking the provenance of ${packageName}@${packageVersion}. This could indicate a security risk. Please double check https://www.npmjs.com/package/${packageName} to see if the package is published correctly or file an issue at https://github.com/nrwl/nx/issues \n Error: ${
116+
error ?? ''
117+
}`;
118+
119+
export function getNxPackageGroup(): string[] {
120+
const packageJsonPath = join(__dirname, '../../package.json');
121+
const packageJson = readJsonFile(packageJsonPath);
122+
const packages = packageJson['nx-migrations'].packageGroup.filter(
123+
(dep) => typeof dep === 'string' && dep.startsWith('@nx/')
124+
);
125+
packages.push('nx');
126+
return packages;
127+
}
128+
129+
type Attestation = {
130+
predicateType: string;
131+
bundle: {
132+
dsseEnvelope: {
133+
payload: string; // base64 encoded JSON
134+
payloadType: string;
135+
signatures: {
136+
keyid: string;
137+
sig: string;
138+
}[];
139+
};
140+
mediaType: string;
141+
[x: string]: unknown;
142+
};
143+
[x: string]: unknown;
144+
};
145+
146+
// referh to https://slsa.dev/spec/v1.1/provenance#schema
147+
export type DecodedAttestationPayload = {
148+
_type: 'https://in-toto.io/Statement/v1';
149+
subject: unknown[];
150+
predicateType: 'https://slsa.dev/provenance/v1';
151+
predicate: {
152+
buildDefinition: {
153+
buildType: string;
154+
externalParameters: Record<string, any>;
155+
internalParameters?: Record<string, any>;
156+
resolvedDependencies?: ResourceDescriptor[];
157+
};
158+
runDetails: {
159+
builder: {
160+
id: string;
161+
builderDependencies?: ResourceDescriptor[];
162+
version?: Record<string, string>;
163+
};
164+
metadata?: {
165+
invocationId?: string;
166+
startedOn?: string; // <YYYY>-<MM>-<DD>T<hh>:<mm>:<ss>Z
167+
finishedOn?: string; // <YYYY>-<MM>-<DD>T<hh>:<mm>:<ss>Z
168+
};
169+
byproducts?: ResourceDescriptor[];
170+
};
171+
};
172+
};
173+
174+
export interface ResourceDescriptor {
175+
uri?: string;
176+
digest?: {
177+
sha256?: string;
178+
sha512?: string;
179+
gitCommit?: string;
180+
[key: string]: string | undefined;
181+
};
182+
name?: string;
183+
downloadLocation?: string;
184+
mediaType?: string;
185+
content?: string;
186+
annotations?: {
187+
[key: string]: any;
188+
};
189+
}

packages/react/src/plugins/__snapshots__/router-plugin.spec.ts.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Jest Snapshot v1, https://goo.gl/fbAQLP
1+
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
22

33
exports[`@nx/react/react-router-plugin React Router should create nodes by default 1`] = `
44
[

packages/remix/src/plugins/__snapshots__/plugin.spec.ts.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Jest Snapshot v1, https://goo.gl/fbAQLP
1+
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
22

33
exports[`@nx/remix/plugin Remix Classic Compiler non-root project should create nodes 1`] = `
44
[

0 commit comments

Comments
 (0)