From 027cbb97872ae584c7f93c36a10a06db73c3154a Mon Sep 17 00:00:00 2001 From: HamzaAziz-1 Date: Tue, 25 Nov 2025 19:43:16 +0500 Subject: [PATCH 1/6] addd cloud functions v2 plugins --- exports.js | 3 + helpers/google/api.js | 23 +++ helpers/google/regions.js | 5 + helpers/google/resources.js | 4 + .../cloudFunctionV2HttpsOnly.js | 104 ++++++++++ .../cloudFunctionV2HttpsOnly.spec.js | 177 ++++++++++++++++++ 6 files changed, 316 insertions(+) create mode 100644 plugins/google/cloudfunctionsv2/cloudFunctionV2HttpsOnly.js create mode 100644 plugins/google/cloudfunctionsv2/cloudFunctionV2HttpsOnly.spec.js diff --git a/exports.js b/exports.js index d2f4c36e0a..23c7a55c96 100644 --- a/exports.js +++ b/exports.js @@ -1624,6 +1624,9 @@ module.exports = { 'serverlessVPCAccess' : require(__dirname + '/plugins/google/cloudfunctions/serverlessVPCAccess.js'), 'cloudFunctionNetworkExposure' : require(__dirname + '/plugins/google/cloudfunctions/cloudFunctionNetworkExposure.js'), 'cloudFunctionsPrivilegeAnalysis': require(__dirname + '/plugins/google/cloudfunctions/cloudFunctionsPrivilegeAnalysis.js'), + + 'cloudFunctionV2HttpsOnly' : require(__dirname + '/plugins/google/cloudfunctionsv2/cloudFunctionV2HttpsOnly.js'), + 'computeAllowedExternalIPs' : require(__dirname + '/plugins/google/cloudresourcemanager/computeAllowedExternalIPs.js'), 'disableAutomaticIAMGrants' : require(__dirname + '/plugins/google/cloudresourcemanager/disableAutomaticIAMGrants.js'), 'disableGuestAttributes' : require(__dirname + '/plugins/google/cloudresourcemanager/disableGuestAttributes.js'), diff --git a/helpers/google/api.js b/helpers/google/api.js index de44961b3b..31c467b610 100644 --- a/helpers/google/api.js +++ b/helpers/google/api.js @@ -390,6 +390,18 @@ var calls = { enabled: true } }, + functionsv2: { + list: { + url: 'https://cloudfunctions.googleapis.com/v2/projects/{projectId}/locations/{locationId}/functions', + location: 'region', + paginationKey: 'pageSize', + pagination: true, + dataFilterKey: 'functions' + }, + sendIntegration: { + enabled: true + } + }, keyRings: { list: { url: 'https://cloudkms.googleapis.com/v1/projects/{projectId}/locations/{locationId}/keyRings', @@ -850,6 +862,17 @@ var postcalls = { properties: ['name'] } }, + functionsv2: { + getIamPolicy: { + url: 'https://cloudfunctions.googleapis.com/v2/{name}:getIamPolicy', + location: null, + method: 'POST', + reliesOnService: ['functionsv2'], + reliesOnCall: ['list'], + properties: ['name'], + body: { options: { requestedPolicyVersion: 3 } } + } + }, jobs: { get: { //https://dataflow.googleapis.com/v1b3/projects/{projectId}/jobs/{jobId} url: 'https://dataflow.googleapis.com/v1b3/projects/{projectId}/locations/{locationId}/jobs/{id}', diff --git a/helpers/google/regions.js b/helpers/google/regions.js index f2bd204292..8c68e69487 100644 --- a/helpers/google/regions.js +++ b/helpers/google/regions.js @@ -112,6 +112,11 @@ module.exports = { 'europe-west1', 'europe-west2', 'europe-west3', 'europe-west6', 'europe-central2', 'asia-south1', 'asia-southeast1', 'asia-southeast2', 'asia-east1', 'asia-east2', 'asia-northeast1', 'asia-northeast2', 'asia-northeast3', 'australia-southeast1' ], + functionsv2: [ + 'us-east1', 'us-east4', 'us-west1', 'us-west2', 'us-west3', 'us-west4', 'us-central1', 'northamerica-northeast1', 'southamerica-east1', + 'europe-west1', 'europe-west2', 'europe-west3', 'europe-west6', 'europe-central2', 'asia-south1', 'asia-southeast1', 'asia-southeast2', + 'asia-east1', 'asia-east2', 'asia-northeast1', 'asia-northeast2', 'asia-northeast3', 'australia-southeast1' + ], cloudbuild: ['global', 'us-east1', 'us-east4', 'us-west2', 'us-west3', 'us-west4', 'us-central1', 'us-west1', 'northamerica-northeast1', 'northamerica-northeast2', 'southamerica-east1', 'southamerica-west1', 'europe-west1', 'europe-west2', 'europe-west3', 'europe-west4', 'europe-west6', 'europe-central2', 'europe-north1', 'asia-south1', 'asia-south2', 'asia-southeast1', 'asia-southeast2', diff --git a/helpers/google/resources.js b/helpers/google/resources.js index 54b7c6feed..0056244717 100644 --- a/helpers/google/resources.js +++ b/helpers/google/resources.js @@ -52,6 +52,10 @@ module.exports = { functions: { list: 'name' }, + functionsv2: { + list: 'name', + getIamPolicy: 'name' + }, instanceGroups: { aggregatedList: '' }, diff --git a/plugins/google/cloudfunctionsv2/cloudFunctionV2HttpsOnly.js b/plugins/google/cloudfunctionsv2/cloudFunctionV2HttpsOnly.js new file mode 100644 index 0000000000..08963eb5ef --- /dev/null +++ b/plugins/google/cloudfunctionsv2/cloudFunctionV2HttpsOnly.js @@ -0,0 +1,104 @@ +var async = require('async'); +var helpers = require('../../../helpers/google'); + +module.exports = { + title: 'HTTP Trigger Require HTTPS V2', + category: 'Cloud Functions', + domain: 'Serverless', + severity: 'Medium', + description: 'Ensure that Cloud Functions V2 are configured to require HTTPS for HTTP invocations.', + more_info: 'You can make your Google Cloud Functions V2 calls secure by making sure that they require HTTPS.', + link: 'https://cloud.google.com/functions/docs/writing/http', + recommended_action: 'Ensure that your Google Cloud Functions V2 always require HTTPS.', + apis: ['functionsv2:list'], + remediation_min_version: '202207282132', + remediation_description: 'All Google Cloud Functions V2 will be configured to require HTTPS for HTTP invocations.', + apis_remediate: ['functionsv2:list', 'projects:get'], + actions: {remediate:['CloudFunctionsService.UpdateFunction'], rollback:['CloudFunctionsService.UpdateFunction']}, + permissions: {remediate: ['cloudfunctions.functions.update'], rollback: ['cloudfunctions.functions.create']}, + realtime_triggers: ['functions.CloudFunctionsService.UpdateFunction','functions.CloudFunctionsService.DeleteFunction', 'functions.CloudFunctionsService.CreateFunction'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + var regions = helpers.regions(); + + async.each(regions.functions, (region, rcb) => { + var functions = helpers.addSource(cache, source, + ['functionsv2', '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(funct => { + if (!funct.name) return; + + if (!funct.environment || funct.environment !== 'GEN_2') return; + + let serviceConfig = funct.serviceConfig || {}; + + if (serviceConfig.uri) { + if (serviceConfig.securityLevel && serviceConfig.securityLevel == 'SECURE_ALWAYS') { + helpers.addResult(results, 0, + 'Cloud Function is configured to require HTTPS for HTTP invocations', region, funct.name); + } else { + helpers.addResult(results, 2, + 'Cloud Function is not configured to require HTTPS for HTTP invocations', region, funct.name); + } + } else { + helpers.addResult(results, 0, + 'Cloud Function trigger type is not HTTP', region, funct.name); + } + }); + + rcb(); + }, function() { + callback(null, results, source); + }); + }, + remediate: function(config, cache, settings, resource, callback) { + var remediation_file = settings.remediation_file; + + // inputs specific to the plugin + var pluginName = 'httpTriggerRequireHttps'; + var baseUrl = 'https://cloudfunctions.googleapis.com/v2/{resource}?updateMask=serviceConfig.securityLevel'; + var method = 'PATCH'; + var putCall = this.actions.remediate; + + // create the params necessary for the remediation + var body = { + serviceConfig: { + securityLevel: 'SECURE_ALWAYS' + } + }; + // logging + remediation_file['pre_remediate']['actions'][pluginName][resource] = { + 'httpTriggerRequireHttps': 'Disabled' + }; + + helpers.remediatePlugin(config, method, body, baseUrl, resource, remediation_file, putCall, pluginName, function(err, action) { + if (err) return callback(err); + if (action) action.action = putCall; + + + remediation_file['post_remediate']['actions'][pluginName][resource] = action; + remediation_file['remediate']['actions'][pluginName][resource] = { + 'Action': 'Enabled' + }; + + callback(null, action); + }); + } + +}; + diff --git a/plugins/google/cloudfunctionsv2/cloudFunctionV2HttpsOnly.spec.js b/plugins/google/cloudfunctionsv2/cloudFunctionV2HttpsOnly.spec.js new file mode 100644 index 0000000000..0d7d2b42f6 --- /dev/null +++ b/plugins/google/cloudfunctionsv2/cloudFunctionV2HttpsOnly.spec.js @@ -0,0 +1,177 @@ +var expect = require('chai').expect; +var plugin = require('./cloudFunctionV2HttpsOnly'); + + +const functions = [ + { + "name": "projects/my-test-project/locations/us-central1/functions/function-1", + "environment": "GEN_2", + "state": "ACTIVE", + "updateTime": "2021-09-24T06:18:15.265Z", + "buildConfig": { + "runtime": "nodejs20", + "entryPoint": "helloWorld" + }, + "serviceConfig": { + "serviceAccountEmail": "test@test-project.iam.gserviceaccount.com", + "uri": "https://us-central1-my-test-project.cloudfunctions.net/function-1", + "securityLevel": "SECURE_OPTIONAL" + } + }, + { + "name": "projects/my-test-project/locations/us-central1/functions/function-2", + "environment": "GEN_2", + "state": "ACTIVE", + "updateTime": "2021-09-24T06:18:15.265Z", + "buildConfig": { + "runtime": "nodejs20", + "entryPoint": "helloWorld" + }, + "serviceConfig": { + "serviceAccountEmail": "test@test-project.iam.gserviceaccount.com", + "uri": "https://us-central1-my-test-project.cloudfunctions.net/function-2", + "securityLevel": "SECURE_ALWAYS" + } + }, + { + "name": "projects/my-test-project/locations/us-central1/functions/function-3", + "environment": "GEN_2", + "state": "ACTIVE", + "updateTime": "2021-09-24T06:18:15.265Z", + "buildConfig": { + "runtime": "nodejs20", + "entryPoint": "handleEvent" + }, + "serviceConfig": { + "serviceAccountEmail": "test@test-project.iam.gserviceaccount.com" + } + }, + { + "name": "projects/my-test-project/locations/us-central1/functions/function-4", + "environment": "GEN_1", + "state": "ACTIVE", + "runtime": "nodejs14", + "httpsTrigger": { + "url": "https://us-central1-my-test-project.cloudfunctions.net/function-4", + "securityLevel": "SECURE_OPTIONAL" + } + } +]; + +const createCache = (list, err) => { + return { + functionsv2: { + list: { + 'us-central1': { + err: err, + data: list + } + } + } + } +}; + +describe('httpTriggerRequireHttps', function () { + describe('run', function () { + it('should give passing result if no Cloud Functions V2 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 Cloud Function is configured to require HTTPS for HTTP invocations', 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 configured to require HTTPS for HTTP invocations'); + 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 Cloud Function is not configured to require HTTPS for HTTP invocations', 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('Cloud Function is not configured to require HTTPS for HTTP invocations'); + expect(results[0].region).to.equal('us-central1'); + done(); + }; + + const cache = createCache( + [functions[0]], + null + ); + + plugin.run(cache, {}, callback); + }); + + it('should give passing result if Cloud Function trigger type is not HTTP', 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 trigger type is not HTTP'); + expect(results[0].region).to.equal('us-central1'); + done(); + }; + + const cache = createCache( + [functions[2]], + null + ); + + plugin.run(cache, {}, callback); + }); + + it('should not check Gen 1 functions in v2 API response', function (done) { + const callback = (err, results) => { + expect(results.length).to.equal(0); + done(); + }; + + const cache = createCache( + [functions[3]], + null + ); + + plugin.run(cache, {}, callback); + }); + + }) +}); + From 0ae0f4757185068306fc023f2841210f4a2e30a3 Mon Sep 17 00:00:00 2001 From: HamzaAziz-1 Date: Wed, 26 Nov 2025 19:58:22 +0500 Subject: [PATCH 2/6] add plugins --- exports.js | 4 +- .../cloudFunctionV2DefaultServiceAccount.js | 65 +++++++ ...oudFunctionV2DefaultServiceAccount.spec.js | 173 ++++++++++++++++++ .../cloudFunctionV2IngressSettings.js | 65 +++++++ .../cloudFunctionV2IngressSettings.spec.js | 172 +++++++++++++++++ 5 files changed, 478 insertions(+), 1 deletion(-) create mode 100644 plugins/google/cloudfunctionsv2/cloudFunctionV2DefaultServiceAccount.js create mode 100644 plugins/google/cloudfunctionsv2/cloudFunctionV2DefaultServiceAccount.spec.js create mode 100644 plugins/google/cloudfunctionsv2/cloudFunctionV2IngressSettings.js create mode 100644 plugins/google/cloudfunctionsv2/cloudFunctionV2IngressSettings.spec.js diff --git a/exports.js b/exports.js index 23c7a55c96..7ca6c96080 100644 --- a/exports.js +++ b/exports.js @@ -1625,7 +1625,9 @@ module.exports = { 'cloudFunctionNetworkExposure' : require(__dirname + '/plugins/google/cloudfunctions/cloudFunctionNetworkExposure.js'), 'cloudFunctionsPrivilegeAnalysis': require(__dirname + '/plugins/google/cloudfunctions/cloudFunctionsPrivilegeAnalysis.js'), - 'cloudFunctionV2HttpsOnly' : require(__dirname + '/plugins/google/cloudfunctionsv2/cloudFunctionV2HttpsOnly.js'), + 'cloudFunctionV2HttpsOnly' : require(__dirname + '/plugins/google/cloudfunctionsv2/cloudFunctionV2HttpsOnly.js'), + 'cloudFunctionV2DefaultServiceAccount': require(__dirname + '/plugins/google/cloudfunctionsv2/cloudFunctionV2DefaultServiceAccount.js'), + 'cloudFunctionV2IngressSettings': require(__dirname + '/plugins/google/cloudfunctionsv2/cloudFunctionV2IngressSettings.js'), 'computeAllowedExternalIPs' : require(__dirname + '/plugins/google/cloudresourcemanager/computeAllowedExternalIPs.js'), 'disableAutomaticIAMGrants' : require(__dirname + '/plugins/google/cloudresourcemanager/disableAutomaticIAMGrants.js'), diff --git a/plugins/google/cloudfunctionsv2/cloudFunctionV2DefaultServiceAccount.js b/plugins/google/cloudfunctionsv2/cloudFunctionV2DefaultServiceAccount.js new file mode 100644 index 0000000000..a212cb5934 --- /dev/null +++ b/plugins/google/cloudfunctionsv2/cloudFunctionV2DefaultServiceAccount.js @@ -0,0 +1,65 @@ +var async = require('async'); +var helpers = require('../../../helpers/google'); + +module.exports = { + title: 'Cloud Function V2 Default Service Account', + category: 'Cloud Functions', + domain: 'Serverless', + severity: 'Medium', + description: 'Ensures that Cloud Functions V2 are not using the default service account.', + more_info: 'Using the default service account for Cloud Functions V2 can lead to privilege escalation and overly permissive access. It is recommended to use a user-managed service account for each function in a project instead of the default service account. A managed service account allows more precise access control by granting only the necessary permissions through Identity and Access Management (IAM).', + link: 'https://cloud.google.com/functions/docs/securing/function-identity', + recommended_action: 'Ensure that no Cloud Functions V2 are using the default service account.', + apis: ['functionsv2:list'], + realtime_triggers: ['functions.CloudFunctionsService.UpdateFunction', 'functions.CloudFunctionsService.CreateFunction', 'functions.CloudFunctionsService.DeleteFunction'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + var regions = helpers.regions(); + + async.each(regions.functions, (region, rcb) => { + var functions = helpers.addSource(cache, source, + ['functionsv2', '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) return; + + if (!func.environment || func.environment !== 'GEN_2') return; + + let serviceAccountEmail = func.serviceConfig && func.serviceConfig.serviceAccountEmail + ? func.serviceConfig.serviceAccountEmail + : null; + + if (serviceAccountEmail && serviceAccountEmail.endsWith('@appspot.gserviceaccount.com')) { + helpers.addResult(results, 2, + 'Cloud Function is using default service account', region, func.name); + } else if (serviceAccountEmail) { + helpers.addResult(results, 0, + 'Cloud Function is not using default service account', region, func.name); + } else { + helpers.addResult(results, 2, + 'Cloud Function does not have a service account configured', region, func.name); + } + }); + + rcb(); + }, function() { + callback(null, results, source); + }); + } +}; + diff --git a/plugins/google/cloudfunctionsv2/cloudFunctionV2DefaultServiceAccount.spec.js b/plugins/google/cloudfunctionsv2/cloudFunctionV2DefaultServiceAccount.spec.js new file mode 100644 index 0000000000..a83f8f7e83 --- /dev/null +++ b/plugins/google/cloudfunctionsv2/cloudFunctionV2DefaultServiceAccount.spec.js @@ -0,0 +1,173 @@ +var expect = require('chai').expect; +var plugin = require('./cloudFunctionV2DefaultServiceAccount'); + + +const functions = [ + { + "name": "projects/my-test-project/locations/us-central1/functions/function-1", + "environment": "GEN_2", + "state": "ACTIVE", + "updateTime": "2021-09-24T06:18:15.265Z", + "buildConfig": { + "runtime": "nodejs20", + "entryPoint": "helloWorld" + }, + "serviceConfig": { + "serviceAccountEmail": "aqua@appspot.gserviceaccount.com", + "ingressSettings": "ALLOW_ALL" + } + }, + { + "name": "projects/my-test-project/locations/us-central1/functions/function-2", + "environment": "GEN_2", + "state": "ACTIVE", + "updateTime": "2021-09-24T06:18:15.265Z", + "buildConfig": { + "runtime": "nodejs20", + "entryPoint": "helloWorld" + }, + "serviceConfig": { + "serviceAccountEmail": "custom-sa@my-test-project.iam.gserviceaccount.com", + "ingressSettings": "ALLOW_INTERNAL_AND_GCLB" + }, + "labels": { 'deployment-tool': 'console-cloud' } + }, + { + "name": "projects/my-test-project/locations/us-central1/functions/function-3", + "environment": "GEN_2", + "state": "ACTIVE", + "updateTime": "2021-09-24T06:18:15.265Z", + "buildConfig": { + "runtime": "nodejs20", + "entryPoint": "helloWorld" + }, + "serviceConfig": { + "ingressSettings": "ALLOW_INTERNAL_ONLY" + } + }, + { + "name": "projects/my-test-project/locations/us-central1/functions/function-4", + "environment": "GEN_1", + "state": "ACTIVE", + "runtime": "nodejs14", + "serviceAccountEmail": "aqua@appspot.gserviceaccount.com" + } +]; + +const createCache = (list, err) => { + return { + functionsv2: { + list: { + 'us-central1': { + err: err, + data: list + } + } + } + } +}; + +describe('functionDefaultServiceAccount', function () { + describe('run', function () { + it('should give passing result if no Cloud Functions V2 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 Cloud Function is not using default service account', 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 not using default service account'); + 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 Cloud Function is using default service account', 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('Cloud Function is using default service account'); + expect(results[0].region).to.equal('us-central1'); + done(); + }; + + const cache = createCache( + [functions[0]], + null + ); + + plugin.run(cache, {}, callback); + }); + + it('should give failing result if Cloud Function does not have a service account configured', 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('Cloud Function does not have a service account configured'); + expect(results[0].region).to.equal('us-central1'); + done(); + }; + + const cache = createCache( + [functions[2]], + null + ); + + plugin.run(cache, {}, callback); + }); + + it('should not check Gen 1 functions in v2 API response', function (done) { + const callback = (err, results) => { + expect(results.length).to.equal(0); + done(); + }; + + const cache = createCache( + [functions[3]], + null + ); + + plugin.run(cache, {}, callback); + }); + + }) +}); + diff --git a/plugins/google/cloudfunctionsv2/cloudFunctionV2IngressSettings.js b/plugins/google/cloudfunctionsv2/cloudFunctionV2IngressSettings.js new file mode 100644 index 0000000000..78219be8b3 --- /dev/null +++ b/plugins/google/cloudfunctionsv2/cloudFunctionV2IngressSettings.js @@ -0,0 +1,65 @@ +var async = require('async'); +var helpers = require('../../../helpers/google'); + +module.exports = { + title: 'Ingress All Traffic Disabled V2', + category: 'Cloud Functions', + domain: 'Serverless', + severity: 'Medium', + description: 'Ensure that Cloud Functions V2 are configured to allow only internal traffic or traffic from Cloud Load Balancer.', + more_info: 'You can secure your Google Cloud Functions V2 by implementing network-based access control.', + link: 'https://cloud.google.com/functions/docs/securing/authenticating', + recommended_action: 'Ensure that your Google Cloud Functions V2 do not allow external traffic from the internet.', + apis: ['functionsv2:list'], + realtime_triggers: ['functions.CloudFunctionsService.UpdateFunction', 'functions.CloudFunctionsService.CreateFunction', 'functions.CloudFunctionsService.DeleteFunction'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + var regions = helpers.regions(); + + async.each(regions.functions, (region, rcb) => { + var functions = helpers.addSource(cache, source, + ['functionsv2', '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) return; + + if (!func.environment || func.environment !== 'GEN_2') return; + + let ingressSettings = func.serviceConfig && func.serviceConfig.ingressSettings + ? func.serviceConfig.ingressSettings + : null; + + if (ingressSettings && ingressSettings.toUpperCase() == 'ALLOW_ALL') { + helpers.addResult(results, 2, + 'Cloud Function is configured to allow all traffic', region, func.name); + } else if (ingressSettings) { + helpers.addResult(results, 0, + 'Cloud Function is configured to allow only internal and CLB traffic', region, func.name); + } else { + helpers.addResult(results, 2, + 'Cloud Function does not have ingress settings configured', region, func.name); + } + }); + + rcb(); + }, function() { + callback(null, results, source); + }); + } +}; + diff --git a/plugins/google/cloudfunctionsv2/cloudFunctionV2IngressSettings.spec.js b/plugins/google/cloudfunctionsv2/cloudFunctionV2IngressSettings.spec.js new file mode 100644 index 0000000000..13168f81e7 --- /dev/null +++ b/plugins/google/cloudfunctionsv2/cloudFunctionV2IngressSettings.spec.js @@ -0,0 +1,172 @@ +var expect = require('chai').expect; +var plugin = require('./cloudFunctionV2IngressSettings'); + + +const functions = [ + { + "name": "projects/my-test-project/locations/us-central1/functions/function-1", + "environment": "GEN_2", + "state": "ACTIVE", + "updateTime": "2021-09-24T06:18:15.265Z", + "buildConfig": { + "runtime": "nodejs20", + "entryPoint": "helloWorld" + }, + "serviceConfig": { + "serviceAccountEmail": "test@test-project.iam.gserviceaccount.com", + "ingressSettings": "ALLOW_ALL" + } + }, + { + "name": "projects/my-test-project/locations/us-central1/functions/function-2", + "environment": "GEN_2", + "state": "ACTIVE", + "updateTime": "2021-09-24T06:18:15.265Z", + "buildConfig": { + "runtime": "nodejs20", + "entryPoint": "helloWorld" + }, + "serviceConfig": { + "serviceAccountEmail": "test@test-project.iam.gserviceaccount.com", + "ingressSettings": "ALLOW_INTERNAL_AND_GCLB" + } + }, + { + "name": "projects/my-test-project/locations/us-central1/functions/function-3", + "environment": "GEN_2", + "state": "ACTIVE", + "updateTime": "2021-09-24T06:18:15.265Z", + "buildConfig": { + "runtime": "nodejs20", + "entryPoint": "helloWorld" + }, + "serviceConfig": { + "serviceAccountEmail": "test@test-project.iam.gserviceaccount.com" + } + }, + { + "name": "projects/my-test-project/locations/us-central1/functions/function-4", + "environment": "GEN_1", + "state": "ACTIVE", + "runtime": "nodejs14", + "ingressSettings": "ALLOW_ALL" + } +]; + +const createCache = (list, err) => { + return { + functionsv2: { + list: { + 'us-central1': { + err: err, + data: list + } + } + } + } +}; + +describe('ingressAllTrafficDisabled', function () { + describe('run', function () { + it('should give passing result if no Cloud Functions V2 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 Cloud Function is configured to allow only internal and CLB traffic', 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 configured to allow only internal and CLB traffic'); + 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 Cloud Function is configured to allow all traffic', 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('Cloud Function is configured to allow all traffic'); + expect(results[0].region).to.equal('us-central1'); + done(); + }; + + const cache = createCache( + [functions[0]], + null + ); + + plugin.run(cache, {}, callback); + }); + + it('should give failing result if Cloud Function does not have ingress settings configured', 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('Cloud Function does not have ingress settings configured'); + expect(results[0].region).to.equal('us-central1'); + done(); + }; + + const cache = createCache( + [functions[2]], + null + ); + + plugin.run(cache, {}, callback); + }); + + it('should not check Gen 1 functions in v2 API response', function (done) { + const callback = (err, results) => { + expect(results.length).to.equal(0); + done(); + }; + + const cache = createCache( + [functions[3]], + null + ); + + plugin.run(cache, {}, callback); + }); + + }) +}); + From 89369db243b5007e2b682fb93f2bebfa5e6ad27b Mon Sep 17 00:00:00 2001 From: HamzaAziz-1 Date: Wed, 26 Nov 2025 20:00:53 +0500 Subject: [PATCH 3/6] add gcp func v2 plugins --- exports.js | 3 + .../cloudFunctionV2LabelsAdded.js | 58 ++++++ .../cloudFunctionV2LabelsAdded.spec.js | 143 ++++++++++++++ .../cloudFunctionV2OldRuntime.js | 129 +++++++++++++ .../cloudFunctionV2OldRuntime.spec.js | 168 +++++++++++++++++ .../cloudFunctionV2VPCConnector.js | 67 +++++++ .../cloudFunctionV2VPCConnector.spec.js | 176 ++++++++++++++++++ 7 files changed, 744 insertions(+) create mode 100644 plugins/google/cloudfunctionsv2/cloudFunctionV2LabelsAdded.js create mode 100644 plugins/google/cloudfunctionsv2/cloudFunctionV2LabelsAdded.spec.js create mode 100644 plugins/google/cloudfunctionsv2/cloudFunctionV2OldRuntime.js create mode 100644 plugins/google/cloudfunctionsv2/cloudFunctionV2OldRuntime.spec.js create mode 100644 plugins/google/cloudfunctionsv2/cloudFunctionV2VPCConnector.js create mode 100644 plugins/google/cloudfunctionsv2/cloudFunctionV2VPCConnector.spec.js diff --git a/exports.js b/exports.js index 7ca6c96080..ae68dfbb91 100644 --- a/exports.js +++ b/exports.js @@ -1628,6 +1628,9 @@ module.exports = { 'cloudFunctionV2HttpsOnly' : require(__dirname + '/plugins/google/cloudfunctionsv2/cloudFunctionV2HttpsOnly.js'), 'cloudFunctionV2DefaultServiceAccount': require(__dirname + '/plugins/google/cloudfunctionsv2/cloudFunctionV2DefaultServiceAccount.js'), 'cloudFunctionV2IngressSettings': require(__dirname + '/plugins/google/cloudfunctionsv2/cloudFunctionV2IngressSettings.js'), + 'cloudFunctionV2LabelsAdded' : require(__dirname + '/plugins/google/cloudfunctionsv2/cloudFunctionV2LabelsAdded.js'), + 'cloudFunctionV2OldRuntime' : require(__dirname + '/plugins/google/cloudfunctionsv2/cloudFunctionV2OldRuntime.js'), + 'cloudFunctionV2VPCConnector' : require(__dirname + '/plugins/google/cloudfunctionsv2/cloudFunctionV2VPCConnector.js'), 'computeAllowedExternalIPs' : require(__dirname + '/plugins/google/cloudresourcemanager/computeAllowedExternalIPs.js'), 'disableAutomaticIAMGrants' : require(__dirname + '/plugins/google/cloudresourcemanager/disableAutomaticIAMGrants.js'), diff --git a/plugins/google/cloudfunctionsv2/cloudFunctionV2LabelsAdded.js b/plugins/google/cloudfunctionsv2/cloudFunctionV2LabelsAdded.js new file mode 100644 index 0000000000..3d7bdd6444 --- /dev/null +++ b/plugins/google/cloudfunctionsv2/cloudFunctionV2LabelsAdded.js @@ -0,0 +1,58 @@ +var async = require('async'); +var helpers = require('../../../helpers/google'); + +module.exports = { + title: 'Cloud Function V2 Labels Added', + category: 'Cloud Functions', + domain: 'Serverless', + severity: 'Low', + description: 'Ensure that all Cloud Functions V2 have labels added.', + more_info: 'Labels are a lightweight way to group resources together that are related to or associated with each other. It is a best practice to label cloud resources to better organize and gain visibility into their usage.', + link: 'https://cloud.google.com/functions/docs/configuring', + recommended_action: 'Ensure labels are added to all Cloud Functions V2.', + apis: ['functionsv2:list'], + realtime_triggers: ['functions.CloudFunctionsService.UpdateFunction', 'functions.CloudFunctionsService.CreateFunction', 'functions.CloudFunctionsService.DeleteFunction'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + var regions = helpers.regions(); + + async.each(regions.functions, (region, rcb) => { + var functions = helpers.addSource(cache, source, + ['functionsv2', '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) return; + + if (!func.environment || func.environment !== 'GEN_2') return; + + if (func.labels && Object.keys(func.labels).length) { + helpers.addResult(results, 0, + `${Object.keys(func.labels).length} labels found for Cloud Function`, region, func.name); + } else { + helpers.addResult(results, 2, + 'Cloud Function does not have any labels', region, func.name); + } + }); + + rcb(); + }, function() { + callback(null, results, source); + }); + } +}; + diff --git a/plugins/google/cloudfunctionsv2/cloudFunctionV2LabelsAdded.spec.js b/plugins/google/cloudfunctionsv2/cloudFunctionV2LabelsAdded.spec.js new file mode 100644 index 0000000000..2bf55b1c37 --- /dev/null +++ b/plugins/google/cloudfunctionsv2/cloudFunctionV2LabelsAdded.spec.js @@ -0,0 +1,143 @@ +var expect = require('chai').expect; +var plugin = require('./cloudFunctionV2LabelsAdded'); + + +const functions = [ + { + "name": "projects/my-test-project/locations/us-central1/functions/function-1", + "environment": "GEN_2", + "state": "ACTIVE", + "updateTime": "2021-09-24T06:18:15.265Z", + "buildConfig": { + "runtime": "nodejs20", + "entryPoint": "helloWorld" + }, + "serviceConfig": { + "serviceAccountEmail": "test@test-project.iam.gserviceaccount.com", + "ingressSettings": "ALLOW_ALL" + } + }, + { + "name": "projects/my-test-project/locations/us-central1/functions/function-2", + "environment": "GEN_2", + "state": "ACTIVE", + "updateTime": "2021-09-24T06:18:15.265Z", + "buildConfig": { + "runtime": "nodejs20", + "entryPoint": "helloWorld" + }, + "serviceConfig": { + "serviceAccountEmail": "test@test-project.iam.gserviceaccount.com", + "ingressSettings": "ALLOW_INTERNAL_AND_GCLB" + }, + "labels": { 'deployment-tool': 'console-cloud', 'env': 'production' } + }, + { + "name": "projects/my-test-project/locations/us-central1/functions/function-3", + "environment": "GEN_1", + "state": "ACTIVE", + "runtime": "nodejs14", + "ingressSettings": "ALLOW_ALL" + } +]; + +const createCache = (list, err) => { + return { + functionsv2: { + list: { + 'us-central1': { + err: err, + data: list + } + } + } + } +}; + +describe('cloudFunctionLabelsAdded', function () { + describe('run', function () { + it('should give passing result if no Google 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 Cloud Function has labels added', 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('labels found for Cloud Function'); + 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 Cloud Function does not have labels added', 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('does not have any labels'); + expect(results[0].region).to.equal('us-central1'); + done(); + }; + + const cache = createCache( + [functions[0]], + null + ); + + plugin.run(cache, {}, callback); + }); + + it('should not check Gen 1 functions in v2 API response', function (done) { + const callback = (err, results) => { + expect(results.length).to.equal(0); + done(); + }; + + const cache = createCache( + [functions[2]], + null + ); + + plugin.run(cache, {}, callback); + }); + + }) +}); + diff --git a/plugins/google/cloudfunctionsv2/cloudFunctionV2OldRuntime.js b/plugins/google/cloudfunctionsv2/cloudFunctionV2OldRuntime.js new file mode 100644 index 0000000000..492dbd3cfc --- /dev/null +++ b/plugins/google/cloudfunctionsv2/cloudFunctionV2OldRuntime.js @@ -0,0 +1,129 @@ +var async = require('async'); +var helpers = require('../../../helpers/google'); + +module.exports = { + title: 'Cloud Function V2 Old Runtimes', + category: 'Cloud Functions', + domain: 'Compute', + severity: 'Medium', + description: 'Ensure Cloud Functions V2 are not using deprecated runtime versions.', + more_info: 'Cloud Functions V2 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 V2 to use latest versions.', + apis: ['functionsv2:list'], + settings: { + function_runtime_fail: { + name: 'Cloud Function V2 Runtime Fail', + description: 'Return a failing result for Cloud Function V2 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, + ['functionsv2', '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) return; + + if (!func.environment || func.environment !== 'GEN_2') return; + + let buildConfig = func.buildConfig || {}; + let runtime = buildConfig.runtime; + + if (!runtime) { + helpers.addResult(results, 2, + 'Cloud Function does not have a runtime configured', region, func.name); + return; + } + + var deprecatedRuntime = deprecatedRuntimes.filter((d) => { + return d.id == runtime; + }); + + var version = 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); + }); + } +}; + diff --git a/plugins/google/cloudfunctionsv2/cloudFunctionV2OldRuntime.spec.js b/plugins/google/cloudfunctionsv2/cloudFunctionV2OldRuntime.spec.js new file mode 100644 index 0000000000..8aa9953135 --- /dev/null +++ b/plugins/google/cloudfunctionsv2/cloudFunctionV2OldRuntime.spec.js @@ -0,0 +1,168 @@ +var expect = require('chai').expect; +var plugin = require('./cloudFunctionV2OldRuntime'); + + +const functions = [ + { + "name": "projects/my-test-project/locations/us-central1/functions/function-1", + "environment": "GEN_2", + "state": "ACTIVE", + "updateTime": "2021-09-24T06:18:15.265Z", + "buildConfig": { + "runtime": "nodejs14", + "entryPoint": "helloWorld" + }, + "serviceConfig": { + "serviceAccountEmail": "test@test-project.iam.gserviceaccount.com", + "ingressSettings": "ALLOW_ALL" + } + }, + { + "name": "projects/my-test-project/locations/us-central1/functions/function-2", + "environment": "GEN_2", + "state": "ACTIVE", + "updateTime": "2021-09-24T06:18:15.265Z", + "buildConfig": { + "runtime": "python312", + "entryPoint": "main" + }, + "serviceConfig": { + "serviceAccountEmail": "test@test-project.iam.gserviceaccount.com", + "ingressSettings": "ALLOW_INTERNAL_AND_GCLB" + }, + "labels": { 'deployment-tool': 'console-cloud' } + }, + { + "name": "projects/my-test-project/locations/us-central1/functions/function-3", + "environment": "GEN_2", + "state": "ACTIVE", + "updateTime": "2021-09-24T06:18:15.265Z", + "serviceConfig": { + "serviceAccountEmail": "test@test-project.iam.gserviceaccount.com" + } + }, + { + "name": "projects/my-test-project/locations/us-central1/functions/function-4", + "environment": "GEN_1", + "state": "ACTIVE", + "runtime": "nodejs14" + } +]; + +const createCache = (list, err) => { + return { + functionsv2: { + list: { + 'us-central1': { + err: err, + data: list + } + } + } + } +}; + +describe('cloudFunctionOldRuntime', function () { + describe('run', function () { + it('should give passing result if no Cloud Functions V2 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 Cloud Function is using latest 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 Cloud Function is using deprecated runtime version', 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); + }); + + it('should give failing result if Cloud Function does not have a runtime configured', 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('Cloud Function does not have a runtime configured'); + expect(results[0].region).to.equal('us-central1'); + done(); + }; + + const cache = createCache( + [functions[2]], + null + ); + + plugin.run(cache, {}, callback); + }); + + it('should not check Gen 1 functions in v2 API response', function (done) { + const callback = (err, results) => { + expect(results.length).to.equal(0); + done(); + }; + + const cache = createCache( + [functions[3]], + null + ); + + plugin.run(cache, {}, callback); + }); + + }) +}); + diff --git a/plugins/google/cloudfunctionsv2/cloudFunctionV2VPCConnector.js b/plugins/google/cloudfunctionsv2/cloudFunctionV2VPCConnector.js new file mode 100644 index 0000000000..b522ee44b6 --- /dev/null +++ b/plugins/google/cloudfunctionsv2/cloudFunctionV2VPCConnector.js @@ -0,0 +1,67 @@ +var async = require('async'); +var helpers = require('../../../helpers/google'); + +module.exports = { + title: 'Cloud Function V2 Serverless VPC Access', + category: 'Cloud Functions', + domain: 'Serverless', + severity: 'High', + description: 'Ensure Cloud Functions V2 are allowed to access only VPC resources.', + more_info: 'Cloud Functions V2 may require to connect directly to Compute Engine VM instances, Memorystore instances, Cloud SQL instances, and any other resources. It is a best practice to send requests to these resources using an internal IP address by connecting to VPC network using "Serverless VPC Access" configuration.', + link: 'https://cloud.google.com/functions/docs/networking/connecting-vpc#create-connector', + recommended_action: 'Ensure all Cloud Functions V2 are using serverless VPC connectors.', + apis: ['functionsv2:list'], + realtime_triggers: ['functions.CloudFunctionsService.UpdateFunction', 'functions.CloudFunctionsService.CreateFunction', 'functions.CloudFunctionsService.DeleteFunction'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + var regions = helpers.regions(); + + async.each(regions.functions, (region, rcb) => { + var functions = helpers.addSource(cache, source, + ['functionsv2', '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) return; + + if (!func.environment || func.environment !== 'GEN_2') return; + + let serviceConfig = func.serviceConfig || {}; + let vpcConnector = serviceConfig.vpcConnector; + let vpcConnectorEgressSettings = serviceConfig.vpcConnectorEgressSettings; + + if (vpcConnector) { + if (vpcConnectorEgressSettings && vpcConnectorEgressSettings.toUpperCase() === 'ALL_TRAFFIC') { + helpers.addResult(results, 0, + 'Cloud Function is using a VPC Connector to route all traffic', region, func.name); + } else { + helpers.addResult(results, 2, + 'Cloud Function is using a VPC Connector for requests to private IPs only', region, func.name); + } + } else { + helpers.addResult(results, 2, + 'Cloud Function is not configured with Serverless VPC Access', region, func.name); + } + }); + + rcb(); + }, function() { + callback(null, results, source); + }); + } +}; + diff --git a/plugins/google/cloudfunctionsv2/cloudFunctionV2VPCConnector.spec.js b/plugins/google/cloudfunctionsv2/cloudFunctionV2VPCConnector.spec.js new file mode 100644 index 0000000000..13ed92c6aa --- /dev/null +++ b/plugins/google/cloudfunctionsv2/cloudFunctionV2VPCConnector.spec.js @@ -0,0 +1,176 @@ +var expect = require('chai').expect; +var plugin = require('./cloudFunctionV2VPCConnector'); + + +const functions = [ + { + "name": "projects/my-test-project/locations/us-central1/functions/function-1", + "environment": "GEN_2", + "state": "ACTIVE", + "updateTime": "2021-09-24T06:18:15.265Z", + "buildConfig": { + "runtime": "nodejs20", + "entryPoint": "helloWorld" + }, + "serviceConfig": { + "serviceAccountEmail": "test@test-project.iam.gserviceaccount.com", + "ingressSettings": "ALLOW_ALL" + } + }, + { + "name": "projects/my-test-project/locations/us-central1/functions/function-2", + "environment": "GEN_2", + "state": "ACTIVE", + "updateTime": "2021-09-24T06:18:15.265Z", + "buildConfig": { + "runtime": "nodejs20", + "entryPoint": "helloWorld" + }, + "serviceConfig": { + "serviceAccountEmail": "test@test-project.iam.gserviceaccount.com", + "ingressSettings": "ALLOW_INTERNAL_AND_GCLB", + "vpcConnector": "projects/my-test-project/locations/us-central1/connectors/cloud-func-connector", + "vpcConnectorEgressSettings": "ALL_TRAFFIC" + } + }, + { + "name": "projects/my-test-project/locations/us-central1/functions/function-3", + "environment": "GEN_2", + "state": "ACTIVE", + "updateTime": "2021-09-24T06:18:15.265Z", + "buildConfig": { + "runtime": "nodejs20", + "entryPoint": "helloWorld" + }, + "serviceConfig": { + "serviceAccountEmail": "test@test-project.iam.gserviceaccount.com", + "vpcConnector": "projects/my-test-project/locations/us-central1/connectors/cloud-func-connector", + "vpcConnectorEgressSettings": "PRIVATE_RANGES_ONLY" + } + }, + { + "name": "projects/my-test-project/locations/us-central1/functions/function-4", + "environment": "GEN_1", + "state": "ACTIVE", + "runtime": "nodejs14", + "vpcConnector": "projects/my-test-project/locations/us-central1/connectors/cloud-func-connector" + } +]; + +const createCache = (list, err) => { + return { + functionsv2: { + list: { + 'us-central1': { + err: err, + data: list + } + } + } + } +}; + +describe('serverlessVPCAccess', function () { + describe('run', function () { + it('should give passing result if no Cloud Functions V2 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 Cloud Function is using a VPC Connector to route all traffic', 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 using a VPC Connector to route all traffic'); + 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 Cloud Function is using a VPC Connector for requests to private IPs only', 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('Cloud Function is using a VPC Connector for requests to private IPs only'); + expect(results[0].region).to.equal('us-central1'); + done(); + }; + + const cache = createCache( + [functions[2]], + null + ); + + plugin.run(cache, {}, callback); + }); + + it('should give failing result if Cloud Function is not configured with Serverless VPC Access', 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('Cloud Function is not configured with Serverless VPC Access'); + expect(results[0].region).to.equal('us-central1'); + done(); + }; + + const cache = createCache( + [functions[0]], + null + ); + + plugin.run(cache, {}, callback); + }); + + it('should not check Gen 1 functions in v2 API response', function (done) { + const callback = (err, results) => { + expect(results.length).to.equal(0); + done(); + }; + + const cache = createCache( + [functions[3]], + null + ); + + plugin.run(cache, {}, callback); + }); + + }) +}); + From fa21b9a81cfdece1c887144bec514ef2765d87b6 Mon Sep 17 00:00:00 2001 From: HamzaAziz-1 Date: Fri, 28 Nov 2025 16:50:33 +0500 Subject: [PATCH 4/6] update plugin --- .../cloudFunctionV2HttpsOnly.js | 38 ------------------- 1 file changed, 38 deletions(-) diff --git a/plugins/google/cloudfunctionsv2/cloudFunctionV2HttpsOnly.js b/plugins/google/cloudfunctionsv2/cloudFunctionV2HttpsOnly.js index 08963eb5ef..00cecfef9f 100644 --- a/plugins/google/cloudfunctionsv2/cloudFunctionV2HttpsOnly.js +++ b/plugins/google/cloudfunctionsv2/cloudFunctionV2HttpsOnly.js @@ -11,11 +11,6 @@ module.exports = { link: 'https://cloud.google.com/functions/docs/writing/http', recommended_action: 'Ensure that your Google Cloud Functions V2 always require HTTPS.', apis: ['functionsv2:list'], - remediation_min_version: '202207282132', - remediation_description: 'All Google Cloud Functions V2 will be configured to require HTTPS for HTTP invocations.', - apis_remediate: ['functionsv2:list', 'projects:get'], - actions: {remediate:['CloudFunctionsService.UpdateFunction'], rollback:['CloudFunctionsService.UpdateFunction']}, - permissions: {remediate: ['cloudfunctions.functions.update'], rollback: ['cloudfunctions.functions.create']}, realtime_triggers: ['functions.CloudFunctionsService.UpdateFunction','functions.CloudFunctionsService.DeleteFunction', 'functions.CloudFunctionsService.CreateFunction'], run: function(cache, settings, callback) { @@ -65,39 +60,6 @@ module.exports = { }, function() { callback(null, results, source); }); - }, - remediate: function(config, cache, settings, resource, callback) { - var remediation_file = settings.remediation_file; - - // inputs specific to the plugin - var pluginName = 'httpTriggerRequireHttps'; - var baseUrl = 'https://cloudfunctions.googleapis.com/v2/{resource}?updateMask=serviceConfig.securityLevel'; - var method = 'PATCH'; - var putCall = this.actions.remediate; - - // create the params necessary for the remediation - var body = { - serviceConfig: { - securityLevel: 'SECURE_ALWAYS' - } - }; - // logging - remediation_file['pre_remediate']['actions'][pluginName][resource] = { - 'httpTriggerRequireHttps': 'Disabled' - }; - - helpers.remediatePlugin(config, method, body, baseUrl, resource, remediation_file, putCall, pluginName, function(err, action) { - if (err) return callback(err); - if (action) action.action = putCall; - - - remediation_file['post_remediate']['actions'][pluginName][resource] = action; - remediation_file['remediate']['actions'][pluginName][resource] = { - 'Action': 'Enabled' - }; - - callback(null, action); - }); } }; From 3bc035442d03c339d64725f87ea7d3480068eb2b Mon Sep 17 00:00:00 2001 From: HamzaAziz-1 Date: Mon, 1 Dec 2025 13:50:19 +0500 Subject: [PATCH 5/6] update plugin name --- exports.js | 2 +- ...aultServiceAccount.js => functionV2DefaultServiceAccount.js} | 0 ...eAccount.spec.js => functionV2DefaultServiceAccount.spec.js} | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename plugins/google/cloudfunctionsv2/{cloudFunctionV2DefaultServiceAccount.js => functionV2DefaultServiceAccount.js} (100%) rename plugins/google/cloudfunctionsv2/{cloudFunctionV2DefaultServiceAccount.spec.js => functionV2DefaultServiceAccount.spec.js} (98%) diff --git a/exports.js b/exports.js index ae68dfbb91..29190a83d1 100644 --- a/exports.js +++ b/exports.js @@ -1626,7 +1626,7 @@ module.exports = { 'cloudFunctionsPrivilegeAnalysis': require(__dirname + '/plugins/google/cloudfunctions/cloudFunctionsPrivilegeAnalysis.js'), 'cloudFunctionV2HttpsOnly' : require(__dirname + '/plugins/google/cloudfunctionsv2/cloudFunctionV2HttpsOnly.js'), - 'cloudFunctionV2DefaultServiceAccount': require(__dirname + '/plugins/google/cloudfunctionsv2/cloudFunctionV2DefaultServiceAccount.js'), + 'functionV2DefaultServiceAccount': require(__dirname + '/plugins/google/cloudfunctionsv2/functionV2DefaultServiceAccount.js'), 'cloudFunctionV2IngressSettings': require(__dirname + '/plugins/google/cloudfunctionsv2/cloudFunctionV2IngressSettings.js'), 'cloudFunctionV2LabelsAdded' : require(__dirname + '/plugins/google/cloudfunctionsv2/cloudFunctionV2LabelsAdded.js'), 'cloudFunctionV2OldRuntime' : require(__dirname + '/plugins/google/cloudfunctionsv2/cloudFunctionV2OldRuntime.js'), diff --git a/plugins/google/cloudfunctionsv2/cloudFunctionV2DefaultServiceAccount.js b/plugins/google/cloudfunctionsv2/functionV2DefaultServiceAccount.js similarity index 100% rename from plugins/google/cloudfunctionsv2/cloudFunctionV2DefaultServiceAccount.js rename to plugins/google/cloudfunctionsv2/functionV2DefaultServiceAccount.js diff --git a/plugins/google/cloudfunctionsv2/cloudFunctionV2DefaultServiceAccount.spec.js b/plugins/google/cloudfunctionsv2/functionV2DefaultServiceAccount.spec.js similarity index 98% rename from plugins/google/cloudfunctionsv2/cloudFunctionV2DefaultServiceAccount.spec.js rename to plugins/google/cloudfunctionsv2/functionV2DefaultServiceAccount.spec.js index a83f8f7e83..0682572761 100644 --- a/plugins/google/cloudfunctionsv2/cloudFunctionV2DefaultServiceAccount.spec.js +++ b/plugins/google/cloudfunctionsv2/functionV2DefaultServiceAccount.spec.js @@ -1,5 +1,5 @@ var expect = require('chai').expect; -var plugin = require('./cloudFunctionV2DefaultServiceAccount'); +var plugin = require('./functionV2DefaultServiceAccount'); const functions = [ From 436a4f3d8f52c6ff90885e901eace8e41ec133fe Mon Sep 17 00:00:00 2001 From: alphadev4 <113519745+alphadev4@users.noreply.github.com> Date: Tue, 2 Dec 2025 15:35:03 +0500 Subject: [PATCH 6/6] Apply suggestions from code review --- plugins/google/cloudfunctionsv2/cloudFunctionV2OldRuntime.js | 2 +- plugins/google/cloudfunctionsv2/cloudFunctionV2VPCConnector.js | 2 +- .../google/cloudfunctionsv2/functionV2DefaultServiceAccount.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/google/cloudfunctionsv2/cloudFunctionV2OldRuntime.js b/plugins/google/cloudfunctionsv2/cloudFunctionV2OldRuntime.js index 492dbd3cfc..424fd12b6d 100644 --- a/plugins/google/cloudfunctionsv2/cloudFunctionV2OldRuntime.js +++ b/plugins/google/cloudfunctionsv2/cloudFunctionV2OldRuntime.js @@ -6,7 +6,7 @@ module.exports = { category: 'Cloud Functions', domain: 'Compute', severity: 'Medium', - description: 'Ensure Cloud Functions V2 are not using deprecated runtime versions.', + description: 'Ensure that Cloud Functions V2 are not using deprecated runtime versions.', more_info: 'Cloud Functions V2 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 V2 to use latest versions.', diff --git a/plugins/google/cloudfunctionsv2/cloudFunctionV2VPCConnector.js b/plugins/google/cloudfunctionsv2/cloudFunctionV2VPCConnector.js index b522ee44b6..075a94fb49 100644 --- a/plugins/google/cloudfunctionsv2/cloudFunctionV2VPCConnector.js +++ b/plugins/google/cloudfunctionsv2/cloudFunctionV2VPCConnector.js @@ -6,7 +6,7 @@ module.exports = { category: 'Cloud Functions', domain: 'Serverless', severity: 'High', - description: 'Ensure Cloud Functions V2 are allowed to access only VPC resources.', + description: 'Ensure that Cloud Functions V2 are allowed to access only VPC resources.', more_info: 'Cloud Functions V2 may require to connect directly to Compute Engine VM instances, Memorystore instances, Cloud SQL instances, and any other resources. It is a best practice to send requests to these resources using an internal IP address by connecting to VPC network using "Serverless VPC Access" configuration.', link: 'https://cloud.google.com/functions/docs/networking/connecting-vpc#create-connector', recommended_action: 'Ensure all Cloud Functions V2 are using serverless VPC connectors.', diff --git a/plugins/google/cloudfunctionsv2/functionV2DefaultServiceAccount.js b/plugins/google/cloudfunctionsv2/functionV2DefaultServiceAccount.js index a212cb5934..cceeadedb9 100644 --- a/plugins/google/cloudfunctionsv2/functionV2DefaultServiceAccount.js +++ b/plugins/google/cloudfunctionsv2/functionV2DefaultServiceAccount.js @@ -6,7 +6,7 @@ module.exports = { category: 'Cloud Functions', domain: 'Serverless', severity: 'Medium', - description: 'Ensures that Cloud Functions V2 are not using the default service account.', + description: 'Ensure that Cloud Functions V2 are not using the default service account.', more_info: 'Using the default service account for Cloud Functions V2 can lead to privilege escalation and overly permissive access. It is recommended to use a user-managed service account for each function in a project instead of the default service account. A managed service account allows more precise access control by granting only the necessary permissions through Identity and Access Management (IAM).', link: 'https://cloud.google.com/functions/docs/securing/function-identity', recommended_action: 'Ensure that no Cloud Functions V2 are using the default service account.',