CVE-2022-29220 (published 2022-05-31T16:15Z)

github-action-merge-dependabot is an action that automatically approves and merges dependabot pull requests (PRs). Prior to version 3.2.0, github-action-merge-dependabot does not check if a commit created by dependabot is verified with the proper GPG key. There is just a check if the actor is set to `dependabot[bot]` to determine if the PR is a legit PR. Theoretically, an owner of a seemingly valid and legit action in the pipeline can check if the PR is created by dependabot and if their own action has enough permissions to modify the PR in the pipeline. If so, they can modify the PR by adding a second seemingly valid and legit commit to the PR, as they can set arbitrarily the username and email in for commits in git. Because the bot only checks if the actor is valid, it would pass the malicious changes through and merge the PR automatically, without getting noticed by project maintainers. It would probably not be possible to determine where the malicious commit came from, as it would only say `dependabot[bot]` and the corresponding email-address. Version 3.2.0 contains a patch for this issue.

Severity: MEDIUM
CVSS v2 base score: 4.0 (AV:N/AC:L/Au:S/C:N/I:P/A:N)
CVSS v3 base score: 6.5, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N)

Weakness information:
cwe_id: CWE-345
cwe_name: Insufficient Verification of Data Authenticity
description: The product does not sufficiently verify the origin or authenticity of data, in a way that causes it to accept invalid data.
extended_description: 
            
         
         

Fix information:
repo_url: https://github.com/fastify/github-action-merge-dependabot
commit_details: repo_url: https://github.com/fastify/github-action-merge-dependabot
author: Ian Lovell
author_date: 2022-05-26 11:13:06+01:00
msg: Merge pull request from GHSA-v5vr-h3xq-8v6w

* fix: add pr commit author check

* chore: update prettier settings

* chore: update prettier settings

* feat: add verify commit signatures

* chore: revert prettier

* test: add action test

* refactor: remove commits fallback

* test: add verify test

* feat: add spawn gpg

* Fixing PR comments: Moved import from func to global; Iterate with hwp; Added GPG information to ReadMe

* Removing Key from files

* Fixing node_modules

* different approach to validating the commit. Checking that the commit is verified, the author is dependabot[bot], the committer is GitHub

* Adding fixes to tests

* Fix for the tests

* eslint, naming and removal of magic text

* chore: formatting

* Refactored tests

* Removed unecessary async function. Iterating through very small amounts of commits.Renamed file for consistency

* removed unecessary dependency

