diff --git a/action.yml b/action.yml index 6a6792a3e..fa18b4ace 100644 --- a/action.yml +++ b/action.yml @@ -59,6 +59,9 @@ inputs: name: description: 'User defined upload name. Visible in Codecov UI' required: false + network_filter: + description: 'Used to restrict the set of git/hg files that can be matched with filenames in the coverage report. This is useful for monorepos or other setups where a full filepath may not be specified in the coverage report, and that shortened filepath may appear multiple times in a directory structure (e.g. __init__.py)' + required: false override_branch: description: 'Specify the branch name' required: false @@ -74,9 +77,6 @@ inputs: override_tag: description: 'Specify the git tag' required: false - network_filter: - description: 'Used to restrict the set of git/hg files that can be matched with filenames in the coverage report. This is useful for monorepos or other setups where a full filepath may not be specified in the coverage report, and that shortened filepath may appear multiple times in a directory structure (e.g. __init__.py)' - required: false path_to_write_report: description: 'Write upload file to path before uploading' required: false diff --git a/dist/index.js b/dist/index.js index 707acd60f..ff84a32d3 100644 --- a/dist/index.js +++ b/dist/index.js @@ -13152,12 +13152,49 @@ module.exports = {"$id":"log.json#","$schema":"http://json-schema.org/draft-06/s "use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; + return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (_) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; exports.__esModule = true; var core = __webpack_require__(470); var exec = __webpack_require__(986); var fs = __webpack_require__(747); var request = __webpack_require__(335); var buildExec_1 = __webpack_require__(983); +var validate_1 = __webpack_require__(743); var failCi; try { request({ @@ -13165,49 +13202,69 @@ try { maxAttempts: 10, timeout: 3000, url: 'https://codecov.io/bash', - }, function (error, response, body) { - var _a = buildExec_1["default"](), execArgs = _a.execArgs, options = _a.options, filepath = _a.filepath, failCi = _a.failCi; - try { - if (error && failCi) { - throw error; - } - else if (error) { - core.warning("Codecov warning: " + error.message); - } - fs.writeFile(filepath, body, function (err) { - if (err && failCi) { - throw err; - } - else if (err) { - core.warning("Codecov warning: " + err.message); - } - exec.exec('bash', execArgs, options)["catch"](function (err) { - if (failCi) { - core.setFailed("Codecov failed with the following error: " + err.message); + }, function (error, response, body) { return __awaiter(void 0, void 0, void 0, function () { + var _a, execArgs, options, filepath, failCi, isValid, failure, error_1; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + _a = buildExec_1["default"](), execArgs = _a.execArgs, options = _a.options, filepath = _a.filepath, failCi = _a.failCi; + _b.label = 1; + case 1: + _b.trys.push([1, 3, , 4]); + return [4 /*yield*/, validate_1["default"](body)]; + case 2: + isValid = _b.sent(); + if (!isValid) { + failure = 'Codecov failure: ' + + 'Bash script checksums do not match published values. ' + + 'Please contact security@codecov.io immediately.'; + core.setFailed(failure); + throw new Error(failure); } - else { - core.warning("Codecov warning: " + err.message); + if (error && failCi) { + throw error; } - }) - .then(function () { - unlinkFile(); - }); - var unlinkFile = function () { - fs.unlink(filepath, function (err) { + else if (error) { + core.warning("Codecov warning: " + error.message); + } + fs.writeFile(filepath, body, function (err) { if (err && failCi) { throw err; } else if (err) { core.warning("Codecov warning: " + err.message); } + exec.exec('bash', execArgs, options)["catch"](function (err) { + if (failCi) { + core.setFailed("Codecov failed with the following error: " + err.message); + } + else { + core.warning("Codecov warning: " + err.message); + } + }) + .then(function () { + unlinkFile(); + }); + var unlinkFile = function () { + fs.unlink(filepath, function (err) { + if (err && failCi) { + throw err; + } + else if (err) { + core.warning("Codecov warning: " + err.message); + } + }); + }; }); - }; - }); - } - catch (error) { - core.setFailed("Codecov failed with the following error: " + error.message); - } - }); + return [3 /*break*/, 4]; + case 3: + error_1 = _b.sent(); + core.setFailed("Codecov failed with the following error: " + error_1.message); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }); } catch (error) { if (failCi) { @@ -49116,7 +49173,121 @@ module.exports = function (data, opts) { /***/ }), -/* 743 */, +/* 743 */ +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; + return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (_) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +exports.__esModule = true; +exports.retrieveChecksum = void 0; +var crypto = __webpack_require__(417); +var core = __webpack_require__(470); +var request = __webpack_require__(335); +var validateUploader = function (body) { return __awaiter(void 0, void 0, void 0, function () { + var version, _i, _a, i, publicChecksum, uploaderChecksum; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + version = getVersion(body); + if (version === null) { + core.warning('Codecov could not identify the bash uploader version.'); + return [2 /*return*/, false]; + } + _i = 0, _a = [1, 256, 512]; + _b.label = 1; + case 1: + if (!(_i < _a.length)) return [3 /*break*/, 4]; + i = _a[_i]; + return [4 /*yield*/, exports.retrieveChecksum(version, i)]; + case 2: + publicChecksum = _b.sent(); + uploaderChecksum = calculateChecksum(body, i); + if (uploaderChecksum !== publicChecksum.trim()) { + core.warning("Codecov " + version + " checksums for SHA" + i + " failed to match.\n" + + ("Public checksum: " + publicChecksum) + + ("Uploader checksum: " + uploaderChecksum)); + return [2 /*return*/, false]; + } + _b.label = 3; + case 3: + _i++; + return [3 /*break*/, 1]; + case 4: return [2 /*return*/, true]; + } + }); +}); }; +var retrieveChecksum = function (version, encryption) { return __awaiter(void 0, void 0, void 0, function () { + var url, response; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + url = "https://raw.githubusercontent.com/codecov/codecov-bash/" + version + "/SHA" + encryption + "SUM"; + return [4 /*yield*/, request({ + maxAttempts: 10, + timeout: 3000, + url: url, + })]; + case 1: + response = _a.sent(); + if (response.statusCode != 200) { + core.warning("Codecov could not retrieve checksum SHA" + encryption + " at " + url); + return [2 /*return*/, '']; + } + return [2 /*return*/, response.body]; + } + }); +}); }; +exports.retrieveChecksum = retrieveChecksum; +var calculateChecksum = function (body, i) { + var shasum = crypto.createHash("sha" + i); + shasum.update(body); + return shasum.digest('hex') + " codecov"; +}; +var getVersion = function (body) { + var regex = /VERSION="(.*)+"/g; + var match = regex.exec(body); + return match ? match[1] : null; +}; +exports["default"] = validateUploader; + + +/***/ }), /* 744 */ /***/ (function(module) { diff --git a/src/index.ts b/src/index.ts index 12e294403..4cb77680c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ const fs = require('fs'); const request = require('requestretry'); import buildExec from './buildExec'; +import validateUploader from './validate'; let failCi; try { @@ -13,10 +14,19 @@ try { maxAttempts: 10, timeout: 3000, url: 'https://codecov.io/bash', - }, (error, response, body) => { + }, async (error, response, body) => { const {execArgs, options, filepath, failCi} = buildExec(); try { + const isValid = await validateUploader(body); + if (!isValid) { + const failure = 'Codecov failure: ' + + 'Bash script checksums do not match published values. ' + + 'Please contact security@codecov.io immediately.'; + core.setFailed(failure); + throw new Error(failure); + } + if (error && failCi) { throw error; } else if (error) { diff --git a/src/validate.test.ts b/src/validate.test.ts new file mode 100644 index 000000000..f59c8384c --- /dev/null +++ b/src/validate.test.ts @@ -0,0 +1,39 @@ +import validateUploader, {retrieveChecksum} from './validate'; + +const request = require('requestretry'); + +const bashScript = (async () => { + try { + const script = await request({ + json: false, + maxAttempts: 10, + timeout: 3000, + url: 'https://codecov.io/bash', + }); + return script.body; + } catch (err) { + throw err; + } +}); + +test('valid checksums', async () => { + const valid = await validateUploader(await bashScript()); + expect(valid).toBeTruthy(); +}); + +test('invalid checksums', async () => { + const script = await bashScript(); + const valid = await validateUploader(script.substring(0, script.length - 1)); + expect(valid).toBeFalsy(); +}); + +test('invalid script version', async () => { + const script = await bashScript(); + const valid = await validateUploader(script.substring(0, 20)); + expect(valid).toBeFalsy(); +}); + +test('invalid public checksum file', async () => { + const checksum = await retrieveChecksum('foo', 'bar'); + expect(checksum).toBeFalsy(); +}); diff --git a/src/validate.ts b/src/validate.ts new file mode 100644 index 000000000..68cbd63ad --- /dev/null +++ b/src/validate.ts @@ -0,0 +1,58 @@ +const crypto = require('crypto'); + +const core = require('@actions/core'); + +const request = require('requestretry'); + +const validateUploader = async (body) => { + const version = getVersion(body); + if (version === null) { + core.warning('Codecov could not identify the bash uploader version.'); + return false; + } + + for (const i of [1, 256, 512]) { + const publicChecksum = await retrieveChecksum(version, i); + const uploaderChecksum = calculateChecksum(body, i); + if (uploaderChecksum !== publicChecksum.trim()) { + core.warning( + `Codecov ${version} checksums for SHA${i} failed to match.\n` + + `Public checksum: ${publicChecksum}` + + `Uploader checksum: ${uploaderChecksum}`, + ); + return false; + } + } + return true; +}; + +export const retrieveChecksum = async (version, encryption) => { + const url = `https://raw.githubusercontent.com/codecov/codecov-bash/${version}/SHA${encryption}SUM`; + const response = await request({ + maxAttempts: 10, + timeout: 3000, + url: url, + }); + + if (response.statusCode != 200) { + core.warning( + `Codecov could not retrieve checksum SHA${encryption} at ${url}`, + ); + return ''; + } + return response.body; +}; + +const calculateChecksum = (body, i) => { + const shasum = crypto.createHash(`sha${i}`); + shasum.update(body); + return `${shasum.digest('hex')} codecov`; +}; + +const getVersion = (body) => { + const regex = /VERSION="(.*)+"/g; + const match = regex.exec(body); + return match ? match[1] : null; +}; + +export default validateUploader;