Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/config/options/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3117,8 +3117,8 @@ const options: RenovateOptions[] = [
which run automatically, and are not explicitly added in \`postUpgradeTasks\``,
type: 'array',
subType: 'string',
default: [],
allowedValues: ['goGenerate'],
default: ['gradleWrapper'],
allowedValues: ['goGenerate', 'gradleWrapper'],
stage: 'repository',
globalOnly: true,
},
Expand Down
3 changes: 1 addition & 2 deletions lib/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -533,9 +533,8 @@ export type MergeStrategy =
| 'rebase-merge'
| 'squash';

// ref: https://github.com/renovatebot/renovate/issues/39458
// This list should be added to as any new unsafe execution commands should be permitted
export type AllowedUnsafeExecution = 'goGenerate';
export type AllowedUnsafeExecution = 'goGenerate' | 'gradleWrapper';

// TODO: Proper typings
export interface PackageRule
Expand Down
110 changes: 110 additions & 0 deletions lib/modules/manager/gradle/artifacts.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import os from 'node:os';
import upath from 'upath';
import { mockDeep } from 'vitest-mock-extended';
import { globalConfig } from 'zod/v4/core';

Check failure on line 4 in lib/modules/manager/gradle/artifacts.spec.ts

View workflow job for this annotation

GitHub Actions / lint-other

'globalConfig' is declared but its value is never read.

Check failure on line 4 in lib/modules/manager/gradle/artifacts.spec.ts

View workflow job for this annotation

GitHub Actions / lint-eslint

'globalConfig' is defined but never used. (@typescript-eslint/no-unused-vars)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import { globalConfig } from 'zod/v4/core';

import { GlobalConfig } from '../../../config/global';
import type { RepoGlobalConfig } from '../../../config/types';
import { TEMPORARY_ERROR } from '../../../constants/error-messages';
import { resetPrefetchedImages } from '../../../util/exec/docker';
import { ExecError } from '../../../util/exec/exec-error';
import type { StatusResult } from '../../../util/git/types';
import { getPkgReleases } from '../../datasource';
import { isGradleExecutionAllowed } from './artifacts';
import { updateArtifacts } from '.';
import { envMock, mockExecAll, mockExecSequence } from '~test/exec-util';
import { env, fs, git, logger, partial, scm } from '~test/util';
Expand Down Expand Up @@ -82,6 +84,54 @@
});
});

describe('isGradleExecutionAllowed', () => {
it('returns true when allowedUnsafeExecutions is empty (as it is a default option)', () => {
GlobalConfig.set({
...adminConfig,
allowedUnsafeExecutions: undefined,
});

const res = isGradleExecutionAllowed('gradlew');

expect(res).toBeTrue();
});

it('returns true when allowedUnsafeExecutions includes `gradleWrapper`', () => {
GlobalConfig.set({
...adminConfig,
allowedUnsafeExecutions: ['gradleWrapper'],
});

const res = isGradleExecutionAllowed('gradlew');

expect(res).toBeTrue();
});

it('returns false when allowedUnsafeExecutions does not include `gradleWrapper`', () => {
GlobalConfig.set({
...adminConfig,
allowedUnsafeExecutions: [],
});

const res = isGradleExecutionAllowed('gradlew');

expect(res).toBeFalse();
});

it('logs when allowedUnsafeExecutions does not include `gradleWrapper`', () => {
GlobalConfig.set({
...adminConfig,
allowedUnsafeExecutions: [],
});

isGradleExecutionAllowed('some_gradle-wrapper.command');

expect(logger.logger.once.warn).toHaveBeenCalledWith(
'Gradle wrapper command, `some_gradle-wrapper.command`, was requested to run, but `gradleWrapper` is not permitted in the allowedUnsafeExecutions',
);
});
});

describe('lockfile tests', () => {
it('aborts if no lockfile is found', async () => {
const execSnapshots = mockExecAll();
Expand Down Expand Up @@ -121,6 +171,32 @@
expect(execSnapshots).toBeEmptyArray();
});

it('aborts if allowedUnsafeExecutions does not include `gradleWrapper`', async () => {
GlobalConfig.set({
...adminConfig,
allowedUnsafeExecutions: [],
});

const execSnapshots = mockExecAll();

expect(
await updateArtifacts({
packageFileName: 'build.gradle',
updatedDeps: [
{ depName: 'org.junit.jupiter:junit-jupiter-api' },
{ depName: 'org.junit.jupiter:junit-jupiter-engine' },
],
newPackageFileContent: '',
config: {},
}),
).toBeNull();

expect(logger.logger.trace).toHaveBeenCalledWith(
'Not allowed to execute gradle due to allowedUnsafeExecutions - aborting update',
);
expect(execSnapshots).toBeEmptyArray();
});

it('updates lock file', async () => {
const execSnapshots = mockExecAll();

Expand Down Expand Up @@ -587,6 +663,40 @@
]);
});

