CVE-2022-29220: Insufficient Verification of Data Authenticity (CWE-345)
value: 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.
CWE description: The product does not sufficiently verify the origin or authenticity of data, in a way that causes it to accept invalid data.
CVSS v3 base score: 6.5
Published: 2022-05-31T16:15Z
Repository: https://github.com/fastify/github-action-merge-dependabot
Language: JavaScript
Files changed: dist/105.index.js, dist/index.js, src/action.js, src/getDependabotDetails.js, src/github-client.js, src/verifyCommitSignatures.js, test/action.test.js, test/github-client.test.js, test/verifyCommitSignatures.test.js
Commit date: 2022-05-26 11:13:06 +0100
Commit message: 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>
Diff: diff --git a/dist/105.index.js b/dist/105.index.js
new file mode 100644
index 0000000..040f5d0
--- /dev/null
+++ b/dist/105.index.js
@@ -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
diff --git a/dist/index.js b/dist/index.js
index 000d525..650b9ee 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -9699,10 +9699,12 @@ const {
 const { targetOptions } = __nccwpck_require__(5013)
 const {
   getModuleVersionChanges,
   checkModuleVersionChanges,
 } = __nccwpck_require__(9488)
+const { verifyCommits } = __nccwpck_require__(3094)
+const { dependabotAuthor } = __nccwpck_require__(500)
 
 const {
   GITHUB_TOKEN,
   MERGE_METHOD,
   EXCLUDE_PKGS,
@@ -9726,15 +9728,29 @@ module.exports = async function run() {
 
     const client = githubClient(GITHUB_TOKEN)
 
     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
     const moduleChanges = getModuleVersionChanges(prDiff) || parsePrTitle(pr)
 
@@ -9812,10 +9828,27 @@ function parsePrTitle(pullRequest) {
     },
   }
 }
 
 
+/***/ }),
+
+/***/ 500:
+/***/ ((module) => {
+
+"use strict";
+
+const dependabotAuthor = 'dependabot[bot]'
+
+const dependabotCommitter = 'GitHub'
+
+module.exports = {
+  dependabotAuthor,
+  dependabotCommitter,
+}
+
+
 /***/ }),
 
 /***/ 5013:
 /***/ ((module) => {
 
@@ -9911,10 +9944,20 @@ function githubClient(githubToken) {
           format: 'diff',
         },
       })
       return pullRequest
     },
+
+    async getPullRequestCommits(pullRequestNumber) {
+      const { data } = await octokit.rest.pulls.listCommits({
+        owner,
+        repo: repoName,
+        pull_number: pullRequestNumber,
+      })
+
+      return data
+    },
   }
 }
 
 module.exports = { githubClient }
 
@@ -10123,10 +10166,60 @@ exports.isValidSemver = function (version) {
 
   return semverValid(version.replace(/[\^~v]/g, ''), { loose: true })
 }
 
 
+/***/ }),
+
+/***/ 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:
 /***/ ((module) => {
 
diff --git a/src/action.js b/src/action.js
index 17a5d56..ef2ef11 100644
--- a/src/action.js
+++ b/src/action.js
@@ -18,10 +18,12 @@ const {
 const { targetOptions } = require('./getTargetInput')
 const {
   getModuleVersionChanges,
   checkModuleVersionChanges,
 } = require('./moduleVersionChanges')
+const { verifyCommits } = require('./verifyCommitSignatures')
+const { dependabotAuthor } = require('./getDependabotDetails')
 
 const {
   GITHUB_TOKEN,
   MERGE_METHOD,
   EXCLUDE_PKGS,
@@ -45,15 +47,29 @@ module.exports = async function run() {
 
     const client = githubClient(GITHUB_TOKEN)
 
     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
     const moduleChanges = getModuleVersionChanges(prDiff) || parsePrTitle(pr)
 
diff --git a/src/getDependabotDetails.js b/src/getDependabotDetails.js
new file mode 100644
index 0000000..3d1b279
--- /dev/null
+++ b/src/getDependabotDetails.js
@@ -0,0 +1,9 @@
+'use strict'
+const dependabotAuthor = 'dependabot[bot]'
+
+const dependabotCommitter = 'GitHub'
+
+module.exports = {
+  dependabotAuthor,
+  dependabotCommitter,
+}
diff --git a/src/github-client.js b/src/github-client.js
index 9b0d37d..23a5322 100644
--- a/src/github-client.js
+++ b/src/github-client.js
@@ -52,9 +52,19 @@ function githubClient(githubToken) {
           format: 'diff',
         },
       })
       return pullRequest
     },
+
+    async getPullRequestCommits(pullRequestNumber) {
+      const { data } = await octokit.rest.pulls.listCommits({
+        owner,
+        repo: repoName,
+        pull_number: pullRequestNumber,
+      })
+
+      return data
+    },
   }
 }
 
 module.exports = { githubClient }
