From aace43e1065abf8e6b22ca8217e5303820708f20 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 29 Aug 2024 11:14:31 -0500 Subject: [PATCH] New plugin to detects open ports in security groups that are not associated with any running service on the instance --- exports.js | 1 + plugins/aws/ec2/unusedOpenPorts.js | 169 ++++++++++++++++++++++++ plugins/aws/ec2/unusedOpenPorts.spec.js | 147 +++++++++++++++++++++ 3 files changed, 317 insertions(+) create mode 100644 plugins/aws/ec2/unusedOpenPorts.js create mode 100644 plugins/aws/ec2/unusedOpenPorts.spec.js diff --git a/exports.js b/exports.js index dcbf2a3dbb..71dee84733 100644 --- a/exports.js +++ b/exports.js @@ -184,6 +184,7 @@ module.exports = { 'flowLogsEnabled' : require(__dirname + '/plugins/aws/ec2/flowLogsEnabled.js'), 'vpcMultipleSubnets' : require(__dirname + '/plugins/aws/ec2/multipleSubnets.js'), 'overlappingSecurityGroups' : require(__dirname + '/plugins/aws/ec2/overlappingSecurityGroups.js'), + 'unusedOpenPorts' : require(__dirname + '/plugins/aws/ec2/unusedOpenPorts.js'), 'publicAmi' : require(__dirname + '/plugins/aws/ec2/publicAmi.js'), 'encryptedAmi' : require(__dirname + '/plugins/aws/ec2/encryptedAmi.js'), 'amiHasTags' : require(__dirname + '/plugins/aws/ec2/amiHasTags.js'), diff --git a/plugins/aws/ec2/unusedOpenPorts.js b/plugins/aws/ec2/unusedOpenPorts.js new file mode 100644 index 0000000000..5a448f2c8f --- /dev/null +++ b/plugins/aws/ec2/unusedOpenPorts.js @@ -0,0 +1,169 @@ +var async = require('async'); +var helpers = require('../../../helpers/aws'); + +module.exports = { + title: 'Unused Open Ports', + category: 'EC2', + domain: 'Compute', + severity: 'Medium', + description: 'Detects open ports in security groups that are not associated with any running service on the instance.', + more_info: 'Unused open ports can pose a security risk as they might be exploited by attackers if not properly managed.', + link: 'https://docs.aws.amazon.com/vpc/latest/userguide/VPC_SecurityGroups.html', + recommended_action: 'Close unused ports in the security group or ensure the associated service is properly configured.', + apis: ['EC2:describeSecurityGroups', 'EC2:describeInstances'], + realtime_triggers: ['ec2:CreateSecurityGroup', 'ec2:AuthorizeSecurityGroupIngress', 'ec2:ModifySecurityGroupRules', 'ec2:RevokeSecurityGroupIngress', 'ec2:DeleteSecurityGroup'], + + run: function (cache, settings, callback) { + var results = []; + var source = {}; + var regions = helpers.regions(settings); + + async.each(regions.ec2, function (region, rcb) { + processRegion(region, cache, settings, results, source, rcb); + }, function () { + callback(null, results, source); + }); + } +}; + +/** + * Process a single AWS region to check for unused open ports. + */ +function processRegion(region, cache, settings, results, source, rcb) { + var describeInstances = helpers.addSource(cache, source, ['ec2', 'describeInstances', region]); + var describeSecurityGroups = helpers.addSource(cache, source, ['ec2', 'describeSecurityGroups', region]); + + if (!describeInstances || !describeSecurityGroups) return rcb(); + + if (hasError(describeInstances) || hasError(describeSecurityGroups)) { + helpers.addResult( + results, + 3, + `Unable to query for instances or security groups: ${helpers.addError(describeInstances || describeSecurityGroups)}`, + region + ); + return rcb(); + } + + var instancePorts = getInstancePorts(describeInstances.data, describeSecurityGroups.data); + + analyzeSecurityGroups(instancePorts, describeSecurityGroups.data, region, results); + + rcb(); +} + +/** + * Check if the AWS API response has an error or missing data. + */ +function hasError(response) { + return response.err || !response.data; +} + +/** + * Collect used ports for each running instance. + */ +function getInstancePorts(instances, securityGroups) { + var instancePorts = {}; + + instances.forEach(function (instance) { + if (!instance.State || instance.State.Name !== 'running') return; + + var instanceId = instance.InstanceId; + instancePorts[instanceId] = collectUsedPorts(instance, securityGroups); + }); + + return instancePorts; +} + +/** + * Collect used ports from instance security groups. + */ +function collectUsedPorts(instance, securityGroups) { + var usedPorts = []; + + if (instance.SecurityGroups) { + instance.SecurityGroups.forEach(function (group) { + var securityGroup = securityGroups.find(g => g.GroupId === group.GroupId); + + if (securityGroup && securityGroup.IpPermissions) { + securityGroup.IpPermissions.forEach(function (permission) { + if (permission.FromPort && permission.ToPort) { + for (let port = permission.FromPort; port <= permission.ToPort; port++) { + if (!usedPorts.includes(port)) { + usedPorts.push(port); + } + } + } + }); + } + }); + } + + return usedPorts; +} + +/** + * Analyze security groups to find unused open ports. + */ +function analyzeSecurityGroups(instancePorts, securityGroups, region, results) { + securityGroups.forEach(function (group) { + if (!group.IpPermissions) return; + + var resource = `arn:${helpers.defaultPartition({})}:ec2:${region}:${group.OwnerId}:security-group/${group.GroupId}`; + var unusedPorts = getUnusedPorts(group, instancePorts); + + if (unusedPorts.length) { + helpers.addResult( + results, + 2, + `Security group "${group.GroupName}" has unused open ports: ${unusedPorts.join(', ')}`, + region, + resource + ); + } else { + helpers.addResult( + results, + 0, + `Security group "${group.GroupName}" has no unused open ports`, + region, + resource + ); + } + }); +} + +/** + * Get unused open ports for a security group. + */ +function getUnusedPorts(group, instancePorts) { + var unusedPorts = []; + + group.IpPermissions.forEach(function (permission) { + if (permission.IpRanges) { + permission.IpRanges.forEach(function (range) { + if (range.CidrIp === '0.0.0.0/0') { + for (let port = permission.FromPort; port <= permission.ToPort; port++) { + if (!isPortUsed(port, instancePorts) && !unusedPorts.includes(port)) { + unusedPorts.push(port); + } + } + } + }); + } + }); + + return unusedPorts; +} + +/** + * Check if a port is used by any instance. + */ +function isPortUsed(port, instancePorts) { + for (var instanceId in instancePorts) { + if (instancePorts[instanceId].includes(port)) { + return true; + } + } + return false; +} + diff --git a/plugins/aws/ec2/unusedOpenPorts.spec.js b/plugins/aws/ec2/unusedOpenPorts.spec.js new file mode 100644 index 0000000000..a96cab891b --- /dev/null +++ b/plugins/aws/ec2/unusedOpenPorts.spec.js @@ -0,0 +1,147 @@ +var expect = require('chai').expect; +const unusedOpenPorts = require('./unusedOpenPorts'); // Asegúrate de que el nombre del archivo sea correcto + +const securityGroups = [ + { + "Description": "Allows SSH access to developer", + "GroupName": "spec-test-sg", + "IpPermissions": [{ + "FromPort": 22, + "IpProtocol": "tcp", + "IpRanges": [ + { + "CidrIp": "0.0.0.0/0" + } + ], + "ToPort": 22, + "UserIdGroupPairs": [] + }], + "OwnerId": "12345654321", + "GroupId": "sg-0b5f2771716acfee4", + "IpPermissionsEgress": [], + "VpcId": "vpc-99de2fe4" + }, + { + "Description": "Open HTTP and HTTPS access", + "GroupName": "launch-wizard-1", + "IpPermissions": [ + { + "FromPort": 80, + "IpProtocol": "tcp", + "IpRanges": [ + { + "CidrIp": "0.0.0.0/0" + } + ], + "ToPort": 80, + "UserIdGroupPairs": [] + }, + { + "FromPort": 443, + "IpProtocol": "tcp", + "IpRanges": [ + { + "CidrIp": "0.0.0.0/0" + } + ], + "ToPort": 443, + "UserIdGroupPairs": [] + } + ], + "OwnerId": "12345654321", + "GroupId": "sg-0ff1642cae23c309a", + "IpPermissionsEgress": [], + "VpcId": "vpc-99de2fe4" + } +]; + +const instances = [ + { + "InstanceId": "i-1234567890abcdef0", + "State": { + "Name": "running" + }, + "SecurityGroups": [ + { + "GroupId": "sg-0ff1642cae23c309a" + } + ] + } +]; + +const createCache = (securityGroups, instances) => { + return { + ec2: { + describeSecurityGroups: { + 'us-east-1': { + data: securityGroups + } + }, + describeInstances: { + 'us-east-1': { + data: instances + } + } + } + }; +}; + +const createErrorCache = () => { + return { + ec2: { + describeSecurityGroups: { + 'us-east-1': { + err: { + message: 'error describing security groups' + } + } + }, + describeInstances: { + 'us-east-1': { + err: { + message: 'error describing instances' + } + } + } + } + }; +}; + +describe('unusedOpenPorts', function () { + describe('run', function () { + it('should FAIL if there are unused open ports', function (done) { + const cache = createCache([securityGroups[0]], instances); + unusedOpenPorts.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + done(); + }); + }); + + it('should PASS if all open ports are associated with running services', function (done) { + const cache = createCache([securityGroups[1]], instances); + unusedOpenPorts.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + done(); + }); + }); + + it('should UNKNOWN if there is an error describing security groups or instances', function (done) { + const cache = createErrorCache(); + unusedOpenPorts.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(3); + done(); + }); + }); + + it('should PASS if no security groups or instances are found', function (done) { + const cache = createCache([], []); + unusedOpenPorts.run(cache, {}, (err, results) => { + expect(results.length).to.equal(0); + done(); + }); + }); + }); +}); \ No newline at end of file