it('aborts verification metadata updates if allowedUnsafeExecutions does not include `gradleWrapper`', async () => {
GlobalConfig.set({
...adminConfig,
allowedUnsafeExecutions: [],
});

const execSnapshots = mockExecAll();
scm.getFileList.mockResolvedValue([
'gradlew',
'build.gradle',
'gradle/wrapper/gradle-wrapper.properties',
'gradle/verification-metadata.xml',
]);
git.getRepoStatus.mockResolvedValue(
partial<StatusResult>({
modified: ['build.gradle', 'gradle/verification-metadata.xml'],
}),
);

expect(
await updateArtifacts({
packageFileName: 'build.gradle',
updatedDeps: [
{ depName: 'org.junit.jupiter:junit-jupiter-api' },
{ depName: 'org.junit.jupiter:junit-jupiter-engine' },
],
newPackageFileContent: '',
config: {},
}),
).toBeNull();

expect(execSnapshots).toBeEmptyArray();
});

it('updates existing checksums also if verify-checksums is disabled', async () => {
const execSnapshots = mockExecAll();
scm.getFileList.mockResolvedValue([
Expand Down
30 changes: 30 additions & 0 deletions lib/modules/manager/gradle/artifacts.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { isNonEmptyStringAndNotWhitespace } from '@sindresorhus/is';
import { quote } from 'shlex';
import upath from 'upath';
import { GlobalConfig } from '../../../config/global';
import { TEMPORARY_ERROR } from '../../../constants/error-messages';
import { logger } from '../../../logger';
import { exec } from '../../../util/exec';
Expand All @@ -23,6 +24,21 @@ import {
} from './extract/consistent-versions-plugin';
import { isGradleBuildFile } from './utils';

export function isGradleExecutionAllowed(command: string): boolean {
const allowlist = GlobalConfig.get('allowedUnsafeExecutions', [
'gradleWrapper',
]);

if (!allowlist.includes('gradleWrapper')) {
logger.once.warn(
`Gradle wrapper command, \`${command}\`, was requested to run, but \`gradleWrapper\` is not permitted in the allowedUnsafeExecutions`,
);
return false;
}

return true;
}

// .lockfile is gradle default lockfile, /versions.lock is gradle-consistent-versions plugin lockfile
function isLockFile(fileName: string): boolean {
return fileName.endsWith('.lockfile') || isGcvLockFile(fileName);
Expand Down Expand Up @@ -61,6 +77,13 @@ async function getSubProjectList(
execOptions: ExecOptions,
): Promise<string[]> {
const subprojects = ['']; // = root project
if (!isGradleExecutionAllowed(cmd)) {
logger.trace(
'Not allowed to execute gradle due to allowedUnsafeExecutions - aborting update',
);
return subprojects;
}

Comment on lines +80 to +86
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage checks aren't happy here - we don't currently test this directly (as it's not exported) and right now it's not called unless isGradleExecutionAllowed === true

But I didn't want to leave it "at risk" of not having it set

const subprojectsRegex = regEx(/^[ \t]*subprojects: \[(?<subprojects>.+)\]/m);

const gradleProperties = await exec(`${cmd} properties`, execOptions);
Expand Down Expand Up @@ -177,6 +200,13 @@ export async function updateArtifacts({
return null;
}

if (!isGradleExecutionAllowed(gradlewFile)) {
logger.trace(
'Not allowed to execute gradle due to allowedUnsafeExecutions - aborting update',
);
return null;
}

logger.debug('Updating found Gradle dependency lockfiles');

try {
Expand Down
5 changes: 5 additions & 0 deletions lib/modules/manager/gradle/readme.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
The `gradle` manager uses a custom parser written in JavaScript, similar to many others managers.
It does not call `gradle` directly in order to extract a list of dependencies.

### Executing the Gradle Wrapper

Renovate will only execute the Gradle Wrapper (via `./gradlew` or `gradlew.bat`) if the self-hosted administrator configures [`allowedUnsafeExecutions`](../../../self-hosted-configuration.md#allowedunsafeexecutions) to include the `gradleWrapper` option.
This is required due to [possibly supply chain security attack vectors](../../../security-and-permissions.md#trusting-repository-developers) that can occur with the Gradle Wrapper being executed.

### Updating lockfiles

The gradle manager supports gradle lock files in `.lockfile` artifacts, as well as lock files used by the [gradle-consistent-versions](https://github.com/palantir/gradle-consistent-versions) plugin.
Expand Down
Loading