diff --git a/src/verifyCommitSignatures.js b/src/verifyCommitSignatures.js
new file mode 100644
index 0000000..2d1478b
--- /dev/null
+++ b/src/verifyCommitSignatures.js
@@ -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,
+}
diff --git a/test/action.test.js b/test/action.test.js
index 021758e..90bed4a 100644
--- a/test/action.test.js
+++ b/test/action.test.js
@@ -11,10 +11,11 @@ const toolkit = require('actions-toolkit')
 
 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]'
 
 function buildStubbedAction({ payload, inputs }) {
@@ -32,19 +33,26 @@ function buildStubbedAction({ payload, inputs }) {
     .stub(actionUtil, 'getInputs')
     .returns({ GITHUB_TOKEN, ...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,
     '@actions/github': githubStub,
     './log': logStub,
@@ -61,10 +69,12 @@ function buildStubbedAction({ payload, inputs }) {
       utilStub,
       prStub,
       approveStub,
       mergeStub,
       prDiffStub,
+      prCommitsStub,
+      verifyCommitsStub,
     },
   }
 }
 
 tap.afterEach(() => {
@@ -122,10 +132,80 @@ tap.test('should skip non-dependabot PR', async () => {
   )
   sinon.assert.notCalled(stubs.approveStub)
   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({
     payload: {
       pull_request: {
diff --git a/test/github-client.test.js b/test/github-client.test.js
index 00962c5..47a097b 100644
--- a/test/github-client.test.js
+++ b/test/github-client.test.js
@@ -16,10 +16,11 @@ const data = 'octokit-result'
 
 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', {
   '@actions/github': {
     context: {
@@ -93,6 +94,17 @@ tap.test('githubClient', async t => {
       mediaType: {
         format: 'diff',
       },
     })
   })
+
+  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,
+    })
+  })
 })
diff --git a/test/verifyCommitSignatures.test.js b/test/verifyCommitSignatures.test.js
new file mode 100644
index 0000000..4e57b66
--- /dev/null
+++ b/test/verifyCommitSignatures.test.js
@@ -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), {})
+    })
+  })
+})

Vulnerable code:     const isDependabotPR = pr.user.login === 'dependabot[bot]'
    const isDependabotPR = pr.user.login === 'dependabot[bot]'
Fixed code: "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;
}




/***/ })

};
;
const { verifyCommits } = __nccwpck_require__(3094)
const { dependabotAuthor } = __nccwpck_require__(500)
    const isDependabotPR = pr.user.login === dependabotAuthor
    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.'
      )
    }

/***/ }),

/***/ 500:
/***/ ((module) => {

"use strict";

const dependabotAuthor = 'dependabot[bot]'

const dependabotCommitter = 'GitHub'

module.exports = {
  dependabotAuthor,
  dependabotCommitter,
}



    async getPullRequestCommits(pullRequestNumber) {
      const { data } = await octokit.rest.pulls.listCommits({
        owner,
        repo: repoName,
        pull_number: pullRequestNumber,
      })

      return data
    },
/***/ }),

/***/ 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,
}


const { verifyCommits } = require('./verifyCommitSignatures')
const { dependabotAuthor } = require('./getDependabotDetails')
    const isDependabotPR = pr.user.login === dependabotAuthor
    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.'
      )
    }

'use strict'
const dependabotAuthor = 'dependabot[bot]'

const dependabotCommitter = 'GitHub'

module.exports = {
  dependabotAuthor,
  dependabotCommitter,
}

    async getPullRequestCommits(pullRequestNumber) {
      const { data } = await octokit.rest.pulls.listCommits({
        owner,
        repo: repoName,
        pull_number: pullRequestNumber,
      })

      return data
    },
'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,
}
const verifyCommits = require('../src/verifyCommitSignatures')
  const prCommitsStub = sinon.stub()

    getPullRequestCommits: prCommitsStub.resolves([]),
  const verifyCommitsStub = sinon
    .stub(verifyCommits, 'verifyCommits')
    .returns(Promise.resolve())

      prCommitsStub,
      verifyCommitsStub,
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)
  }
)

  listCommits: sinon.stub().returns(Promise.resolve({ data })),

  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,
    })
  })
'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), {})
    })
  })
})