Skip to content

Commit

Permalink
refactor(vulnerability-alerts): move to REST API (#27378)
Browse files Browse the repository at this point in the history
  • Loading branch information
RahulGautamSingh committed May 10, 2024
1 parent 28c5c8e commit 01faf42
Show file tree
Hide file tree
Showing 9 changed files with 426 additions and 521 deletions.
28 changes: 0 additions & 28 deletions lib/modules/platform/github/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,34 +68,6 @@ query(
}
`;

export const vulnerabilityAlertsQuery = (filterByState: boolean): string => `
query($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
vulnerabilityAlerts(last: 100, ${filterByState ? 'states: [OPEN]' : ''}) {
edges {
node {
dismissReason
vulnerableManifestFilename
vulnerableManifestPath
vulnerableRequirements
securityAdvisory {
description
identifiers { type value }
references { url }
severity
}
securityVulnerability {
package { name ecosystem }
firstPatchedVersion { identifier }
vulnerableVersionRange
}
}
}
}
}
}
`;

export const enableAutoMergeMutation = `
mutation EnablePullRequestAutoMerge(
$pullRequestId: ID!,
Expand Down
195 changes: 83 additions & 112 deletions lib/modules/platform/github/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3770,147 +3770,118 @@ describe('modules/platform/github/index', () => {
});

it('returns empty if error', async () => {
httpMock.scope(githubApiHost).post('/graphql').reply(200, {});
const scope = httpMock.scope(githubApiHost);
initRepoMock(scope, 'some/repo');
scope
.get(
'/repos/some/repo/dependabot/alerts?state=open&direction=asc&per_page=100',
)
.reply(200, {});
await github.initRepo({ repository: 'some/repo' });
const res = await github.getVulnerabilityAlerts();
expect(res).toHaveLength(0);
});

it('returns array if found', async () => {
httpMock
.scope(githubApiHost)
.post('/graphql')
.reply(200, {
data: {
repository: {
vulnerabilityAlerts: {
edges: [
{
node: {
securityAdvisory: { severity: 'HIGH', references: [] },
securityVulnerability: {
package: {
ecosystem: 'NPM',
name: 'left-pad',
range: '0.0.2',
},
vulnerableVersionRange: '0.0.2',
firstPatchedVersion: { identifier: '0.0.3' },
},
vulnerableManifestFilename: 'foo',
vulnerableManifestPath: 'bar',
},
},
],
},
const scope = httpMock.scope(githubApiHost);
initRepoMock(scope, 'some/repo');
scope
.get(
'/repos/some/repo/dependabot/alerts?state=open&direction=asc&per_page=100',
)
.reply(200, [
{
security_advisory: {
description: 'description',
identifiers: [{ type: 'type', value: 'value' }],
references: [],
},
},
});
const res = await github.getVulnerabilityAlerts();
expect(res).toHaveLength(1);
});

it('returns array if found on GHE', async () => {
const gheApiHost = 'https://ghe.renovatebot.com';

httpMock
.scope(gheApiHost)
.head('/')
.reply(200, '', { 'x-github-enterprise-version': '3.0.15' })
.get('/user')
.reply(200, { login: 'renovate-bot' })
.get('/user/emails')
.reply(200, {});

httpMock
.scope(gheApiHost)
.post('/graphql')
.reply(200, {
data: {
repository: {
vulnerabilityAlerts: {
edges: [
{
node: {
securityAdvisory: { severity: 'HIGH', references: [] },
securityVulnerability: {
package: {
ecosystem: 'NPM',
name: 'left-pad',
range: '0.0.2',
},
vulnerableVersionRange: '0.0.2',
firstPatchedVersion: { identifier: '0.0.3' },
},
vulnerableManifestFilename: 'foo',
vulnerableManifestPath: 'bar',
},
},
],
security_vulnerability: {
package: {
ecosystem: 'npm',
name: 'left-pad',
},
vulnerable_version_range: '0.0.2',
first_patched_version: { identifier: '0.0.3' },
},
dependency: {
manifest_path: 'bar/foo',
},
},
});

await github.initPlatform({
endpoint: gheApiHost,
token: '123test',
});

]);
await github.initRepo({ repository: 'some/repo' });
const res = await github.getVulnerabilityAlerts();
expect(res).toHaveLength(1);
});

it('returns empty if disabled', async () => {
// prettier-ignore
httpMock.scope(githubApiHost).post('/graphql').reply(200, {data: {repository: {}}});
const scope = httpMock.scope(githubApiHost);
initRepoMock(scope, 'some/repo');
scope
.get(
'/repos/some/repo/dependabot/alerts?state=open&direction=asc&per_page=100',
)
.reply(200, { data: { repository: {} } });
await github.initRepo({ repository: 'some/repo' });
const res = await github.getVulnerabilityAlerts();
expect(res).toHaveLength(0);
});

it('handles network error', async () => {
// prettier-ignore
httpMock.scope(githubApiHost).post('/graphql').replyWithError('unknown error');
const scope = httpMock.scope(githubApiHost);
initRepoMock(scope, 'some/repo');
scope
.get(
'/repos/some/repo/dependabot/alerts?state=open&direction=asc&per_page=100',
)
.replyWithError('unknown error');
await github.initRepo({ repository: 'some/repo' });
const res = await github.getVulnerabilityAlerts();
expect(res).toHaveLength(0);
});

it('calls logger.debug with only items that include securityVulnerability', async () => {
httpMock
.scope(githubApiHost)
.post('/graphql')
.reply(200, {
data: {
repository: {
vulnerabilityAlerts: {
edges: [
{
node: {
securityAdvisory: { severity: 'HIGH', references: [] },
securityVulnerability: {
package: {
ecosystem: 'NPM',
name: 'left-pad',
},
vulnerableVersionRange: '0.0.2',
firstPatchedVersion: { identifier: '0.0.3' },
},
vulnerableManifestFilename: 'foo',
vulnerableManifestPath: 'bar',
},
},
{
node: {
securityAdvisory: { severity: 'HIGH', references: [] },
securityVulnerability: null,
vulnerableManifestFilename: 'foo',
vulnerableManifestPath: 'bar',
},
},
],
const scope = httpMock.scope(githubApiHost);
initRepoMock(scope, 'some/repo');
scope
.get(
'/repos/some/repo/dependabot/alerts?state=open&direction=asc&per_page=100',
)
.reply(200, [
{
security_advisory: {
description: 'description',
identifiers: [{ type: 'type', value: 'value' }],
references: [],
},
security_vulnerability: {
package: {
ecosystem: 'npm',
name: 'left-pad',
},
vulnerable_version_range: '0.0.2',
first_patched_version: { identifier: '0.0.3' },
},
dependency: {
manifest_path: 'bar/foo',
},
},
});

{
security_advisory: {
description: 'description',
identifiers: [{ type: 'type', value: 'value' }],
references: [],
},
security_vulnerability: null,
dependency: {
manifest_path: 'bar/foo',
},
},
]);
await github.initRepo({ repository: 'some/repo' });
await github.getVulnerabilityAlerts();
expect(logger.logger.debug).toHaveBeenCalledWith(
{ alerts: { 'npm/left-pad': { '0.0.2': '0.0.3' } } },
Expand Down
83 changes: 37 additions & 46 deletions lib/modules/platform/github/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,11 @@ import {
enableAutoMergeMutation,
getIssuesQuery,
repoInfoQuery,
vulnerabilityAlertsQuery,
} from './graphql';
import { GithubIssueCache, GithubIssue as Issue } from './issue';
import { massageMarkdownLinks } from './massage-markdown-links';
import { getPrCache, updatePrCache } from './pr';
import { VulnerabilityAlertSchema } from './schema';
import type {
BranchProtection,
CombinedBranchStatus,
Expand Down Expand Up @@ -1959,27 +1959,19 @@ export async function getVulnerabilityAlerts(): Promise<VulnerabilityAlert[]> {
logger.debug('No vulnerability alerts enabled for repo');
return [];
}
let vulnerabilityAlerts: { node: VulnerabilityAlert }[] | undefined;

// TODO #22198
const gheSupportsStateFilter = semver.satisfies(
// semver not null safe, accepts null and undefined

platformConfig.gheVersion!,
'>=3.5',
);
const filterByState = !platformConfig.isGhe || gheSupportsStateFilter;
const query = vulnerabilityAlertsQuery(filterByState);

let vulnerabilityAlerts: VulnerabilityAlert[] | undefined;
try {
vulnerabilityAlerts = await githubApi.queryRepoField<{
node: VulnerabilityAlert;
}>(query, 'vulnerabilityAlerts', {
variables: { owner: config.repositoryOwner, name: config.repositoryName },
paginate: false,
acceptHeader: 'application/vnd.github.vixen-preview+json',
readOnly: true,
});
vulnerabilityAlerts = (
await githubApi.getJson(
`/repos/${config.repositoryOwner}/${config.repositoryName}/dependabot/alerts?state=open&direction=asc&per_page=100`,
{
paginate: false,
headers: { accept: 'application/vnd.github+json' },
cacheProvider: repoCacheProvider,
},
VulnerabilityAlertSchema,
)
).body;
} catch (err) {
logger.debug({ err }, 'Error retrieving vulnerability alerts');
logger.warn(
Expand All @@ -1989,42 +1981,41 @@ export async function getVulnerabilityAlerts(): Promise<VulnerabilityAlert[]> {
'Cannot access vulnerability alerts. Please ensure permissions have been granted.',
);
}
let alerts: VulnerabilityAlert[] = [];
try {
if (vulnerabilityAlerts?.length) {
alerts = vulnerabilityAlerts.map((edge) => edge.node);
const shortAlerts: AggregatedVulnerabilities = {};
if (alerts.length) {
logger.trace({ alerts }, 'GitHub vulnerability details');
for (const alert of alerts) {
if (alert.securityVulnerability === null) {
// As described in the documentation, there are cases in which
// GitHub API responds with `"securityVulnerability": null`.
// But it's may be faulty, so skip processing it here.
continue;
}
const {
package: { name, ecosystem },
vulnerableVersionRange,
firstPatchedVersion,
} = alert.securityVulnerability;
const patch = firstPatchedVersion?.identifier;

const key = `${ecosystem.toLowerCase()}/${name}`;
const range = vulnerableVersionRange;
const elem = shortAlerts[key] || {};
elem[range] = coerceToNull(patch);
shortAlerts[key] = elem;
logger.trace(
{ alerts: vulnerabilityAlerts },
'GitHub vulnerability details',
);
for (const alert of vulnerabilityAlerts) {
if (alert.security_vulnerability === null) {
// As described in the documentation, there are cases in which
// GitHub API responds with `"securityVulnerability": null`.
// But it's may be faulty, so skip processing it here.
continue;
}
logger.debug({ alerts: shortAlerts }, 'GitHub vulnerability details');
const {
package: { name, ecosystem },
vulnerable_version_range: vulnerableVersionRange,
first_patched_version: firstPatchedVersion,
} = alert.security_vulnerability;
const patch = firstPatchedVersion?.identifier;

const key = `${ecosystem.toLowerCase()}/${name}`;
const range = vulnerableVersionRange;
const elem = shortAlerts[key] || {};
elem[range] = coerceToNull(patch);
shortAlerts[key] = elem;
}
logger.debug({ alerts: shortAlerts }, 'GitHub vulnerability details');
} else {
logger.debug('No vulnerability alerts found');
}
} catch (err) /* istanbul ignore next */ {
logger.error({ err }, 'Error processing vulnerabity alerts');
}
return alerts;
return vulnerabilityAlerts ?? [];
}

async function pushFiles(
Expand Down

0 comments on commit 01faf42

Please sign in to comment.