Co-authored-by: marceloFerreira90 <marcelo.ferreira@nearform.com>
Co-authored-by: marceloFerreira90 <marcyff2@gmail.com>
Co-authored-by: Simone Busoli <simone.busoli@gmail.com>
repository: repo_url: https://github.com/fastify/github-action-merge-dependabot
repo_name: fastify/github-action-merge-dependabot
description: This action automatically approves and merges dependabot PRs.
repo_language: JavaScript
owner: fastify
file_changes: filename: 105.index.js
change_type: ModificationType.ADD
diff: @@ -0,0 +1,315 @@
+"use strict";
+exports.id = 105;
+exports.ids = [105];
+exports.modules = {
+
+/***/ 8770:
+/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
+
+
+const fs = __webpack_require__(7147);
+const os = __webpack_require__(2037);
+
+const tempDirectorySymbol = Symbol.for('__RESOLVED_TEMP_DIRECTORY__');
+
+if (!global[tempDirectorySymbol]) {
+	Object.defineProperty(global, tempDirectorySymbol, {
+		value: fs.realpathSync(os.tmpdir())
+	});
+}
+
+module.exports = global[tempDirectorySymbol];
+
+
+/***/ }),
+
+/***/ 4105:
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+// ESM COMPAT FLAG
+__webpack_require__.r(__webpack_exports__);
+
+// EXPORTS
+__webpack_require__.d(__webpack_exports__, {
+  "rootTemporaryDirectory": () => (/* reexport */ temp_dir),
+  "temporaryDirectory": () => (/* binding */ temporaryDirectory),
+  "temporaryDirectoryTask": () => (/* binding */ temporaryDirectoryTask),
+  "temporaryFile": () => (/* binding */ temporaryFile),
+  "temporaryFileTask": () => (/* binding */ temporaryFileTask),
+  "temporaryWrite": () => (/* binding */ temporaryWrite),
+  "temporaryWriteSync": () => (/* binding */ temporaryWriteSync),
+  "temporaryWriteTask": () => (/* binding */ temporaryWriteTask)
+});
+
+// EXTERNAL MODULE: external "node:fs"
+var external_node_fs_ = __webpack_require__(7561);
+// EXTERNAL MODULE: external "node:fs/promises"
+var promises_ = __webpack_require__(3977);
+// EXTERNAL MODULE: external "node:path"
+var external_node_path_ = __webpack_require__(9411);
+// EXTERNAL MODULE: external "node:stream"
+var external_node_stream_ = __webpack_require__(4492);
+// EXTERNAL MODULE: external "node:util"
+var external_node_util_ = __webpack_require__(7261);
+// EXTERNAL MODULE: external "util"
+var external_util_ = __webpack_require__(3837);
+// EXTERNAL MODULE: external "crypto"
+var external_crypto_ = __webpack_require__(6113);
+;// CONCATENATED MODULE: ./node_modules/crypto-random-string/index.js
+
+
+
+const randomBytesAsync = (0,external_util_.promisify)(external_crypto_.randomBytes);
+
+const urlSafeCharacters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~'.split('');
+const numericCharacters = '0123456789'.split('');
+const distinguishableCharacters = 'CDEHKMPRTUWXY012458'.split('');
+const asciiPrintableCharacters = '!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'.split('');
+const alphanumericCharacters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'.split('');
+
+const generateForCustomCharacters = (length, characters) => {
+	// Generating entropy is faster than complex math operations, so we use the simplest way
+	const characterCount = characters.length;
+	const maxValidSelector = (Math.floor(0x10000 / characterCount) * characterCount) - 1; // Using values above this will ruin distribution when using modular division
+	const entropyLength = 2 * Math.ceil(1.1 * length); // Generating a bit more than required so chances we need more than one pass will be really low
+	let string = '';
+	let stringLength = 0;
+
+	while (stringLength < length) { // In case we had many bad values, which may happen for character sets of size above 0x8000 but close to it
+		const entropy = external_crypto_.randomBytes(entropyLength);
+		let entropyPosition = 0;
+
+		while (entropyPosition < entropyLength && stringLength < length) {
+			const entropyValue = entropy.readUInt16LE(entropyPosition);
+			entropyPosition += 2;
+			if (entropyValue > maxValidSelector) { // Skip values which will ruin distribution when using modular division
+				continue;
+			}
+
+			string += characters[entropyValue % characterCount];
+			stringLength++;
+		}
+	}
+
+	return string;
+};
+
+const generateForCustomCharactersAsync = async (length, characters) => {
+	// Generating entropy is faster than complex math operations, so we use the simplest way
+	const characterCount = characters.length;
+	const maxValidSelector = (Math.floor(0x10000 / characterCount) * characterCount) - 1; // Using values above this will ruin distribution when using modular division
+	const entropyLength = 2 * Math.ceil(1.1 * length); // Generating a bit more than required so chances we need more than one pass will be really low
+	let string = '';
+	let stringLength = 0;
+
+	while (stringLength < length) { // In case we had many bad values, which may happen for character sets of size above 0x8000 but close to it
+		const entropy = await randomBytesAsync(entropyLength); // eslint-disable-line no-await-in-loop
+		let entropyPosition = 0;
+
+		while (entropyPosition < entropyLength && stringLength < length) {
+			const entropyValue = entropy.readUInt16LE(entropyPosition);
+			entropyPosition += 2;
+			if (entropyValue > maxValidSelector) { // Skip values which will ruin distribution when using modular division
+				continue;
+			}
+
+			string += characters[entropyValue % characterCount];
+			stringLength++;
+		}
+	}
+
+	return string;
+};
+
+const generateRandomBytes = (byteLength, type, length) => external_crypto_.randomBytes(byteLength).toString(type).slice(0, length);
+
+const generateRandomBytesAsync = async (byteLength, type, length) => {
+	const buffer = await randomBytesAsync(byteLength);
+	return buffer.toString(type).slice(0, length);
+};
+
+const allowedTypes = new Set([
+	undefined,
+	'hex',
+	'base64',
+	'url-safe',
+	'numeric',
+	'distinguishable',
+	'ascii-printable',
+	'alphanumeric'
+]);
+
+const createGenerator = (generateForCustomCharacters, generateRandomBytes) => ({length, type, characters}) => {
+	if (!(length >= 0 && Number.isFinite(length))) {
+		throw new TypeError('Expected a `length` to be a non-negative finite number');
+	}
+
+	if (type !== undefined && characters !== undefined) {
+		throw new TypeError('Expected either `type` or `characters`');
+	}
+
+	if (characters !== undefined && typeof characters !== 'string') {
+		throw new TypeError('Expected `characters` to be string');
+	}
+
+	if (!allowedTypes.has(type)) {
+		throw new TypeError(`Unknown type: ${type}`);
+	}
+
+	if (type === undefined && characters === undefined) {
+		type = 'hex';
+	}
+
+	if (type === 'hex' || (type === undefined && characters === undefined)) {
+		return generateRandomBytes(Math.ceil(length * 0.5), 'hex', length); // Need 0.5 byte entropy per character
+	}
+
+	if (type === 'base64') {
+		return generateRandomBytes(Math.ceil(length * 0.75), 'base64', length); // Need 0.75 byte of entropy per character
+	}
+
+	if (type === 'url-safe') {
+		return generateForCustomCharacters(length, urlSafeCharacters);
+	}
+
+	if (type === 'numeric') {
+		return generateForCustomCharacters(length, numericCharacters);
+	}
+
+	if (type === 'distinguishable') {
+		return generateForCustomCharacters(length, distinguishableCharacters);
+	}
+
+	if (type === 'ascii-printable') {
+		return generateForCustomCharacters(length, asciiPrintableCharacters);
+	}
+
+	if (type === 'alphanumeric') {
+		return generateForCustomCharacters(length, alphanumericCharacters);
+	}
+
+	if (characters.length === 0) {
+		throw new TypeError('Expected `characters` string length to be greater than or equal to 1');
+	}
+
+	if (characters.length > 0x10000) {
+		throw new TypeError('Expected `characters` string length to be less or equal to 65536');
+	}
+
+	return generateForCustomCharacters(length, characters.split(''));
+};
+
+const cryptoRandomString = createGenerator(generateForCustomCharacters, generateRandomBytes);
+
+cryptoRandomString.async = createGenerator(generateForCustomCharactersAsync, generateRandomBytesAsync);
+
+/* harmony default export */ const crypto_random_string = (cryptoRandomString);
+
+;// CONCATENATED MODULE: ./node_modules/unique-string/index.js
+
+
+function uniqueString() {
+	return crypto_random_string({length: 32});
+}
+
+// EXTERNAL MODULE: ./node_modules/temp-dir/index.js
+var temp_dir = __webpack_require__(8770);
+;// CONCATENATED MODULE: ./node_modules/tempy/node_modules/is-stream/index.js
+function isStream(stream) {
+	return stream !== null
+		&& typeof stream === 'object'
+		&& typeof stream.pipe === 'function';
+}
+
+function isWritableStream(stream) {
+	return isStream(stream)
+		&& stream.writable !== false
+		&& typeof stream._write === 'function'
+		&& typeof stream._writableState === 'object';
+}
+
+function isReadableStream(stream) {
+	return isStream(stream)
+		&& stream.readable !== false
+		&& typeof stream._read === 'function'
+		&& typeof stream._readableState === 'object';
+}
+
+function isDuplexStream(stream) {
+	return isWritableStream(stream)
+		&& isReadableStream(stream);
+}
+
+function isTransformStream(stream) {
+	return isDuplexStream(stream)
+		&& typeof stream._transform === 'function';
+}
+
+;// CONCATENATED MODULE: ./node_modules/tempy/index.js
+
+
+
+
+
+
+
+
+
+const pipeline = (0,external_node_util_.promisify)(external_node_stream_.pipeline); // TODO: Use `node:stream/promises` when targeting Node.js 16.
+
+const getPath = (prefix = '') => external_node_path_.join(temp_dir, prefix + uniqueString());
+
+const writeStream = async (filePath, data) => pipeline(data, external_node_fs_.createWriteStream(filePath));
+
+async function runTask(temporaryPath, callback) {
+	try {
+		return await callback(temporaryPath);
+	} finally {
+		await promises_.rm(temporaryPath, {recursive: true, force: true});
+	}
+}
+
+function temporaryFile({name, extension} = {}) {
+	if (name) {
+		if (extension !== undefined && extension !== null) {
+			throw new Error('The `name` and `extension` options are mutually exclusive');
+		}
+
+		return external_node_path_.join(temporaryDirectory(), name);
+	}
+
+	return getPath() + (extension === undefined || extension === null ? '' : '.' + extension.replace(/^\./, ''));
+}
+
+const temporaryFileTask = async (callback, options) => runTask(temporaryFile(options), callback);
+
+function temporaryDirectory({prefix = ''} = {}) {
+	const directory = getPath(prefix);
+	external_node_fs_.mkdirSync(directory);
+	return directory;
+}
+
+const temporaryDirectoryTask = async (callback, options) => runTask(temporaryDirectory(options), callback);
+
+async function temporaryWrite(fileContent, options) {
+	const filename = temporaryFile(options);
+	const write = isStream(fileContent) ? writeStream : promises_.writeFile;
+	await write(filename, fileContent);
+	return filename;
+}
+
+const temporaryWriteTask = async (fileContent, callback, options) => runTask(await temporaryWrite(fileContent, options), callback);
+
+function temporaryWriteSync(fileContent, options) {
+	const filename = temporaryFile(options);
+	external_node_fs_.writeFileSync(filename, fileContent);
+	return filename;
+}
+
+
+
+
+/***/ })
+
+};
+;
\ No newline at end of file

