Skip to content

Commit 12e87c0

Browse files
committed
addd cloud functions v2 plugins
1 parent 5f95cda commit 12e87c0

File tree

6 files changed

+316
-0
lines changed

6 files changed

+316
-0
lines changed

exports.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1622,6 +1622,9 @@ 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+
16251628
'computeAllowedExternalIPs' : require(__dirname + '/plugins/google/cloudresourcemanager/computeAllowedExternalIPs.js'),
16261629
'disableAutomaticIAMGrants' : require(__dirname + '/plugins/google/cloudresourcemanager/disableAutomaticIAMGrants.js'),
16271630
'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: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
var async = require('async');
2+
var helpers = require('../../../helpers/google');
3+
4+
module.exports = {
5+
title: 'HTTP Trigger Require HTTPS V2',
6+
category: 'Cloud Functions',
7+
domain: 'Serverless',
8+
severity: 'Medium',
9+
description: 'Ensure that Cloud Functions V2 are configured to require HTTPS for HTTP invocations.',
10+
more_info: 'You can make your Google Cloud Functions V2 calls secure by making sure that they require HTTPS.',
11+
link: 'https://cloud.google.com/functions/docs/writing/http',
12+
recommended_action: 'Ensure that your Google Cloud Functions V2 always require HTTPS.',
13+
apis: ['functionsv2:list'],
14+
remediation_min_version: '202207282132',
15+
remediation_description: 'All Google Cloud Functions V2 will be configured to require HTTPS for HTTP invocations.',
16+
apis_remediate: ['functionsv2:list', 'projects:get'],
17+
actions: {remediate:['CloudFunctionsService.UpdateFunction'], rollback:['CloudFunctionsService.UpdateFunction']},
18+
permissions: {remediate: ['cloudfunctions.functions.update'], rollback: ['cloudfunctions.functions.create']},
19+
realtime_triggers: ['functions.CloudFunctionsService.UpdateFunction','functions.CloudFunctionsService.DeleteFunction', 'functions.CloudFunctionsService.CreateFunction'],
20+
21+
run: function(cache, settings, callback) {
22+
var results = [];
23+
var source = {};
24+
var regions = helpers.regions();
25+
26+
async.each(regions.functions, (region, rcb) => {
27+
var functions = helpers.addSource(cache, source,
28+
['functionsv2', 'list', region]);
29+
30+
if (!functions) return rcb();
31+
32+
if (functions.err || !functions.data) {
33+
helpers.addResult(results, 3,
34+
'Unable to query for Google Cloud functions: ' + helpers.addError(functions), region, null, null, functions.err);
35+
return rcb();
36+
}
37+
38+
if (!functions.data.length) {
39+
helpers.addResult(results, 0, 'No Google Cloud functions found', region);
40+
return rcb();
41+
}
42+
43+
functions.data.forEach(funct => {
44+
if (!funct.name) return;
45+
46+
if (!funct.environment || funct.environment !== 'GEN_2') return;
47+
48+
let serviceConfig = funct.serviceConfig || {};
49+
50+
if (serviceConfig.uri) {
51+
if (serviceConfig.securityLevel && serviceConfig.securityLevel == 'SECURE_ALWAYS') {
52+
helpers.addResult(results, 0,
53+
'Cloud Function is configured to require HTTPS for HTTP invocations', region, funct.name);
54+
} else {
55+
helpers.addResult(results, 2,
56+
'Cloud Function is not configured to require HTTPS for HTTP invocations', region, funct.name);
57+
}
58+
} else {
59+
helpers.addResult(results, 0,
60+
'Cloud Function trigger type is not HTTP', region, funct.name);
61+
}
62+
});
63+
64+
rcb();
65+
}, function() {
66+
callback(null, results, source);
67+
});
68+
},
69+
remediate: function(config, cache, settings, resource, callback) {
70+
var remediation_file = settings.remediation_file;
71+
72+
// inputs specific to the plugin
73+
var pluginName = 'httpTriggerRequireHttps';
74+
var baseUrl = 'https://cloudfunctions.googleapis.com/v2/{resource}?updateMask=serviceConfig.securityLevel';
75+
var method = 'PATCH';
76+
var putCall = this.actions.remediate;
77+
78+
// create the params necessary for the remediation
79+
var body = {
80+
serviceConfig: {
81+
securityLevel: 'SECURE_ALWAYS'
82+
}
83+
};
84+
// logging
85+
remediation_file['pre_remediate']['actions'][pluginName][resource] = {
86+
'httpTriggerRequireHttps': 'Disabled'
87+
};
88+
89+
helpers.remediatePlugin(config, method, body, baseUrl, resource, remediation_file, putCall, pluginName, function(err, action) {
90+
if (err) return callback(err);
91+
if (action) action.action = putCall;
92+
93+
94+
remediation_file['post_remediate']['actions'][pluginName][resource] = action;
95+
remediation_file['remediate']['actions'][pluginName][resource] = {
96+
'Action': 'Enabled'
97+
};
98+
99+
callback(null, action);
100+
});
101+
}
102+
103+
};
104+
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
var expect = require('chai').expect;
2+
var plugin = require('./cloudFunctionV2HttpsOnly');
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": "test@test-project.iam.gserviceaccount.com",
17+
"uri": "https://us-central1-my-test-project.cloudfunctions.net/function-1",
18+
"securityLevel": "SECURE_OPTIONAL"
19+
}
20+
},
21+
{
22+
"name": "projects/my-test-project/locations/us-central1/functions/function-2",
23+
"environment": "GEN_2",
24+
"state": "ACTIVE",
25+
"updateTime": "2021-09-24T06:18:15.265Z",
26+
"buildConfig": {
27+
"runtime": "nodejs20",
28+
"entryPoint": "helloWorld"
29+
},
30+
"serviceConfig": {
31+
"serviceAccountEmail": "test@test-project.iam.gserviceaccount.com",
32+
"uri": "https://us-central1-my-test-project.cloudfunctions.net/function-2",
33+
"securityLevel": "SECURE_ALWAYS"
34+
}
35+
},
36+
{
37+
"name": "projects/my-test-project/locations/us-central1/functions/function-3",
38+
"environment": "GEN_2",
39+
"state": "ACTIVE",
40+
"updateTime": "2021-09-24T06:18:15.265Z",
41+
"buildConfig": {
42+
"runtime": "nodejs20",
43+
"entryPoint": "handleEvent"
44+
},
45+
"serviceConfig": {
46+
"serviceAccountEmail": "test@test-project.iam.gserviceaccount.com"
47+
}
48+
},
49+
{
50+
"name": "projects/my-test-project/locations/us-central1/functions/function-4",
51+
"environment": "GEN_1",
52+
"state": "ACTIVE",
53+
"runtime": "nodejs14",
54+
"httpsTrigger": {
55+
"url": "https://us-central1-my-test-project.cloudfunctions.net/function-4",
56+
"securityLevel": "SECURE_OPTIONAL"
57+
}
58+
}
59+
];
60+
61+
const createCache = (list, err) => {
62+
return {
63+
functionsv2: {
64+
list: {
65+
'us-central1': {
66+
err: err,
67+
data: list
68+
}
69+
}
70+
}
71+
}
72+
};
73+
74+
describe('httpTriggerRequireHttps', function () {
75+
describe('run', function () {
76+
it('should give passing result if no Cloud Functions V2 found', function (done) {
77+
const callback = (err, results) => {
78+
expect(results.length).to.be.above(0);
79+
expect(results[0].status).to.equal(0);
80+
expect(results[0].message).to.include('No Google Cloud functions found');
81+
expect(results[0].region).to.equal('us-central1');
82+
done()
83+
};
84+
85+
const cache = createCache(
86+
[],
87+
null
88+
);
89+
90+
plugin.run(cache, {}, callback);
91+
});
92+
93+
it('should give unknown result if unable to query for Google Cloud functions', function (done) {
94+
const callback = (err, results) => {
95+
expect(results.length).to.be.above(0);
96+
expect(results[0].status).to.equal(3);
97+
expect(results[0].message).to.include('Unable to query for Google Cloud functions');
98+
expect(results[0].region).to.equal('us-central1');
99+
done()
100+
};
101+
102+
const cache = createCache(
103+
[],
104+
{message: 'error'},
105+
);
106+
107+
plugin.run(cache, {}, callback);
108+
});
109+
110+
it('should give passing result if Cloud Function is configured to require HTTPS for HTTP invocations', function (done) {
111+
const callback = (err, results) => {
112+
expect(results.length).to.be.above(0);
113+
expect(results[0].status).to.equal(0);
114+
expect(results[0].message).to.include('Cloud Function is configured to require HTTPS for HTTP invocations');
115+
expect(results[0].region).to.equal('us-central1');
116+
done()
117+
};
118+
119+
const cache = createCache(
120+
[functions[1]],
121+
null
122+
);
123+
124+
plugin.run(cache, {}, callback);
125+
});
126+
127+
it('should give failing result if Cloud Function is not configured to require HTTPS for HTTP invocations', function (done) {
128+
const callback = (err, results) => {
129+
expect(results.length).to.be.above(0);
130+
expect(results[0].status).to.equal(2);
131+
expect(results[0].message).to.include('Cloud Function is not configured to require HTTPS for HTTP invocations');
132+
expect(results[0].region).to.equal('us-central1');
133+
done();
134+
};
135+
136+
const cache = createCache(
137+
[functions[0]],
138+
null
139+
);
140+
141+
plugin.run(cache, {}, callback);
142+
});
143+
144+
it('should give passing result if Cloud Function trigger type is not HTTP', function (done) {
145+
const callback = (err, results) => {
146+
expect(results.length).to.be.above(0);
147+
expect(results[0].status).to.equal(0);
148+
expect(results[0].message).to.include('Cloud Function trigger type is not HTTP');
149+
expect(results[0].region).to.equal('us-central1');
150+
done();
151+
};
152+
153+
const cache = createCache(
154+
[functions[2]],
155+
null
156+
);
157+
158+
plugin.run(cache, {}, callback);
159+
});
160+
161+
it('should not check Gen 1 functions in v2 API response', function (done) {
162+
const callback = (err, results) => {
163+
expect(results.length).to.equal(0);
164+
done();
165+
};
166+
167+
const cache = createCache(
168+
[functions[3]],
169+
null
170+
);
171+
172+
plugin.run(cache, {}, callback);
173+
});
174+
175+
})
176+
});
177+

0 commit comments

Comments
 (0)