Skip to content

Commit 3fbb18b

Browse files
authored
Added Google Spanner Instance Node Count plugin and test cases (aquasecurity#813)
1 parent 6de4397 commit 3fbb18b

File tree

6 files changed

+174
-3
lines changed

6 files changed

+174
-3
lines changed

collectors/google/collector.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,13 @@ var calls = {
6161
location: null
6262
}
6363
},
64+
spanner: {
65+
list: {
66+
api: 'spanner',
67+
version: 'v1',
68+
parent: 'projects'
69+
}
70+
},
6471
manyApi: true,
6572
},
6673
instanceGroups: {
@@ -204,7 +211,6 @@ var calls = {
204211
api: 'pubsub',
205212
version: 'v1',
206213
parent: 'project'
207-
208214
}
209215
}
210216
};

exports.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -779,7 +779,9 @@ module.exports = {
779779

780780
'datasetAllUsersPolicy' : require(__dirname + '/plugins/google/bigquery/datasetAllUsersPolicy.js'),
781781

782-
'topicEncryption' : require(__dirname + '/plugins/google/pubsub/topicEncryption.js')
782+
'topicEncryption' : require(__dirname + '/plugins/google/pubsub/topicEncryption.js'),
783+
784+
'instanceNodeCount' : require(__dirname + '/plugins/google/spanner/instanceNodeCount.js')
783785
},
784786
alibaba: {
785787
'passwordMinLength' : require(__dirname + '/plugins/alibaba/ram/passwordMinLength.js'),

helpers/google/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,9 @@ var execute = function(LocalGoogleConfig, collection, service, callObj, callKey,
382382
} else if (callObj.parent && callObj.parent === 'project') {
383383
parentParams = {auth: callObj.params.auth, project: callObj.params.parent};
384384
executor['projects'][service][callKey](parentParams, LocalGoogleConfig, executorCb);
385+
} else if (callObj.parent && callObj.parent === 'projects') {
386+
parentParams = {auth: callObj.params.auth, parent: `projects/${callObj.params.project}`};
387+
executor['projects'][service][callKey](parentParams, LocalGoogleConfig, executorCb);
385388
} else if (callObj.parent) {
386389
parentParams = {auth: callObj.params.auth, parent: callObj.params.parent};
387390
executor['projects'][service][callKey](parentParams, LocalGoogleConfig, executorCb);

helpers/google/regions.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ module.exports = {
5858
buckets: ['global'],
5959
instances: {
6060
compute: regions,
61-
sql: ['global']
61+
sql: ['global'],
62+
spanner: ['global']
6263
},
6364
networks: ['global'],
6465
backendServices: ['global'],
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
var async = require('async');
2+
var helpers = require('../../../helpers/google');
3+
4+
module.exports = {
5+
title: 'Spanner Instance Node Count',
6+
category: 'Spanner',
7+
description: 'Ensure than node count for Spanner instances is not above allowed count.',
8+
more_info: 'The number of provisioned Cloud Spanner instance nodes must be under desired limit to avoid reaching the limit and exceeding the set budget.',
9+
link: 'https://cloud.google.com/spanner/docs/instances',
10+
recommended_action: 'Modify Spanner instances to decrease number of nodes',
11+
apis: ['instances:spanner:list'],
12+
settings: {
13+
spanner_allowed_instance_node_count: {
14+
name: 'Spanner Allowed Instance Node Count',
15+
description: 'The number of nodes allowed per one Spanner instance',
16+
regex: '^.*$',
17+
default: '20'
18+
}
19+
},
20+
21+
run: function(cache, settings, callback) {
22+
var results = [];
23+
var source = {};
24+
var regions = helpers.regions();
25+
26+
var config = {
27+
spanner_allowed_instance_node_count: parseInt(settings.spanner_allowed_instance_node_count || this.settings.spanner_allowed_instance_node_count.default)
28+
};
29+
30+
async.each(regions.instances.spanner, function(region, rcb){
31+
let instances = helpers.addSource(cache, source,
32+
['instances', 'spanner', 'list', region]);
33+
34+
if (!instances) return rcb();
35+
36+
if (instances.err || !instances.data) {
37+
helpers.addResult(results, 3, 'Unable to query Spanner instances: ' + helpers.addError(instances), region, null, null, instances.err);
38+
return rcb();
39+
}
40+
41+
if (!instances.data.length) {
42+
helpers.addResult(results, 0, 'No Spanner instances found', region);
43+
return rcb();
44+
}
45+
46+
instances.data.forEach(spannerInstance => {
47+
if (!spannerInstance.name) return;
48+
49+
let nodeCount = spannerInstance.nodeCount;
50+
let resultStatus = (nodeCount <= config.spanner_allowed_instance_node_count) ? 0 : 2;
51+
52+
helpers.addResult(results, resultStatus,
53+
`Spanner instance has ${nodeCount} node of ${config.spanner_allowed_instance_node_count} limit`,
54+
region, spannerInstance.name);
55+
});
56+
57+
rcb();
58+
}, function(){
59+
// Global checking goes here
60+
callback(null, results, source);
61+
});
62+
}
63+
};
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
var expect = require('chai').expect;
2+
var plugin = require('./instanceNodeCount');
3+
4+
const createCache = (err, data) => {
5+
return {
6+
instances: {
7+
spanner: {
8+
list: {
9+
'global': {
10+
err: err,
11+
data: data
12+
}
13+
}
14+
}
15+
}
16+
}
17+
};
18+
19+
describe('instanceNodeCount', function () {
20+
describe('run', function () {
21+
it('should give unknown result if error while querying Spanner instances', function (done) {
22+
const callback = (err, results) => {
23+
expect(results.length).to.be.above(0);
24+
expect(results[0].status).to.equal(3);
25+
expect(results[0].message).to.include('Unable to query Spanner instances');
26+
expect(results[0].region).to.equal('global');
27+
done()
28+
};
29+
const cache = createCache(
30+
['error'],
31+
null,
32+
);
33+
plugin.run(cache, {}, callback);
34+
});
35+
it('should give passing result if no Spanner instances found', function (done) {
36+
const callback = (err, results) => {
37+
expect(results.length).to.be.above(0);
38+
expect(results[0].status).to.equal(0);
39+
expect(results[0].message).to.include('No Spanner instances found');
40+
expect(results[0].region).to.equal('global');
41+
done()
42+
};
43+
const cache = createCache(
44+
null,
45+
[],
46+
);
47+
plugin.run(cache, {}, callback);
48+
});
49+
it('should give passing result if instance has less nodes than allowed limit', function (done) {
50+
const callback = (err, results) => {
51+
expect(results.length).to.be.above(0);
52+
expect(results[0].status).to.equal(0);
53+
expect(results[0].message).to.include('Spanner instance has 1 node of 20 limit');
54+
expect(results[0].region).to.equal('global');
55+
done()
56+
};
57+
const cache = createCache(
58+
null,
59+
[
60+
{
61+
"name": "projects/test-aqua/instances/test-ins",
62+
"config": "projects/test-aqua/instanceConfigs/regional-us-east1",
63+
"displayName": "test-ins",
64+
"nodeCount": 1,
65+
"state": "READY",
66+
"processingUnits": 1000
67+
}
68+
],
69+
);
70+
plugin.run(cache, {}, callback);
71+
});
72+
it('should give failing result if instance has more nodes that allowed limit', function (done) {
73+
const callback = (err, results) => {
74+
expect(results.length).to.be.above(0);
75+
expect(results[0].status).to.equal(2);
76+
expect(results[0].message).to.include('Spanner instance has 21 node of 20 limit');
77+
expect(results[0].region).to.equal('global');
78+
done()
79+
};
80+
const cache = createCache(
81+
null,
82+
[
83+
{
84+
"name": "projects/test-aqua/instances/test-ins",
85+
"config": "projects/test-aqua/instanceConfigs/regional-asia1",
86+
"displayName": "test-ins",
87+
"nodeCount": 21,
88+
"state": "READY",
89+
"processingUnits": 1000
90+
}
91+
],
92+
);
93+
plugin.run(cache, {}, callback);
94+
});
95+
})
96+
});

0 commit comments

Comments
 (0)