programming_language: JavaScript
filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -9701,6 +9701,8 @@ const {
   getModuleVersionChanges,
   checkModuleVersionChanges,
 } = __nccwpck_require__(9488)
+const { verifyCommits } = __nccwpck_require__(3094)
+const { dependabotAuthor } = __nccwpck_require__(500)
 
 const {
   GITHUB_TOKEN,
@@ -9728,11 +9730,25 @@ module.exports = async function run() {
 
     const pr = pull_request || (await client.getPullRequest(PR_NUMBER))
 
-    const isDependabotPR = pr.user.login === 'dependabot[bot]'
+    const isDependabotPR = pr.user.login === dependabotAuthor
     if (!isDependabotPR) {
       return logWarning('Not a dependabot PR, skipping.')
     }
 
+    const commits = await client.getPullRequestCommits(pr.number)
+
+    if (!commits.every(commit => commit.author.login === dependabotAuthor)) {
+      return logWarning('PR contains non dependabot commits, skipping.')
+    }
+
+    try {
+      await verifyCommits(commits)
+    } catch {
+      return logWarning(
+        'PR contains invalid dependabot commit signatures, skipping.'
+      )
+    }
+
     const prDiff = await client.getPullRequestDiff(pr.number)
 
     // Get changed modules from diff if available or from PR title as fallback
@@ -9814,6 +9830,23 @@ function parsePrTitle(pullRequest) {
 }
 
 
+/***/ }),
+
+/***/ 500:
+/***/ ((module) => {
+
+"use strict";
+
+const dependabotAuthor = 'dependabot[bot]'
+
+const dependabotCommitter = 'GitHub'
+
+module.exports = {
+  dependabotAuthor,
+  dependabotCommitter,
+}
+
+
 /***/ }),
 
 /***/ 5013:
