Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions exports.js
Original file line number Diff line number Diff line change
Expand Up @@ -1622,6 +1622,14 @@ 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'),
'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'),
'disableGuestAttributes' : require(__dirname + '/plugins/google/cloudresourcemanager/disableGuestAttributes.js'),
Expand Down
23 changes: 23 additions & 0 deletions helpers/google/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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}',
Expand Down
5 changes: 5 additions & 0 deletions helpers/google/regions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
4 changes: 4 additions & 0 deletions helpers/google/resources.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ module.exports = {
functions: {
list: 'name'
},
functionsv2: {
list: 'name',
getIamPolicy: 'name'
},
instanceGroups: {
aggregatedList: ''
},
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
}
};

Original file line number Diff line number Diff line change
@@ -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": "[email protected]",
"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": "[email protected]",
"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": "[email protected]"
}
];

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);
});

})
});

66 changes: 66 additions & 0 deletions plugins/google/cloudfunctionsv2/cloudFunctionV2HttpsOnly.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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'],
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);
});
}

};

Loading