hubot-aws-insight
Version:
A hubot scripts that shows you what's running on aws. Needs Python 3
306 lines (269 loc) • 8.52 kB
JavaScript
// Description
// A hubot scripts that shows our aws resources
//
// Configuration:
//
// Commands
// hubot aws costly
// hubot aws summary [include default] [exclude tf] [on account <account>]
// hubot aws grep [<search>] [on account <account>]
// hubot add aws account 123 nilo
// hubot forget aws account nilo
// hubot ls aws accounts
//
// Notes:
//
// Author:
// Farid Nouri Neshat <FaridN_SOAD@Yahoo.com>
;
const {spawn} = require('child_process');
const JSONStream = require('json-stream');
const PBatch = require('p-batch');
const S3Lambda = require('s3-lambda');
const sts = new (require('aws-sdk/clients/sts'));
const util = require('util');
const fs = require('fs');
const ColorHash = require('color-hash');
const colorHash = new ColorHash();
const lambda = new S3Lambda({});
const writeFile = util.promisify(fs.writeFile);
const skewFilePath = __dirname + '/../tmp/skew';
const awsFilePath = __dirname + '/../tmp/aws';
const namespace = 'aws-insight.accounts';
module.exports = function (robot) {
const createListCp = (arn, costly) => {
saveAccounts();
const args = [__dirname + '/../aws-list.py', '--arn', arn];
if (costly) {
args.push('--costly');
}
return spawn('python3', args, {
env: {
...process.env,
PYTHONPATH: __dirname + '/../pip',
SKEW_CONFIG: skewFilePath,
AWS_CONFIG_FILE: awsFilePath,
},
});
};
const getAccounts = () => robot.brain.get(namespace) || {};
const setAccount = (name, account) => {
const accounts = getAccounts();
accounts[name] = account;
saveAccounts(accounts);
};
const saveAccounts = (accounts) => {
if (accounts) {
robot.brain.set(namespace, accounts);
} else {
accounts = getAccounts();
}
writeSkewFile(accounts);
writeAwsFile(accounts);
function writeSkewFile(accounts) {
const skewFile = `---
accounts:${Object.entries(accounts).map(([ profile, no ]) => `
"${no}":
profile: ${profile}`).join('')}`;
return writeFile(skewFilePath, skewFile);
}
function writeAwsFile(accounts) {
const awsFile = Object.entries(accounts).map(([ profile, no ]) => {
return (profile === 'default') ? '[default]' : `[profile ${profile}]
role_arn = arn:aws:iam::${no}:role/nilo-v1
source_profile = default`;
}).join('\n');
writeFile(awsFilePath, awsFile);
}
};
sts.getCallerIdentity({}).promise().then(({ Account }) => {
setAccount('default', Account);
});
const run = ({ filterData, res, formatResult, account = '*', costly }) => {
const cp = createListCp(`arn:aws:*:*:${account}:*`, costly);
let firstMessagePromise =
robot.adapter.client.web.chat.postMessage(res.envelope.room,
'Searching...');
const text = "I'm still searching, but here's what I found so far:";
const groups = {};
let ts;
const batch = new PBatch(() => {
const attachments = [];
for (let service in groups) {
let result = `For service ${service}:\n`;
for (let region in groups[service]) {
result += `In region ${region || 'global'}:\n`;
for (let type in groups[service][region]) {
result += formatResult(groups[service][region][type], type) + '\n';
}
result += '\n';
}
attachments.push({
color: colorHash.hex(service),
text: result
});
}
robot.adapter.client.web.chat.update(ts, res.envelope.room, text,
{attachments});
// Needed for PBatch.
return [];
});
const update = () => batch.add().catch(console.error);
cp.stdout.pipe(JSONStream()).on('data', async resource => {
let [,,service, region, account, type, id] = resource.arn.split(/[:\/]/);
if (!id) {
id = type;
}
if (filterData &&
!filterData({ ...resource, service, region, account, type, id })) {
return;
}
if (!groups[service]) {
groups[service] = {};
}
if (!groups[service][region]) {
groups[service][region] = {};
}
groups[service][region][type] = (groups[service][region][type] || [])
.concat(id);
if (firstMessagePromise) {
const result = await firstMessagePromise;
ts = result.ts;
firstMessagePromise = null;
}
update();
}).on('end', () => {
res.send('I think that was all!');
});
const onError = msg => {
msg = msg.toString().trim();
console.error(msg);
if (msg) {
res.send('Something bad happened: ' + msg);
}
};
cp.stderr.on('data', onError);
cp.on('error', err => onError(err.message || err));
};
const getAccount = query => {
let name;
const rest = query.replace(/(on|for)? *account *(\S*)/,
($0, $1, $2) => {
name = $2;
return ' ';
}).trim();
return {
account: getAccounts()[name] || name,
rest,
}
};
robot.respond(/aws grep (.*?) */i, res => {
const { account, rest: search } = getAccount(res.match[1]);
run({
res,
account,
formatResult: (ids, type) =>
`There's the following ${type}(s): ${ids.join(',')}`,
filterData: ({data}) => data.includes(search),
});
});
robot.respond(/aws costly *(.*) */i, res => {
const { account } = getAccount(res.match[1]);
run({
account,
res,
formatResult: summaryFormat,
costly: true,
});
});
const summaryFormat = (ids, type) => `There's ${ids.length} ${type}`;
robot.respond(/aws summary *(.*) */i, async res => {
const { account, rest } = getAccount(res.match[1]);
const keywords = rest.match(/(include|exclude) +(default|tf)/g);
const args = {
default: false, // exclude default (by default)!
tf: true, // include tf (by default)
};
if (keywords) {
keywords.forEach(match => {
const [verb, subject] = match.split(/ +/);
args[subject] = verb === 'include';
});
}
const toExcludeIds = new Set();
if (!args.tf) {
await lambda
.context({
bucket: process.env.TERRAFORM_S3_BUCKET,
})
.forEach(object => {
try {
object = JSON.parse(object);
} catch (e) {
console.error("Can't parse the S3 object into JSON.");
return;
}
for (const module of object.modules) {
for (const key in module.resources) {
const {primary} = module.resources[key];
toExcludeIds.add(primary.attributes.arn || primary.id);
}
}
});
}
run({
res,
account,
formatResult: (ids, type) =>
`There's ${ids.length} ${type}`,
filterData: ({arn, data, id, type}) => {
let parsed;
const getData = () => parsed = parsed || JSON.parse(data);
if (!args.tf) {
if (toExcludeIds.has(arn) || toExcludeIds.has(id)) {
return false;
}
const data = getData();
if (data.Attachments && data.Attachments
.some(({ InstanceId }) => toExcludeIds.has(InstanceId))) {
return false;
}
}
if (!args.default) {
const data = getData();
return !(data.IsDefault || // For vpc and network acl
data.DefaultForAz || // For subnet
data.GroupName === 'default' || // for ec2 security group
data.DBSecurityGroupName === 'default' || // For rds security groups
type === 'policy' && data.Arn.split(':')[4] === 'aws' || // AWS managed policies
// For route tables
(data.Associations && data.Associations.some(({Main}) => Main)));
}
return true;
}
});
});
robot.respond(/add aws account *(\S*) *(\S*)/i, res => {
res.reply('Added the account.');
const [,a,b] = res.match;
let accNo, profile;
if (parseInt(a)) {
accNo = a;
profile = b;
} else {
accNo = b;
profile = a;
}
setAccount(profile, accNo);
});
robot.respond(/(list|ls|show) aws accounts?/i, res => {
res.reply(`Here's accounts: ${util.inspect(getAccounts())}`);
});
robot.respond(/forget aws account *(\S*)/i, res => {
const [,profile] = res.match;
const accounts = getAccounts();
delete accounts[profile];
saveAccounts(accounts);
res.reply(`Yes, master!`);
});
};