@@ -9913,6 +9946,16 @@ function githubClient(githubToken) {
       })
       return pullRequest
     },
+
+    async getPullRequestCommits(pullRequestNumber) {
+      const { data } = await octokit.rest.pulls.listCommits({
+        owner,
+        repo: repoName,
+        pull_number: pullRequestNumber,
+      })
+
+      return data
+    },
   }
 }
 
@@ -10125,6 +10168,56 @@ exports.isValidSemver = function (version) {
 }
 
 
+/***/ }),
+
+/***/ 3094:
+/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
+
+"use strict";
+
+
+const {
+  dependabotAuthor,
+  dependabotCommitter,
+} = __nccwpck_require__(500)
+
+function verifyCommits(commits) {
+  commits.forEach(function (commit) {
+    const {
+      commit: {
+        verification: { verified },
+        committer,
+        author,
+      },
+      sha,
+    } = commit
+    verifyCommitSignatureCommitterAndAuthor(sha, author, committer, verified)
+  })
+}
+
+function verifyCommitSignatureCommitterAndAuthor(
+  sha,
+  author,
+  committer,
+  verified
+) {
+  if (
+    !verified ||
+    committer.name !== dependabotCommitter ||
+    author.name !== dependabotAuthor
+  ) {
+    throw new Error(
+      `Signature for commit ${sha} could not be verified - Not a dependabot commit`
+    )
+  }
+}
+
+module.exports = {
+  verifyCommits,
+  verifyCommitSignatureCommitterAndAuthor,
+}
+
+
 /***/ }),
 
 /***/ 2877:

