diff --git a/exports.js b/exports.js index 215cd9c338..ac73444428 100644 --- a/exports.js +++ b/exports.js @@ -1622,6 +1622,11 @@ 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'), + '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'), '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/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/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); + }); + + }) +}); + 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); + }); + + }) +}); +