diff --git a/exports.js b/exports.js index df3fd7f7f4..e2217caa80 100644 --- a/exports.js +++ b/exports.js @@ -1575,6 +1575,7 @@ module.exports = { 'httpTriggerRequireHttps' : require(__dirname + '/plugins/google/cloudfunctions/httpTriggerRequireHttps.js'), 'ingressAllTrafficDisabled' : require(__dirname + '/plugins/google/cloudfunctions/ingressAllTrafficDisabled.js'), 'cloudFunctionLabelsAdded' : require(__dirname + '/plugins/google/cloudfunctions/cloudFunctionLabelsAdded.js'), + 'cloudFunctionOldRuntime' : require(__dirname + '/plugins/google/cloudfunctions/cloudFunctionOldRuntime.js'), 'functionAllUsersPolicy' : require(__dirname + '/plugins/google/cloudfunctions/functionAllUsersPolicy.js'), 'serverlessVPCAccess' : require(__dirname + '/plugins/google/cloudfunctions/serverlessVPCAccess.js'), diff --git a/plugins/google/cloudfunctions/cloudFunctionOldRuntime.js b/plugins/google/cloudfunctions/cloudFunctionOldRuntime.js new file mode 100644 index 0000000000..c7a6ee2c75 --- /dev/null +++ b/plugins/google/cloudfunctions/cloudFunctionOldRuntime.js @@ -0,0 +1,118 @@ +var async = require('async'); +var helpers = require('../../../helpers/google'); + +module.exports = { + title: 'Cloud Function Old Runtimes', + category: 'Cloud Functions', + domain: 'Compute', + severity: 'Medium', + description: 'Ensure Cloud Functions are not using deprecated runtime versions.', + more_info: 'Cloud Functions runtimes should be kept current with recent versions of the underlying codebase. It is recommended to update to the latest supported versions to avoid potential security risks and ensure compatibility.', + link: 'https://cloud.google.com/functions/docs/concepts/execution-environment', + recommended_action: 'Modify Cloud Functions to use latest versions.', + apis: ['functions:list'], + settings: { + function_runtime_fail: { + name: 'Cloud Function Runtime Fail', + description: 'Return a failing result for cloud function runtime before this number of days for their end of life date.', + regex: '^[1-9]{1}[0-9]{0,3}$', + default: 0 + } + }, + realtime_triggers: ['functions.CloudFunctionsService.UpdateFunction', 'functions.CloudFunctionsService.CreateFunction', 'functions.CloudFunctionsService.DeleteFunction'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + var regions = helpers.regions(); + + var config = { + function_runtime_fail: parseInt(settings.function_runtime_fail || this.settings.function_runtime_fail.default) + }; + + var deprecatedRuntimes = [ + { 'id':'nodejs10', 'name': 'Node.js 10.x', 'endOfLifeDate': '2021-07-30' }, + { 'id':'nodejs12', 'name': 'Node.js 12', 'endOfLifeDate': '2024-01-30' }, + { 'id':'nodejs14', 'name': 'Node.js 14', 'endOfLifeDate': '2024-01-30' }, + { 'id':'nodejs16', 'name': 'Node.js 16', 'endOfLifeDate': '2024-01-30' }, + { 'id':'nodejs18', 'name': 'Node.js 18', 'endOfLifeDate': '2025-04-30' }, + { 'id':'nodejs20', 'name': 'Node.js 20', 'endOfLifeDate': '2026-04-30' }, + { 'id':'dotnet6', 'name': '.Net 6', 'endOfLifeDate': '2024-11-12' }, + { 'id':'dotnet7', 'name': '.Net 7', 'endOfLifeDate': '2024-05-14' }, + { 'id':'dotnet3', 'name': '.Net Core 3', 'endOfLifeDate': '2024-01-30' }, + { 'id':'python27', 'name': 'Python 2.7', 'endOfLifeDate': '2021-07-15' }, + { 'id':'python36', 'name': 'Python 3.6', 'endOfLifeDate': '2022-07-18' }, + { 'id':'python37', 'name': 'Python 3.7', 'endOfLifeDate': '2024-01-30' }, + { 'id':'python38', 'name': 'Python 3.8', 'endOfLifeDate': '2024-10-14' }, + { 'id':'python39', 'name': 'Python 3.9', 'endOfLifeDate': '2025-10-05' }, + { 'id':'python310', 'name': 'Python 3.10', 'endOfLifeDate': '2026-10-04' }, + { 'id':'python311', 'name': 'Python 3.11', 'endOfLifeDate': '2027-10-24' }, + { 'id':'python312', 'name': 'Python 3.12', 'endOfLifeDate': '2028-10-02' }, + { 'id':'ruby25', 'name': 'Ruby 2.5', 'endOfLifeDate': '2021-07-30' }, + { 'id':'ruby27', 'name': 'Ruby 2.7', 'endOfLifeDate': '2024-01-30' }, + { 'id':'ruby30', 'name': 'Ruby 3.0', 'endOfLifeDate': '2024-03-31' }, + { 'id':'ruby32', 'name': 'Ruby 3.2', 'endOfLifeDate': '2026-03-31' }, + { 'id':'go121', 'name': 'Go 1.21', 'endOfLifeDate': '2024-05-01' }, + { 'id':'go119', 'name': 'Go 1.19', 'endOfLifeDate': '2024-04-30' }, + { 'id':'go118', 'name': 'Go 1.18', 'endOfLifeDate': '2024-01-30' }, + { 'id':'go116', 'name': 'Go 1.16', 'endOfLifeDate': '2024-01-30' }, + { 'id':'go113', 'name': 'Go 1.13', 'endOfLifeDate': '2024-01-30' }, + { 'id':'java8', 'name': 'Java 8', 'endOfLifeDate': '2024-01-08' }, + { 'id':'java11', 'name': 'Java 11', 'endOfLifeDate': '2024-10-01' }, + { 'id':'java17', 'name': 'Java 17', 'endOfLifeDate': '2027-10-01' }, + { 'id':'php74', 'name': 'PHP 7.4', 'endOfLifeDate': '2024-01-30' }, + { 'id':'php81', 'name': 'PHP 8.1', 'endOfLifeDate': '2024-11-25' }, + { 'id':'php82', 'name': 'PHP 8.2', 'endOfLifeDate': '2025-12-08' }, + ]; + + async.each(regions.functions, (region, rcb) => { + var functions = helpers.addSource(cache, source, + ['functions', 'list', region]); + + if (!functions) return rcb(); + + if (functions.err || !functions.data) { + helpers.addResult(results, 3, + 'Unable to query for Google Cloud Functions: ' + helpers.addError(functions), region, null, null, functions.err); + return rcb(); + } + + if (!functions.data.length) { + helpers.addResult(results, 0, 'No Google Cloud functions found', region); + return rcb(); + } + + functions.data.forEach(func => { + if (!func.name || !func.runtime) return; + + var deprecatedRuntime = deprecatedRuntimes.filter((d) => { + return d.id == func.runtime; + }); + + var version = func.runtime; + var runtimeDeprecationDate = (deprecatedRuntime && deprecatedRuntime.length && deprecatedRuntime[0].endOfLifeDate) ? Date.parse(deprecatedRuntime[0].endOfLifeDate) : null; + let today = new Date(); + today = Date.parse(`${today.getFullYear()}-${today.getMonth()+1}-${today.getDate()}`); + var difference = runtimeDeprecationDate? Math.round((runtimeDeprecationDate - today)/(1000 * 3600 * 24)): null; + if (runtimeDeprecationDate && today > runtimeDeprecationDate) { + helpers.addResult(results, 2, + 'Cloud Function is using runtime: ' + deprecatedRuntime[0].name + ' which was deprecated on: ' + deprecatedRuntime[0].endOfLifeDate, + region, func.name); + } else if (difference && config.function_runtime_fail >= difference) { + helpers.addResult(results, 2, + 'Cloud Function is using runtime: ' + version + ' which is deprecating in ' + Math.abs(difference) + ' days', + region, func.name); + } else { + helpers.addResult(results, 0, + 'Cloud Function is running the current version: ' + version, + region, func.name); + } + + }); + + rcb(); + }, function() { + callback(null, results, source); + }); + } +}; \ No newline at end of file diff --git a/plugins/google/cloudfunctions/cloudFunctionOldRuntime.spec.js b/plugins/google/cloudfunctions/cloudFunctionOldRuntime.spec.js new file mode 100644 index 0000000000..7f00884a49 --- /dev/null +++ b/plugins/google/cloudfunctions/cloudFunctionOldRuntime.spec.js @@ -0,0 +1,115 @@ +var expect = require('chai').expect; +var plugin = require('./cloudFunctionOldRuntime'); + + +const functions = [ + { + "name": "projects/my-test-project/locations/us-central1/functions/function-1", + "status": "ACTIVE", + "entryPoint": "helloWorld", + "timeout": "60s", + "availableMemoryMb": 256, + "updateTime": "2021-09-24T06:18:15.265Z", + "runtime": "nodejs14", + "ingressSettings": "ALLOW_ALL" + }, + { + "name": "projects/my-test-project/locations/us-central1/functions/function-2", + "status": "ACTIVE", + "entryPoint": "helloWorld", + "timeout": "60s", + "availableMemoryMb": 256, + "updateTime": "2021-09-24T06:18:15.265Z", + "versionId": "1", + "runtime": "python311", + "ingressSettings": "ALLOW_INTERNAL_AND_GCLB", + "labels": { 'deployment-tool': 'console-cloud' } + + } +]; + +const createCache = (list, err) => { + return { + functions: { + list: { + 'us-central1': { + err: err, + data: list + } + } + } + } +}; + +describe('cloudFunctionOldRuntime', function () { + describe('run', function () { + it('should give passing result if no cloud functions found', function (done) { + const callback = (err, results) => { + expect(results.length).to.be.above(0); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('No Google Cloud functions found'); + expect(results[0].region).to.equal('us-central1'); + done() + }; + + const cache = createCache( + [], + null + ); + + plugin.run(cache, {}, callback); + }); + + it('should give unknown result if unable to query for Google Cloud functions', function (done) { + const callback = (err, results) => { + expect(results.length).to.be.above(0); + expect(results[0].status).to.equal(3); + expect(results[0].message).to.include('Unable to query for Google Cloud Functions'); + expect(results[0].region).to.equal('us-central1'); + done() + }; + + const cache = createCache( + [], + {message: 'error'}, + ); + + plugin.run(cache, {}, callback); + }); + + it('should give passing result if google cloud function is using lasted runtime version', function (done) { + const callback = (err, results) => { + expect(results.length).to.be.above(0); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('Cloud Function is running the current version: '); + expect(results[0].region).to.equal('us-central1'); + done() + }; + + const cache = createCache( + [functions[1]], + null + ); + + plugin.run(cache, {}, callback); + }); + + it('should give failing result if google cloud function is using deprecated runtimeversion', function (done) { + const callback = (err, results) => { + expect(results.length).to.be.above(0); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('which was deprecated on'); + expect(results[0].region).to.equal('us-central1'); + done(); + }; + + const cache = createCache( + [functions[0]], + null ); + + plugin.run(cache, {}, callback); + }); + + }) +}); +