programming_language: JavaScript
filename: action.js
change_type: ModificationType.MODIFY
diff: @@ -20,6 +20,8 @@ const {
   getModuleVersionChanges,
   checkModuleVersionChanges,
 } = require('./moduleVersionChanges')
+const { verifyCommits } = require('./verifyCommitSignatures')
+const { dependabotAuthor } = require('./getDependabotDetails')
 
 const {
   GITHUB_TOKEN,
@@ -47,11 +49,25 @@ module.exports = async function run() {
 
     const pr = pull_request || (await client.getPullRequest(PR_NUMBER))
 
-    const isDependabotPR = pr.user.login === 'dependabot[bot]'
+    const isDependabotPR = pr.user.login === dependabotAuthor
     if (!isDependabotPR) {
       return logWarning('Not a dependabot PR, skipping.')
     }
 
+    const commits = await client.getPullRequestCommits(pr.number)
+
+    if (!commits.every(commit => commit.author.login === dependabotAuthor)) {
+      return logWarning('PR contains non dependabot commits, skipping.')
+    }
+
+    try {
+      await verifyCommits(commits)
+    } catch {
+      return logWarning(
+        'PR contains invalid dependabot commit signatures, skipping.'
+      )
+    }
+
     const prDiff = await client.getPullRequestDiff(pr.number)
 
     // Get changed modules from diff if available or from PR title as fallback

programming_language: JavaScript
filename: getDependabotDetails.js
change_type: ModificationType.ADD
diff: @@ -0,0 +1,9 @@
+'use strict'
+const dependabotAuthor = 'dependabot[bot]'
+
+const dependabotCommitter = 'GitHub'
+
+module.exports = {
+  dependabotAuthor,
+  dependabotCommitter,
+}

programming_language: CoffeeScript
filename: github-client.js
change_type: ModificationType.MODIFY
diff: @@ -54,6 +54,16 @@ function githubClient(githubToken) {
       })
       return pullRequest
     },
+
+    async getPullRequestCommits(pullRequestNumber) {
+      const { data } = await octokit.rest.pulls.listCommits({
+        owner,
+        repo: repoName,
+        pull_number: pullRequestNumber,
+      })
+
+      return data
+    },
   }
 }
 

