Skip to content

Commit 8b3f883

Browse files
committed
add cloudfunctions v2 plugin
1 parent 5f95cda commit 8b3f883

16 files changed

+1537
-0
lines changed

exports.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1622,6 +1622,14 @@ module.exports = {
16221622
'serverlessVPCAccess' : require(__dirname + '/plugins/google/cloudfunctions/serverlessVPCAccess.js'),
16231623
'cloudFunctionNetworkExposure' : require(__dirname + '/plugins/google/cloudfunctions/cloudFunctionNetworkExposure.js'),
16241624
'cloudFunctionsPrivilegeAnalysis': require(__dirname + '/plugins/google/cloudfunctions/cloudFunctionsPrivilegeAnalysis.js'),
1625+
1626+
'cloudFunctionV2HttpsOnly': require(__dirname + '/plugins/google/cloudfunctionsv2/cloudFunctionV2HttpsOnly.js'),
1627+
'cloudFunctionV2DefaultServiceAccount': require(__dirname + '/plugins/google/cloudfunctionsv2/cloudFunctionV2DefaultServiceAccount.js'),
1628+
'cloudFunctionV2IngressSettings': require(__dirname + '/plugins/google/cloudfunctionsv2/cloudFunctionV2IngressSettings.js'),
1629+
'cloudFunctionV2LabelsAdded': require(__dirname + '/plugins/google/cloudfunctionsv2/cloudFunctionV2LabelsAdded.js'),
1630+
'cloudFunctionV2OldRuntime': require(__dirname + '/plugins/google/cloudfunctionsv2/cloudFunctionV2OldRuntime.js'),
1631+
'cloudFunctionV2VPCConnector': require(__dirname + '/plugins/google/cloudfunctionsv2/cloudFunctionV2VPCConnector.js'),
1632+
16251633
'computeAllowedExternalIPs' : require(__dirname + '/plugins/google/cloudresourcemanager/computeAllowedExternalIPs.js'),
16261634
'disableAutomaticIAMGrants' : require(__dirname + '/plugins/google/cloudresourcemanager/disableAutomaticIAMGrants.js'),
16271635
'disableGuestAttributes' : require(__dirname + '/plugins/google/cloudresourcemanager/disableGuestAttributes.js'),

helpers/google/api.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,18 @@ var calls = {
390390
enabled: true
391391
}
392392
},
393+
functionsv2: {
394+
list: {
395+
url: 'https://cloudfunctions.googleapis.com/v2/projects/{projectId}/locations/{locationId}/functions',
396+
location: 'region',
397+
paginationKey: 'pageSize',
398+
pagination: true,
399+
dataFilterKey: 'functions'
400+
},
401+
sendIntegration: {
402+
enabled: true
403+
}
404+
},
393405
keyRings: {
394406
list: {
395407
url: 'https://cloudkms.googleapis.com/v1/projects/{projectId}/locations/{locationId}/keyRings',
@@ -850,6 +862,17 @@ var postcalls = {
850862
properties: ['name']
851863
}
852864
},
865+
functionsv2: {
866+
getIamPolicy: {
867+
url: 'https://cloudfunctions.googleapis.com/v2/{name}:getIamPolicy',
868+
location: null,
869+
method: 'POST',
870+
reliesOnService: ['functionsv2'],
871+
reliesOnCall: ['list'],
872+
properties: ['name'],
873+
body: { options: { requestedPolicyVersion: 3 } }
874+
}
875+
},
853876
jobs: {
854877
get: { //https://dataflow.googleapis.com/v1b3/projects/{projectId}/jobs/{jobId}
855878
url: 'https://dataflow.googleapis.com/v1b3/projects/{projectId}/locations/{locationId}/jobs/{id}',

helpers/google/regions.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,11 @@ module.exports = {
112112
'europe-west1', 'europe-west2', 'europe-west3', 'europe-west6', 'europe-central2', 'asia-south1', 'asia-southeast1', 'asia-southeast2',
113113
'asia-east1', 'asia-east2', 'asia-northeast1', 'asia-northeast2', 'asia-northeast3', 'australia-southeast1'
114114
],
115+
functionsv2: [
116+
'us-east1', 'us-east4', 'us-west1', 'us-west2', 'us-west3', 'us-west4', 'us-central1', 'northamerica-northeast1', 'southamerica-east1',
117+
'europe-west1', 'europe-west2', 'europe-west3', 'europe-west6', 'europe-central2', 'asia-south1', 'asia-southeast1', 'asia-southeast2',
118+
'asia-east1', 'asia-east2', 'asia-northeast1', 'asia-northeast2', 'asia-northeast3', 'australia-southeast1'
119+
],
115120
cloudbuild: ['global', 'us-east1', 'us-east4', 'us-west2', 'us-west3', 'us-west4', 'us-central1', 'us-west1',
116121
'northamerica-northeast1', 'northamerica-northeast2', 'southamerica-east1', 'southamerica-west1', 'europe-west1', 'europe-west2',
117122
'europe-west3', 'europe-west4', 'europe-west6', 'europe-central2', 'europe-north1', 'asia-south1', 'asia-south2', 'asia-southeast1', 'asia-southeast2',

helpers/google/resources.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ module.exports = {
5252
functions: {
5353
list: 'name'
5454
},
55+
functionsv2: {
56+
list: 'name',
57+
getIamPolicy: 'name'
58+
},
5559
instanceGroups: {
5660
aggregatedList: ''
5761
},
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
var async = require('async');
2+
var helpers = require('../../../helpers/google');
3+
4+
module.exports = {
5+
title: 'Cloud Function V2 Default Service Account',
6+
category: 'Cloud Functions',
7+
domain: 'Serverless',
8+
severity: 'Medium',
9+
description: 'Ensures that Cloud Functions V2 are not using the default service account.',
10+
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).',
11+
link: 'https://cloud.google.com/functions/docs/securing/function-identity',
12+
recommended_action: 'Ensure that no Cloud Functions V2 are using the default service account.',
13+
apis: ['functionsv2:list'],
14+
realtime_triggers: ['functions.CloudFunctionsService.UpdateFunction', 'functions.CloudFunctionsService.CreateFunction', 'functions.CloudFunctionsService.DeleteFunction'],
15+
16+
run: function(cache, settings, callback) {
17+
var results = [];
18+
var source = {};
19+
var regions = helpers.regions();
20+
21+
async.each(regions.functions, (region, rcb) => {
22+
var functions = helpers.addSource(cache, source,
23+
['functionsv2', 'list', region]);
24+
25+
if (!functions) return rcb();
26+
27+
if (functions.err || !functions.data) {
28+
helpers.addResult(results, 3,
29+
'Unable to query for Google Cloud functions: ' + helpers.addError(functions), region, null, null, functions.err);
30+
return rcb();
31+
}
32+
33+
if (!functions.data.length) {
34+
helpers.addResult(results, 0, 'No Google Cloud functions found', region);
35+
return rcb();
36+
}
37+
38+
functions.data.forEach(func => {
39+
if (!func.name) return;
40+
41+
if (!func.environment || func.environment !== 'GEN_2') return;
42+
43+
let serviceAccountEmail = func.serviceConfig && func.serviceConfig.serviceAccountEmail
44+
? func.serviceConfig.serviceAccountEmail
45+
: null;
46+
47+
if (serviceAccountEmail && serviceAccountEmail.endsWith('@appspot.gserviceaccount.com')) {
48+
helpers.addResult(results, 2,
49+
'Cloud Function is using default service account', region, func.name);
50+
} else if (serviceAccountEmail) {
51+
helpers.addResult(results, 0,
52+
'Cloud Function is not using default service account', region, func.name);
53+
} else {
54+
helpers.addResult(results, 2,
55+
'Cloud Function does not have a service account configured', region, func.name);
56+
}
57+
});
58+
59+
rcb();
60+
}, function() {
61+
callback(null, results, source);
62+
});
63+
}
64+
};
65+
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
var expect = require('chai').expect;
2+
var plugin = require('./cloudFunctionV2DefaultServiceAccount');
3+
4+
5+
const functions = [
6+
{
7+
"name": "projects/my-test-project/locations/us-central1/functions/function-1",
8+
"environment": "GEN_2",
9+
"state": "ACTIVE",
10+
"updateTime": "2021-09-24T06:18:15.265Z",
11+
"buildConfig": {
12+
"runtime": "nodejs20",
13+
"entryPoint": "helloWorld"
14+
},
15+
"serviceConfig": {
16+
"serviceAccountEmail": "aqua@appspot.gserviceaccount.com",
17+
"ingressSettings": "ALLOW_ALL"
18+
}
19+
},
20+
{
21+
"name": "projects/my-test-project/locations/us-central1/functions/function-2",
22+
"environment": "GEN_2",
23+
"state": "ACTIVE",
24+
"updateTime": "2021-09-24T06:18:15.265Z",
25+
"buildConfig": {
26+
"runtime": "nodejs20",
27+
"entryPoint": "helloWorld"
28+
},
29+
"serviceConfig": {
30+
"serviceAccountEmail": "custom-sa@my-test-project.iam.gserviceaccount.com",
31+
"ingressSettings": "ALLOW_INTERNAL_AND_GCLB"
32+
},
33+
"labels": { 'deployment-tool': 'console-cloud' }
34+
},
35+
{
36+
"name": "projects/my-test-project/locations/us-central1/functions/function-3",
37+
"environment": "GEN_2",
38+
"state": "ACTIVE",
39+
"updateTime": "2021-09-24T06:18:15.265Z",
40+
"buildConfig": {
41+
"runtime": "nodejs20",
42+
"entryPoint": "helloWorld"
43+
},
44+
"serviceConfig": {
45+
"ingressSettings": "ALLOW_INTERNAL_ONLY"
46+
}
47+
},
48+
{
49+
"name": "projects/my-test-project/locations/us-central1/functions/function-4",
50+
"environment": "GEN_1",
51+
"state": "ACTIVE",
52+
"runtime": "nodejs14",
53+
"serviceAccountEmail": "aqua@appspot.gserviceaccount.com"
54+
}
55+
];
56+
57+
const createCache = (list, err) => {
58+
return {
59+
functionsv2: {
60+
list: {
61+
'us-central1': {
62+
err: err,
63+
data: list
64+
}
65+
}
66+
}
67+
}
68+
};
69+
70+
describe('functionDefaultServiceAccount', function () {
71+
describe('run', function () {
72+
it('should give passing result if no Cloud Functions V2 found', function (done) {
73+
const callback = (err, results) => {
74+
expect(results.length).to.be.above(0);
75+
expect(results[0].status).to.equal(0);
76+
expect(results[0].message).to.include('No Google Cloud functions found');
77+
expect(results[0].region).to.equal('us-central1');
78+
done()
79+
};
80+
81+
const cache = createCache(
82+
[],
83+
null
84+
);
85+
86+
plugin.run(cache, {}, callback);
87+
});
88+
89+
it('should give unknown result if unable to query for Google Cloud functions', function (done) {
90+
const callback = (err, results) => {
91+
expect(results.length).to.be.above(0);
92+
expect(results[0].status).to.equal(3);
93+
expect(results[0].message).to.include('Unable to query for Google Cloud functions');
94+
expect(results[0].region).to.equal('us-central1');
95+
done()
96+
};
97+
98+
const cache = createCache(
99+
[],
100+
{message: 'error'},
101+
);
102+
103+
plugin.run(cache, {}, callback);
104+
});
105+
106+
it('should give passing result if Cloud Function is not using default service account', function (done) {
107+
const callback = (err, results) => {
108+
expect(results.length).to.be.above(0);
109+
expect(results[0].status).to.equal(0);
110+
expect(results[0].message).to.include('Cloud Function is not using default service account');
111+
expect(results[0].region).to.equal('us-central1');
112+
done()
113+
};
114+
115+
const cache = createCache(
116+
[functions[1]],
117+
null
118+
);
119+
120+
plugin.run(cache, {}, callback);
121+
});
122+
123+
it('should give failing result if Cloud Function is using default service account', function (done) {
124+
const callback = (err, results) => {
125+
expect(results.length).to.be.above(0);
126+
expect(results[0].status).to.equal(2);
127+
expect(results[0].message).to.include('Cloud Function is using default service account');
128+
expect(results[0].region).to.equal('us-central1');
129+
done();
130+
};
131+
132+
const cache = createCache(
133+
[functions[0]],
134+
null
135+
);
136+
137+
plugin.run(cache, {}, callback);
138+
});
139+
140+
it('should give failing result if Cloud Function does not have a service account configured', function (done) {
141+
const callback = (err, results) => {
142+
expect(results.length).to.be.above(0);
143+
expect(results[0].status).to.equal(2);
144+
expect(results[0].message).to.include('Cloud Function does not have a service account configured');
145+
expect(results[0].region).to.equal('us-central1');
146+
done();
147+
};
148+
149+
const cache = createCache(
150+
[functions[2]],
151+
null
152+
);
153+
154+
plugin.run(cache, {}, callback);
155+
});
156+
157+
it('should not check Gen 1 functions in v2 API response', function (done) {
158+
const callback = (err, results) => {
159+
expect(results.length).to.equal(0);
160+
done();
161+
};
162+
163+
const cache = createCache(
164+
[functions[3]],
165+
null
166+
);
167+
168+
plugin.run(cache, {}, callback);
169+
});
170+
171+
})
172+
});
173+

0 commit comments

Comments
 (0)