Skip to content
Closed
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
1 change: 1 addition & 0 deletions exports.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ module.exports = {
'vpcEndpointExposed' : require(__dirname + '/plugins/aws/ec2/vpcEndpointExposed.js'),
'vpcEndpointCrossAccount' : require(__dirname + '/plugins/aws/ec2/vpcEndpointCrossAccount.js'),
'vpcPeeringConnections' : require(__dirname + '/plugins/aws/ec2/vpcPeeringConnections.js'),
'vpcPeeringLeastAccessRoutes' : require(__dirname + '/plugins/aws/ec2/vpcPeeringLeastAccessRoutes.js'),
'ebsEncryptedSnapshots' : require(__dirname + '/plugins/aws/ec2/ebsEncryptedSnapshots.js'),
'ebsDefaultEncryptionEnabled' : require(__dirname + '/plugins/aws/ec2/ebsDefaultEncryptionEnabled.js'),
'ec2MetadataOptions' : require(__dirname + '/plugins/aws/ec2/ec2MetadataOptions.js'),
Expand Down
129 changes: 129 additions & 0 deletions plugins/aws/ec2/vpcPeeringLeastAccessRoutes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
var async = require('async');
var helpers = require('../../../helpers/aws');

module.exports = {
title: 'VPC Peering Least Access Routes',
category: 'EC2',
domain: 'Compute',
severity: 'Medium',
description: 'Ensures VPC peering route table entries do not route entire peer VPC CIDR blocks',
more_info: 'VPC peering routes should be scoped to the minimum destination CIDR blocks required rather than routing all subnets of the peer VPC.',
link: 'https://docs.aws.amazon.com/vpc/latest/peering/vpc-peering-routing.html',
recommended_action: 'Replace broad VPC peering routes with more specific destination CIDR blocks',
apis: ['EC2:describeRouteTables', 'EC2:describeVpcPeeringConnections', 'STS:getCallerIdentity'],
realtime_triggers: ['ec2:CreateRoute', 'ec2:ReplaceRoute', 'ec2:DeleteRoute', 'ec2:CreateVpcPeeringConnection', 'ec2:DeleteVpcPeeringConnection'],

run: function(cache, settings, callback) {
var results = [];
var source = {};

var regions = helpers.regions(settings);
var acctRegion = helpers.defaultRegion(settings);
var awsOrGov = helpers.defaultPartition(settings);
var accountId = helpers.addSource(cache, source, ['sts', 'getCallerIdentity', acctRegion, 'data']);

async.each(regions.ec2, function(region, rcb) {
var describeRouteTables = helpers.addSource(cache, source, ['ec2', 'describeRouteTables', region]);
var describeVpcPeeringConnections = helpers.addSource(cache, source, ['ec2', 'describeVpcPeeringConnections', region]);

if (!describeRouteTables || !describeVpcPeeringConnections) return rcb();

if (describeRouteTables.err || !describeRouteTables.data) {
helpers.addResult(results, 3,
'Unable to query for route tables: ' + helpers.addError(describeRouteTables), region);
return rcb();
}

if (describeVpcPeeringConnections.err || !describeVpcPeeringConnections.data) {
helpers.addResult(results, 3,
'Unable to query for VPC peering connections: ' + helpers.addError(describeVpcPeeringConnections), region);
return rcb();
}

var peeringById = {};
describeVpcPeeringConnections.data.forEach(function(peering) {
peeringById[peering.VpcPeeringConnectionId] = peering;
});

var peeringRouteTables = 0;

describeRouteTables.data.forEach(function(routeTable) {
var peeringRoutes = getPeeringRoutes(routeTable.Routes);
if (!peeringRoutes.length) return;

peeringRouteTables++;
var resource = `arn:${awsOrGov}:ec2:${region}:${accountId}:route-table/${routeTable.RouteTableId}`;
var violations = [];

peeringRoutes.forEach(function(route) {
var peeringId = route.VpcPeeringConnectionId || route.GatewayId;
var peerCidrs = getPeerCidrs(peeringById[peeringId], routeTable.VpcId);
var destinations = [];

if (route.DestinationCidrBlock) destinations.push(route.DestinationCidrBlock);
if (route.DestinationIpv6CidrBlock) destinations.push(route.DestinationIpv6CidrBlock);

destinations.forEach(function(destination) {
if (peerCidrs.indexOf(destination.toLowerCase()) > -1) {
violations.push(`${destination} via ${peeringId}: routes full peer VPC CIDR`);
}
});
});

if (violations.length) {
helpers.addResult(results, 2,
`Route table "${routeTable.RouteTableId}" has non-compliant VPC peering routes: ${violations.join('; ')}`,
region, resource);
} else {
helpers.addResult(results, 0,
`Route table "${routeTable.RouteTableId}" uses least-access VPC peering routes`,
region, resource);
}
});

if (!peeringRouteTables) {
helpers.addResult(results, 0, 'No VPC peering routes found', region);
}

rcb();
}, function() {
callback(null, results, source);
});
}
};

function getPeeringRoutes(routes) {
if (!routes || !routes.length) return [];

return routes.filter(function(route) {
return route.VpcPeeringConnectionId ||
(route.GatewayId && route.GatewayId.indexOf('pcx-') === 0);
});
}

function getPeerCidrs(peering, localVpcId) {
var cidrs = [];

if (!peering || !localVpcId) return cidrs;

var requester = peering.RequesterVpcInfo || {};
var accepter = peering.AccepterVpcInfo || {};
var peerInfo;

if (requester.VpcId === localVpcId) peerInfo = accepter;
else if (accepter.VpcId === localVpcId) peerInfo = requester;

if (!peerInfo) return cidrs;

if (peerInfo.CidrBlock) cidrs.push(peerInfo.CidrBlock.toLowerCase());

if (peerInfo.CidrBlockSet && peerInfo.CidrBlockSet.length) {
peerInfo.CidrBlockSet.forEach(function(block) {
if (block.CidrBlock && cidrs.indexOf(block.CidrBlock.toLowerCase()) === -1) {
cidrs.push(block.CidrBlock.toLowerCase());
}
});
}

return cidrs;
}
110 changes: 110 additions & 0 deletions plugins/aws/ec2/vpcPeeringLeastAccessRoutes.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
var expect = require('chai').expect;
const vpcPeeringLeastAccessRoutes = require('./vpcPeeringLeastAccessRoutes');

const describeVpcPeeringConnections = [{
VpcPeeringConnectionId: 'pcx-abc123',
RequesterVpcInfo: {
VpcId: 'vpc-local',
CidrBlock: '10.0.0.0/16'
},
AccepterVpcInfo: {
VpcId: 'vpc-peer',
CidrBlock: '10.1.0.0/16'
}
}];

const createCache = (routeTables, peerings) => {
return {
sts: {
getCallerIdentity: {
'us-east-1': {
data: '111122223333'
}
}
},
ec2: {
describeRouteTables: {
'us-east-1': {
data: routeTables
}
},
describeVpcPeeringConnections: {
'us-east-1': {
data: peerings
}
}
}
};
};

describe('vpcPeeringLeastAccessRoutes', function() {
describe('run', function() {
it('should PASS when no VPC peering routes exist', function(done) {
const cache = createCache([{
RouteTableId: 'rtb-1',
VpcId: 'vpc-local',
Routes: [{
DestinationCidrBlock: '0.0.0.0/0',
GatewayId: 'igw-123'
}]
}], describeVpcPeeringConnections);

vpcPeeringLeastAccessRoutes.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(0);
done();
});
});

it('should FAIL when peering route uses full peer VPC CIDR', function(done) {
const cache = createCache([{
RouteTableId: 'rtb-1',
VpcId: 'vpc-local',
Routes: [{
DestinationCidrBlock: '10.1.0.0/16',
VpcPeeringConnectionId: 'pcx-abc123'
}]
}], describeVpcPeeringConnections);

vpcPeeringLeastAccessRoutes.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(2);
done();
});
});

it('should PASS when peering route is more specific than peer VPC CIDR', function(done) {
const cache = createCache([{
RouteTableId: 'rtb-1',
VpcId: 'vpc-local',
Routes: [{
DestinationCidrBlock: '10.1.2.0/24',
VpcPeeringConnectionId: 'pcx-abc123'
}]
}], describeVpcPeeringConnections);

vpcPeeringLeastAccessRoutes.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(0);
done();
});
});

it('should PASS when peering route targets a single host', function(done) {
const cache = createCache([{
RouteTableId: 'rtb-1',
VpcId: 'vpc-local',
Routes: [{
DestinationCidrBlock: '10.1.2.5/32',
VpcPeeringConnectionId: 'pcx-abc123'
}]
}], describeVpcPeeringConnections);

vpcPeeringLeastAccessRoutes.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(0);
done();
});
});
});
});