programming_language: TypeScript
filename: verifyCommitSignatures.js
change_type: ModificationType.ADD
diff: @@ -0,0 +1,42 @@
+'use strict'
+
+const {
+  dependabotAuthor,
+  dependabotCommitter,
+} = require('./getDependabotDetails')
+
+function verifyCommits(commits) {
+  commits.forEach(function (commit) {
+    const {
+      commit: {
+        verification: { verified },
+        committer,
+        author,
+      },
+      sha,
+    } = commit
+    verifyCommitSignatureCommitterAndAuthor(sha, author, committer, verified)
+  })
+}
+
+function verifyCommitSignatureCommitterAndAuthor(
+  sha,
+  author,
+  committer,
+  verified
+) {
+  if (
+    !verified ||
+    committer.name !== dependabotCommitter ||
+    author.name !== dependabotAuthor
+  ) {
+    throw new Error(
+      `Signature for commit ${sha} could not be verified - Not a dependabot commit`
+    )
+  }
+}
+
+module.exports = {
+  verifyCommits,
+  verifyCommitSignatureCommitterAndAuthor,
+}

programming_language: JavaScript
filename: action.test.js
change_type: ModificationType.MODIFY
diff: @@ -13,6 +13,7 @@ const { diffs } = require('./moduleChanges')
 const actionLog = require('../src/log')
 const actionUtil = require('../src/util')
 const actionGithubClient = require('../src/github-client')
+const verifyCommits = require('../src/verifyCommitSignatures')
 
 const GITHUB_TOKEN = 'the-token'
 const BOT_NAME = 'dependabot[bot]'
