Skip to content

Commit

Permalink
Merge commit 'a55a34798922fdbd6fd9dab3ac4c4fe08782f2ac' into feature/…
Browse files Browse the repository at this point in the history
…security-only-updates
  • Loading branch information
rhyskoedijk committed Oct 22, 2024
2 parents 0934cfe + a55a347 commit 21f480a
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 115 deletions.
58 changes: 42 additions & 16 deletions extension/tasks/dependabotV2/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { debug, error, setResult, TaskResult, warning, which } from 'azure-pipelines-task-lib/task';
import { AzureDevOpsWebApiClient } from './utils/azure-devops/AzureDevOpsWebApiClient';
import { setSecrets } from './utils/azure-devops/formattingCommands';
import { DependabotCli } from './utils/dependabot-cli/DependabotCli';
import { DependabotJobBuilder } from './utils/dependabot-cli/DependabotJobBuilder';
import {
Expand Down Expand Up @@ -28,6 +29,22 @@ async function run() {
throw new Error('Failed to parse task input configuration');
}

// Mask environment, organisation, and project specific variables from the logs.
// Most user's environments are private and they're less likely to share diagnostic info when it exposes information about their environment or organisation.
// Although not exhaustive, this will mask the most common information that could be used to identify the user's environment.
setSecrets(
taskInputs.hostname,
taskInputs.virtualDirectory,
taskInputs.organization,
taskInputs.project,
taskInputs.repository,
taskInputs.githubAccessToken,
taskInputs.systemAccessUser,
taskInputs.systemAccessToken,
taskInputs.autoApproveUserToken,
taskInputs.authorEmail,
);

// Parse dependabot.yaml configuration file
const dependabotConfig = await parseDependabotConfigFile(taskInputs);
if (!dependabotConfig) {
Expand Down Expand Up @@ -56,7 +73,8 @@ async function run() {
: null;

// Fetch the active pull requests created by the author user
const prAuthorActivePullRequests = await prAuthorClient.getActivePullRequestProperties(
const existingBranchNames = await prAuthorClient.getBranchNames(taskInputs.project, taskInputs.repository);
const existingPullRequests = await prAuthorClient.getActivePullRequestProperties(
taskInputs.project,
taskInputs.repository,
await prAuthorClient.getUserId(),
Expand All @@ -65,7 +83,13 @@ async function run() {
// Initialise the Dependabot updater
dependabot = new DependabotCli(
DependabotCli.CLI_IMAGE_LATEST, // TODO: Add config for this?
new DependabotOutputProcessor(taskInputs, prAuthorClient, prApproverClient, prAuthorActivePullRequests),
new DependabotOutputProcessor(
taskInputs,
prAuthorClient,
prApproverClient,
existingPullRequests,
existingBranchNames,
),
taskInputs.debug,
);

Expand Down Expand Up @@ -100,8 +124,13 @@ async function run() {

// Parse the Dependabot metadata for the existing pull requests that are related to this update
// Dependabot will use this to determine if we need to create new pull requests or update/close existing ones
const existingPullRequests = parsePullRequestProperties(prAuthorActivePullRequests, packageEcosystem);
const existingPullRequestDependencies = Object.entries(existingPullRequests).map(([id, deps]) => deps);
const existingPullRequestsForPackageEcosystem = parsePullRequestProperties(
existingPullRequests,
packageEcosystem,
);
const existingPullRequestDependenciesForPackageEcosystem = Object.entries(
existingPullRequestsForPackageEcosystem,
).map(([id, deps]) => deps);

// If this is a security-only update (i.e. 'open-pull-requests-limit: 0'), then we first need to discover the dependencies
// that need updating and check each one for security advisories. This is because Dependabot requires the list of vulnerable dependencies
Expand Down Expand Up @@ -132,9 +161,8 @@ async function run() {
}

// Run an update job for "all dependencies"; this will create new pull requests for dependencies that need updating
const dependenciesHaveVulnerabilities = (dependencyNamesToUpdate.length && securityAdvisories.length);
if (!securityUpdatesOnly || dependenciesHaveVulnerabilities)
{
const dependenciesHaveVulnerabilities = dependencyNamesToUpdate.length && securityAdvisories.length;
if (!securityUpdatesOnly || dependenciesHaveVulnerabilities) {
failedTasks += handleUpdateOperationResults(
await dependabot.update(
DependabotJobBuilder.updateAllDependenciesJob(
Expand All @@ -143,32 +171,30 @@ async function run() {
update,
dependabotConfig.registries,
dependencyNamesToUpdate,
existingPullRequestDependencies,
existingPullRequestDependenciesForPackageEcosystem,
securityAdvisories,
),
dependabotUpdaterOptions,
),
);
}
else
{
console.info('Nothing to update; dependencies are not affected by any known vulnerability')
} else {
console.info('Nothing to update; dependencies are not affected by any known vulnerability');
}

// If there are existing pull requests, run an update job for each one; this will resolve merge conflicts and close pull requests that are no longer needed
const numberOfPullRequestsToUpdate = Object.keys(existingPullRequests).length;
const numberOfPullRequestsToUpdate = Object.keys(existingPullRequestsForPackageEcosystem).length;
if (numberOfPullRequestsToUpdate > 0) {
if (!taskInputs.skipPullRequests) {
for (const pullRequestId in existingPullRequests) {
for (const pullRequestId in existingPullRequestsForPackageEcosystem) {
failedTasks += handleUpdateOperationResults(
await dependabot.update(
DependabotJobBuilder.updatePullRequestJob(
taskInputs,
pullRequestId,
update,
dependabotConfig.registries,
existingPullRequestDependencies,
existingPullRequests[pullRequestId],
existingPullRequestDependenciesForPackageEcosystem,
existingPullRequestsForPackageEcosystem[pullRequestId],
),
dependabotUpdaterOptions,
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,30 @@ export class AzureDevOpsWebApiClient {
}
}

/**
* Get the list of branch names for a repository.
* Requires scope "Code (Read)" (vso.code).
* @param project
* @param repository
* @returns
*/
public async getBranchNames(project: string, repository: string): Promise<string[]> {
try {
const refs = await this.restApiGet(
`${this.organisationApiUrl}/${project}/_apis/git/repositories/${repository}/refs`,
);
if (!refs) {
throw new Error(`Repository '${project}/${repository}' not found`);
}

return refs.value?.map((r) => normalizeBranchName(r.name)) || [];
} catch (e) {
error(`Failed to list branch names for '${project}/${repository}': ${e}`);
console.debug(e); // Dump the error stack trace to help with debugging
return undefined;
}
}

/**
* Get the properties for all active pull request created by the supplied user.
* Requires scope "Code (Read)" (vso.code).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Formats the logs into groups and sections to allow for easier navigation and readability.
* https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=bash#formatting-commands
*/

export function group(name: string) {
console.log(`##[group]${name}`);
}

export function endgroup() {
console.log(`##[endgroup]`);
}

export function section(name: string) {
console.log(`##[section]${name}`);
}

/**
* Masks the supplied values in the task log output.
* https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=bash#setsecret-register-a-value-as-a-secret
*/

import { setSecret } from 'azure-pipelines-task-lib';
export function setSecrets(...args: string[]) {
for (const arg of args.filter((a) => a && a?.toLowerCase() !== 'dependabot')) {
// Mask the value and the uri encoded value. This is required to ensure that API and package feed url don't expose the value.
// e.g. "Contoso Ltd" would appear as "Contoso%20Ltd" unless the uri encoded value was set as a secret.
setSecret(arg);
setSecret(encodeURIComponent(arg));
}
}
Loading

0 comments on commit 21f480a

Please sign in to comment.