From 00b43242ed7acac947ebd47786c605fb3f07bb02 Mon Sep 17 00:00:00 2001 From: Hamza Aziz Date: Wed, 22 Jul 2026 13:07:42 +0000 Subject: [PATCH 1/2] SLK-130552/vpc-new-plugin --- exports.js | 1 + .../aws/ec2/vpcPeeringLeastAccessRoutes.js | 204 ++++++++++++++++++ .../ec2/vpcPeeringLeastAccessRoutes.spec.js | 149 +++++++++++++ 3 files changed, 354 insertions(+) create mode 100644 plugins/aws/ec2/vpcPeeringLeastAccessRoutes.js create mode 100644 plugins/aws/ec2/vpcPeeringLeastAccessRoutes.spec.js diff --git a/exports.js b/exports.js index 261e4485ff..748f99069c 100644 --- a/exports.js +++ b/exports.js @@ -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'), diff --git a/plugins/aws/ec2/vpcPeeringLeastAccessRoutes.js b/plugins/aws/ec2/vpcPeeringLeastAccessRoutes.js new file mode 100644 index 0000000000..6f558a2022 --- /dev/null +++ b/plugins/aws/ec2/vpcPeeringLeastAccessRoutes.js @@ -0,0 +1,204 @@ +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 follow a configurable least-access routing policy', + more_info: 'VPC peering routes should be scoped to the minimum destination CIDR blocks required. Use plugin settings to define the maximum allowed route breadth for your environment.', + 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'], + settings: { + max_peering_route_prefix_length: { + name: 'Max Peering Route Prefix Length', + description: 'Minimum required prefix length for peering route destinations (e.g. 24 allows /24-/32 and fails on /16)', + regex: '^([0-9]|[1-2][0-9]|3[0-2])$', + default: '' + }, + allowed_peering_destinations: { + name: 'Allowed Peering Destinations', + description: 'Comma-separated destination CIDR blocks permitted on VPC peering routes', + regex: '[0-9./:,a-fA-F-]', + default: '' + }, + fail_on_full_vpc_cidr: { + name: 'Fail on Full VPC CIDR Peering Routes', + description: 'When true, fail peering routes whose destination equals the entire peer VPC CIDR block', + regex: '^(true|false)$', + default: 'true' + } + }, + compliance: { + cis1: '4.4 Ensure routing tables for VPC peering are "least access"', + cis2: '6.6 Ensure routing tables for VPC peering are "least access"' + }, + realtime_triggers: ['ec2:CreateRoute', 'ec2:ReplaceRoute', 'ec2:DeleteRoute', 'ec2:CreateVpcPeeringConnection', 'ec2:DeleteVpcPeeringConnection'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + + var config = { + maxPrefix: settings.max_peering_route_prefix_length || this.settings.max_peering_route_prefix_length.default, + allowedDestinations: settings.allowed_peering_destinations || this.settings.allowed_peering_destinations.default, + failOnFullVpcCidr: settings.fail_on_full_vpc_cidr || this.settings.fail_on_full_vpc_cidr.default + }; + + config.failOnFullVpcCidr = (config.failOnFullVpcCidr === 'true'); + config.allowedDestinations = config.allowedDestinations ? + config.allowedDestinations.split(',').map(function(cidr) { return cidr.trim().toLowerCase(); }).filter(Boolean) : + []; + + if (!config.maxPrefix && !config.allowedDestinations.length && !config.failOnFullVpcCidr) { + return callback(null, results, source); + } + + if (config.maxPrefix) config.maxPrefix = Number(config.maxPrefix); + + 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 peering = peeringById[peeringId]; + var destinations = []; + + if (route.DestinationCidrBlock) destinations.push(route.DestinationCidrBlock); + if (route.DestinationIpv6CidrBlock) destinations.push(route.DestinationIpv6CidrBlock); + + destinations.forEach(function(destination) { + var issue = evaluateRoute(destination, config, peering, routeTable.VpcId); + if (issue) { + violations.push(`${destination} via ${peeringId}: ${issue}`); + } + }); + }); + + 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 per configured policy`, + 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 getPrefixLength(cidr) { + if (!cidr || cidr.indexOf('/') === -1) return null; + + var prefix = parseInt(cidr.split('/')[1], 10); + return isNaN(prefix) ? null : prefix; +} + +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; +} + +function evaluateRoute(destination, config, peering, localVpcId) { + var normalizedDestination = destination.toLowerCase(); + var issues = []; + + if (config.allowedDestinations.length && + config.allowedDestinations.indexOf(normalizedDestination) === -1) { + issues.push('destination not in allowed_peering_destinations'); + } + + if (config.maxPrefix) { + var prefixLength = getPrefixLength(normalizedDestination); + if (prefixLength === null || prefixLength < config.maxPrefix) { + issues.push(`destination broader than /${config.maxPrefix}`); + } + } + + if (config.failOnFullVpcCidr) { + var peerCidrs = getPeerCidrs(peering, localVpcId); + if (peerCidrs.indexOf(normalizedDestination) > -1) { + issues.push('destination equals full peer VPC CIDR'); + } + } + + return issues.length ? issues.join(', ') : null; +} diff --git a/plugins/aws/ec2/vpcPeeringLeastAccessRoutes.spec.js b/plugins/aws/ec2/vpcPeeringLeastAccessRoutes.spec.js new file mode 100644 index 0000000000..4d7b85819c --- /dev/null +++ b/plugins/aws/ec2/vpcPeeringLeastAccessRoutes.spec.js @@ -0,0 +1,149 @@ +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 do nothing when all policy settings are disabled', 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, { fail_on_full_vpc_cidr: 'false' }, (err, results) => { + expect(results.length).to.equal(0); + done(); + }); + }); + + 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 by default', 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 FAIL when route is broader than max_peering_route_prefix_length', 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, { + fail_on_full_vpc_cidr: 'false', + max_peering_route_prefix_length: '24' + }, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + done(); + }); + }); + + it('should PASS when route matches allowed_peering_destinations', 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, { + fail_on_full_vpc_cidr: 'false', + allowed_peering_destinations: '10.1.2.5/32' + }, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + done(); + }); + }); + }); +}); From 6007ec82b0a00d78e899bd0d2039e23e0582a7dc Mon Sep 17 00:00:00 2001 From: Hamza Aziz Date: Thu, 23 Jul 2026 11:50:38 +0000 Subject: [PATCH 2/2] SLK-130552/vpc-new-plugin --- .../aws/ec2/vpcPeeringLeastAccessRoutes.js | 87 ++----------------- .../ec2/vpcPeeringLeastAccessRoutes.spec.js | 45 +--------- 2 files changed, 9 insertions(+), 123 deletions(-) diff --git a/plugins/aws/ec2/vpcPeeringLeastAccessRoutes.js b/plugins/aws/ec2/vpcPeeringLeastAccessRoutes.js index 6f558a2022..533e384b57 100644 --- a/plugins/aws/ec2/vpcPeeringLeastAccessRoutes.js +++ b/plugins/aws/ec2/vpcPeeringLeastAccessRoutes.js @@ -6,58 +6,17 @@ module.exports = { category: 'EC2', domain: 'Compute', severity: 'Medium', - description: 'Ensures VPC peering route table entries follow a configurable least-access routing policy', - more_info: 'VPC peering routes should be scoped to the minimum destination CIDR blocks required. Use plugin settings to define the maximum allowed route breadth for your environment.', + 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'], - settings: { - max_peering_route_prefix_length: { - name: 'Max Peering Route Prefix Length', - description: 'Minimum required prefix length for peering route destinations (e.g. 24 allows /24-/32 and fails on /16)', - regex: '^([0-9]|[1-2][0-9]|3[0-2])$', - default: '' - }, - allowed_peering_destinations: { - name: 'Allowed Peering Destinations', - description: 'Comma-separated destination CIDR blocks permitted on VPC peering routes', - regex: '[0-9./:,a-fA-F-]', - default: '' - }, - fail_on_full_vpc_cidr: { - name: 'Fail on Full VPC CIDR Peering Routes', - description: 'When true, fail peering routes whose destination equals the entire peer VPC CIDR block', - regex: '^(true|false)$', - default: 'true' - } - }, - compliance: { - cis1: '4.4 Ensure routing tables for VPC peering are "least access"', - cis2: '6.6 Ensure routing tables for VPC peering are "least access"' - }, realtime_triggers: ['ec2:CreateRoute', 'ec2:ReplaceRoute', 'ec2:DeleteRoute', 'ec2:CreateVpcPeeringConnection', 'ec2:DeleteVpcPeeringConnection'], run: function(cache, settings, callback) { var results = []; var source = {}; - var config = { - maxPrefix: settings.max_peering_route_prefix_length || this.settings.max_peering_route_prefix_length.default, - allowedDestinations: settings.allowed_peering_destinations || this.settings.allowed_peering_destinations.default, - failOnFullVpcCidr: settings.fail_on_full_vpc_cidr || this.settings.fail_on_full_vpc_cidr.default - }; - - config.failOnFullVpcCidr = (config.failOnFullVpcCidr === 'true'); - config.allowedDestinations = config.allowedDestinations ? - config.allowedDestinations.split(',').map(function(cidr) { return cidr.trim().toLowerCase(); }).filter(Boolean) : - []; - - if (!config.maxPrefix && !config.allowedDestinations.length && !config.failOnFullVpcCidr) { - return callback(null, results, source); - } - - if (config.maxPrefix) config.maxPrefix = Number(config.maxPrefix); - var regions = helpers.regions(settings); var acctRegion = helpers.defaultRegion(settings); var awsOrGov = helpers.defaultPartition(settings); @@ -98,16 +57,15 @@ module.exports = { peeringRoutes.forEach(function(route) { var peeringId = route.VpcPeeringConnectionId || route.GatewayId; - var peering = peeringById[peeringId]; + 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) { - var issue = evaluateRoute(destination, config, peering, routeTable.VpcId); - if (issue) { - violations.push(`${destination} via ${peeringId}: ${issue}`); + if (peerCidrs.indexOf(destination.toLowerCase()) > -1) { + violations.push(`${destination} via ${peeringId}: routes full peer VPC CIDR`); } }); }); @@ -118,7 +76,7 @@ module.exports = { region, resource); } else { helpers.addResult(results, 0, - `Route table "${routeTable.RouteTableId}" uses least-access VPC peering routes per configured policy`, + `Route table "${routeTable.RouteTableId}" uses least-access VPC peering routes`, region, resource); } }); @@ -143,13 +101,6 @@ function getPeeringRoutes(routes) { }); } -function getPrefixLength(cidr) { - if (!cidr || cidr.indexOf('/') === -1) return null; - - var prefix = parseInt(cidr.split('/')[1], 10); - return isNaN(prefix) ? null : prefix; -} - function getPeerCidrs(peering, localVpcId) { var cidrs = []; @@ -176,29 +127,3 @@ function getPeerCidrs(peering, localVpcId) { return cidrs; } - -function evaluateRoute(destination, config, peering, localVpcId) { - var normalizedDestination = destination.toLowerCase(); - var issues = []; - - if (config.allowedDestinations.length && - config.allowedDestinations.indexOf(normalizedDestination) === -1) { - issues.push('destination not in allowed_peering_destinations'); - } - - if (config.maxPrefix) { - var prefixLength = getPrefixLength(normalizedDestination); - if (prefixLength === null || prefixLength < config.maxPrefix) { - issues.push(`destination broader than /${config.maxPrefix}`); - } - } - - if (config.failOnFullVpcCidr) { - var peerCidrs = getPeerCidrs(peering, localVpcId); - if (peerCidrs.indexOf(normalizedDestination) > -1) { - issues.push('destination equals full peer VPC CIDR'); - } - } - - return issues.length ? issues.join(', ') : null; -} diff --git a/plugins/aws/ec2/vpcPeeringLeastAccessRoutes.spec.js b/plugins/aws/ec2/vpcPeeringLeastAccessRoutes.spec.js index 4d7b85819c..919b2908dd 100644 --- a/plugins/aws/ec2/vpcPeeringLeastAccessRoutes.spec.js +++ b/plugins/aws/ec2/vpcPeeringLeastAccessRoutes.spec.js @@ -39,22 +39,6 @@ const createCache = (routeTables, peerings) => { describe('vpcPeeringLeastAccessRoutes', function() { describe('run', function() { - it('should do nothing when all policy settings are disabled', 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, { fail_on_full_vpc_cidr: 'false' }, (err, results) => { - expect(results.length).to.equal(0); - done(); - }); - }); - it('should PASS when no VPC peering routes exist', function(done) { const cache = createCache([{ RouteTableId: 'rtb-1', @@ -72,7 +56,7 @@ describe('vpcPeeringLeastAccessRoutes', function() { }); }); - it('should FAIL when peering route uses full peer VPC CIDR by default', function(done) { + it('should FAIL when peering route uses full peer VPC CIDR', function(done) { const cache = createCache([{ RouteTableId: 'rtb-1', VpcId: 'vpc-local', @@ -106,27 +90,7 @@ describe('vpcPeeringLeastAccessRoutes', function() { }); }); - it('should FAIL when route is broader than max_peering_route_prefix_length', 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, { - fail_on_full_vpc_cidr: 'false', - max_peering_route_prefix_length: '24' - }, (err, results) => { - expect(results.length).to.equal(1); - expect(results[0].status).to.equal(2); - done(); - }); - }); - - it('should PASS when route matches allowed_peering_destinations', function(done) { + it('should PASS when peering route targets a single host', function(done) { const cache = createCache([{ RouteTableId: 'rtb-1', VpcId: 'vpc-local', @@ -136,10 +100,7 @@ describe('vpcPeeringLeastAccessRoutes', function() { }] }], describeVpcPeeringConnections); - vpcPeeringLeastAccessRoutes.run(cache, { - fail_on_full_vpc_cidr: 'false', - allowed_peering_destinations: '10.1.2.5/32' - }, (err, results) => { + vpcPeeringLeastAccessRoutes.run(cache, {}, (err, results) => { expect(results.length).to.equal(1); expect(results[0].status).to.equal(0); done();