@@ -34,15 +35,22 @@ function buildStubbedAction({ payload, inputs }) {
 
   const prStub = sinon.stub()
   const prDiffStub = sinon.stub()
+  const prCommitsStub = sinon.stub()
   const approveStub = sinon.stub()
   const mergeStub = sinon.stub()
+
   const clientStub = sinon.stub(actionGithubClient, 'githubClient').returns({
     getPullRequest: prStub.resolves(),
     approvePullRequest: approveStub.resolves(),
     mergePullRequest: mergeStub.resolves(),
     getPullRequestDiff: prDiffStub.resolves(),
+    getPullRequestCommits: prCommitsStub.resolves([]),
   })
 
+  const verifyCommitsStub = sinon
+    .stub(verifyCommits, 'verifyCommits')
+    .returns(Promise.resolve())
+
   const action = proxyquire('../src/action', {
     '@actions/core': coreStub,
     'actions-toolkit': toolkitStub,
@@ -63,6 +71,8 @@ function buildStubbedAction({ payload, inputs }) {
       approveStub,
       mergeStub,
       prDiffStub,
+      prCommitsStub,
+      verifyCommitsStub,
     },
   }
 }
@@ -124,6 +134,76 @@ tap.test('should skip non-dependabot PR', async () => {
   sinon.assert.notCalled(stubs.mergeStub)
 })
 
+tap.test('should skip PR with non dependabot commit', async () => {
+  const PR_NUMBER = Math.random()
+  const { action, stubs } = buildStubbedAction({
+    payload: {
+      pull_request: {
+        user: {
+          login: BOT_NAME,
+        },
+        number: PR_NUMBER,
+      },
+    },
+    inputs: { PR_NUMBER },
+  })
+
+  stubs.prCommitsStub.resolves([
+    {
+      author: {
+        login: 'not dependabot',
+      },
+    },
+  ])
+
+  await action()
+
+  sinon.assert.calledOnce(stubs.prCommitsStub)
+  sinon.assert.calledWithExactly(
+    stubs.logStub.logWarning,
+    'PR contains non dependabot commits, skipping.'
+  )
+  sinon.assert.notCalled(stubs.approveStub)
+  sinon.assert.notCalled(stubs.mergeStub)
+})
+
+tap.test(
+  'should skip PR if dependabot commit signatures cannot be verified',
+  async () => {
+    const PR_NUMBER = Math.random()
+    const { action, stubs } = buildStubbedAction({
+      payload: {
+        pull_request: {
+          user: {
+            login: BOT_NAME,
+          },
+          number: PR_NUMBER,
+        },
+      },
+      inputs: { PR_NUMBER },
+    })
+
+    stubs.prCommitsStub.resolves([
+      {
+        author: {
+          login: 'dependabot[bot]',
+        },
+      },
+    ])
+
+    stubs.verifyCommitsStub.rejects()
+
+    await action()
+
+    sinon.assert.calledWithExactly(
+      stubs.logStub.logWarning,
+      'PR contains invalid dependabot commit signatures, skipping.'
+    )
+    sinon.assert.notCalled(stubs.approveStub)
+    sinon.assert.notCalled(stubs.mergeStub)
+  }
+)
+
 tap.test('should process dependabot PR and skip PR not in target', async () => {
   const PR_NUMBER = Math.random()
   const { action, stubs } = buildStubbedAction({

programming_language: JavaScript
filename: github-client.test.js
change_type: ModificationType.MODIFY
diff: @@ -18,6 +18,7 @@ const octokitStubs = {
   get: sinon.stub().returns(Promise.resolve({ data })),
   createReview: sinon.stub().returns(Promise.resolve({ data })),
   merge: sinon.stub().returns(Promise.resolve({ data })),
+  listCommits: sinon.stub().returns(Promise.resolve({ data })),
 }
 
 const { githubClient } = tap.mock('../src/github-client', {
@@ -95,4 +96,15 @@ tap.test('githubClient', async t => {
       },
     })
   })
+
+  t.test('getPullRequestCommits', async () => {
+    const result = await githubClient(TOKEN).getPullRequestCommits(PR_NUMBER)
+    tap.equal(result, data)
+
+    sinon.assert.calledWith(octokitStubs.listCommits, {
+      owner: githubContext.repository.owner.login,
+      repo: githubContext.repository.name,
+      pull_number: PR_NUMBER,
+    })
+  })
 })

programming_language: JavaScript
filename: verifyCommitSignatures.test.js
change_type: ModificationType.ADD
diff: @@ -0,0 +1,218 @@
+'use strict'
+
+const tap = require('tap')
+
+const {
+  dependabotAuthor,
+  dependabotCommitter,
+} = require('../src/getDependabotDetails')
+const {
+  verifyCommits,
+  verifyCommitSignatureCommitterAndAuthor,
+} = require('../src/verifyCommitSignatures')
+
+const ValidAuthorValidCommitterVerifiedCommitMock = {
+  commit: {
+    author: { name: dependabotAuthor },
+    committer: { name: dependabotCommitter },
+    verification: { verified: true },
+  },
+  sha: 'sha',
+}
+const ValidAuthorValidCommitterUnverifiedCommitMock = {
+  commit: {
+    author: { name: dependabotAuthor },
+    committer: { name: dependabotCommitter },
+    verification: { verified: false },
+  },
+  sha: 'sha',
+}
+const InvalidAuthorValidCommitterVerifiedCommitMock = {
+  commit: {
+    author: { name: 'testUser' },
+    committer: { name: dependabotCommitter },
+    verification: { verified: true },
+  },
+  sha: 'sha',
+}
+const ValidAuthorInvalidCommitterVerifiedCommitMock = {
+  commit: {
+    author: { name: dependabotAuthor },
+    committer: { name: 'testUser' },
+    verification: { verified: true },
+  },
+  sha: 'sha',
+}
+const InvalidAuthorInvalidCommitterUnverifiedCommitMock = {
+  commit: {
+    author: { name: 'testUser' },
+    committer: { name: 'testUser' },
+    verification: { verified: false },
+  },
+  sha: 'sha',
+}
+
+const prCommitsValid = [
+  ValidAuthorValidCommitterVerifiedCommitMock,
+  ValidAuthorValidCommitterVerifiedCommitMock,
+]
+const prCommitsInvalid = [
+  InvalidAuthorValidCommitterVerifiedCommitMock,
+  InvalidAuthorValidCommitterVerifiedCommitMock,
+]
+const prCommitsValidAndInvalid = [
+  ValidAuthorValidCommitterVerifiedCommitMock,
+  InvalidAuthorValidCommitterVerifiedCommitMock,
+]
+
+tap.test('verifyCommitSignatureCommitterAndAuthor', async t => {
+  t.test('shoud throw error when', async t => {
+    t.test('the verified flag is set to false', async t => {
+      const {
+        commit: {
+          author,
+          committer,
+          verification: { verified },
+        },
+        sha,
+      } = ValidAuthorValidCommitterUnverifiedCommitMock
+      t.throws(
+        () =>
+          verifyCommitSignatureCommitterAndAuthor(
+            sha,
+            author,
+            committer,
+            verified
+          ),
+        new Error(
+          `Signature for commit ${sha} could not be verified - Not a dependabot commit`
+        )
+      )
+    })
+
+    t.test('the committer name is not GitHub', async t => {
+      const {
+        commit: {
+          author,
+          committer,
+          verification: { verified },
+        },
+        sha,
+      } = ValidAuthorInvalidCommitterVerifiedCommitMock
+      t.throws(
+        () =>
+          verifyCommitSignatureCommitterAndAuthor(
+            sha,
+            author,
+            committer,
+            verified
+          ),
+        new Error(
+          `Signature for commit ${sha} could not be verified - Not a dependabot commit`
+        )
+      )
+    })
+
+    t.test('the authot name is not dependabot[bot]', async t => {
+      const {
+        commit: {
+          author,
+          committer,
+          verification: { verified },
+        },
+        sha,
+      } = InvalidAuthorValidCommitterVerifiedCommitMock
+      t.throws(
+        () =>
+          verifyCommitSignatureCommitterAndAuthor(
+            sha,
+            author,
+            committer,
+            verified
+          ),
+        new Error(
+          `Signature for commit ${sha} could not be verified - Not a dependabot commit`
+        )
+      )
+    })
+
+    t.test(
+      'the committer name is not Github, the author is not dependabot[bot] and is not verified',
+      async t => {
+        const {
+          commit: {
+            author,
+            committer,
+            verification: { verified },
+          },
+          sha,
+        } = InvalidAuthorInvalidCommitterUnverifiedCommitMock
+        t.throws(
+          () =>
+            verifyCommitSignatureCommitterAndAuthor(
+              sha,
+              author,
+              committer,
+              verified
+            ),
+          new Error(
+            `Signature for commit ${sha} could not be verified - Not a dependabot commit`
+          )
+        )
+      }
+    )
+  })
+
+  t.test('should not throw an error when', async t => {
+    t.test(
+      'the committer name is Github, the author is dependabot[bot] and is verified',
+      async t => {
+        const {
+          commit: {
+            author,
+            committer,
+            verification: { verified },
+          },
+          sha,
+        } = ValidAuthorValidCommitterVerifiedCommitMock
+        t.doesNotThrow(
+          () =>
+            verifyCommitSignatureCommitterAndAuthor(
+              sha,
+              author,
+              committer,
+              verified
+            ),
+          {}
+        )
+      }
+    )
+  })
+})
+
+tap.test('VerifyCommits', async t => {
+  t.test('Should throw error when', async t => {
+    t.test('At least one commit does not match the requirements', async t => {
+      t.throws(
+        () => verifyCommits(prCommitsValidAndInvalid),
+        new Error(
+          `Signature for commit sha could not be verified - Not a dependabot commit`
+        )
+      )
+    })
+
+    t.test('At least one commit does not match the requirements', async t => {
+      t.throws(
+        () => verifyCommits(prCommitsInvalid),
+        new Error(
+          `Signature for commit sha could not be verified - Not a dependabot commit`
+        )
+      )
+    })
+  })
+  t.test('Should not throw error when', async t => {
+    t.test('All commits match the requirements', async t => {
+      t.doesNotThrow(() => verifyCommits(prCommitsValid), {})
+    })
+  })
+})

programming_language: TypeScript