|
10 | 10 | import time |
11 | 11 |
|
12 | 12 | from utils.Tools import _pi |
| 13 | +from utils.Tools import _warn |
13 | 14 |
|
14 | 15 | from utils.Config import Config |
15 | 16 | from services.Service import Service |
|
26 | 27 | from services.ec2.drivers.Ec2Vpc import Ec2Vpc |
27 | 28 | from services.ec2.drivers.Ec2NACL import Ec2NACL |
28 | 29 | from services.ec2.drivers.Ec2Regional import Ec2Regional |
| 30 | +from services.ec2.drivers.Ec2KeyPair import Ec2KeyPair |
| 31 | +from services.ec2.drivers.Ec2SSM import Ec2SSM |
29 | 32 |
|
30 | 33 | class Ec2(Service): |
31 | 34 | CHARTSTYPE = { |
@@ -385,9 +388,60 @@ def getNetworkACLs(self): |
385 | 388 | result = self.ec2Client.describe_network_acls( |
386 | 389 | NextToken = result.get('NextToken') |
387 | 390 | ) |
388 | | - networkACLs = networkACLs + result.get('NetworkAcls') |
| 391 | + networkACLs = networkACLs + result.get('NetworkAcls') |
389 | 392 | return networkACLs |
390 | 393 |
|
| 394 | + def getKeyPairs(self): |
| 395 | + filters = [] |
| 396 | + if self.tags: |
| 397 | + filters = self.tags |
| 398 | + |
| 399 | + result = self.ec2Client.describe_key_pairs( |
| 400 | + Filters=filters |
| 401 | + ) |
| 402 | + return result.get('KeyPairs', []) |
| 403 | + |
| 404 | + def getInstanceKeyNames(self, instances): |
| 405 | + """Collect all key names actively used by running/stopped instances""" |
| 406 | + keyNames = set() |
| 407 | + for instanceArr in instances: |
| 408 | + for instanceData in instanceArr['Instances']: |
| 409 | + keyName = instanceData.get('KeyName') |
| 410 | + if keyName: |
| 411 | + keyNames.add(keyName) |
| 412 | + return keyNames |
| 413 | + |
| 414 | + def getSSMManagedInstances(self): |
| 415 | + """Get set of instance IDs managed by SSM. |
| 416 | +
|
| 417 | + Returns a set of managed instance IDs on success, or None if the |
| 418 | + SSM API call fails (e.g. missing ssm:DescribeInstanceInformation |
| 419 | + permission, or SSM unavailable in the region). Returning None lets |
| 420 | + the caller skip the SSM check entirely instead of flagging every |
| 421 | + instance as unmanaged (false positives). |
| 422 | + """ |
| 423 | + managedSet = set() |
| 424 | + try: |
| 425 | + # MaxResults=50 is the API maximum — request full pages to |
| 426 | + # minimize round-trips for large fleets. |
| 427 | + results = self.ssmClient.describe_instance_information(MaxResults=50) |
| 428 | + for info in results.get('InstanceInformationList', []): |
| 429 | + managedSet.add(info['InstanceId']) |
| 430 | + |
| 431 | + while results.get('NextToken'): |
| 432 | + results = self.ssmClient.describe_instance_information( |
| 433 | + MaxResults=50, |
| 434 | + NextToken=results['NextToken'] |
| 435 | + ) |
| 436 | + for info in results.get('InstanceInformationList', []): |
| 437 | + managedSet.add(info['InstanceId']) |
| 438 | + except botocore.exceptions.ClientError as e: |
| 439 | + _warn("Unable to retrieve SSM managed instances ({}); skipping SSM check to avoid false positives.".format(e.response['Error']['Code'])) |
| 440 | + return None |
| 441 | + except Exception as e: |
| 442 | + _warn("Unable to retrieve SSM managed instances ({}); skipping SSM check to avoid false positives.".format(e)) |
| 443 | + return None |
| 444 | + return managedSet |
391 | 445 |
|
392 | 446 | def getChartGenCost(self): |
393 | 447 | ''' |
@@ -665,6 +719,29 @@ def advise(self): |
665 | 719 | objs[f"NACL::{nacl['NetworkAclId']}"] = obj.getInfo() |
666 | 720 |
|
667 | 721 |
|
| 722 | + # Key Pair Checks |
| 723 | + keyPairs = self.getKeyPairs() |
| 724 | + instanceKeyNames = self.getInstanceKeyNames(instances) |
| 725 | + for kp in keyPairs: |
| 726 | + _pi('EC2::Key Pair', kp['KeyName']) |
| 727 | + obj = Ec2KeyPair(kp, instanceKeyNames) |
| 728 | + obj.run(self.__class__) |
| 729 | + objs[f"KeyPair::{kp['KeyName']}"] = obj.getInfo() |
| 730 | + |
| 731 | + # SSM Managed Instance Checks |
| 732 | + ssmManagedSet = self.getSSMManagedInstances() |
| 733 | + # Skip the SSM check entirely if the managed-instance lookup failed |
| 734 | + # (getSSMManagedInstances returns None) to avoid false positives. |
| 735 | + if ssmManagedSet is not None: |
| 736 | + for instanceArr in instances: |
| 737 | + for instanceData in instanceArr['Instances']: |
| 738 | + if instanceData['State']['Name'] not in ('running', 'stopped'): |
| 739 | + continue |
| 740 | + _pi('EC2::SSM Check', instanceData['InstanceId']) |
| 741 | + obj = Ec2SSM(instanceData['InstanceId'], ssmManagedSet) |
| 742 | + obj.run(self.__class__) |
| 743 | + objs[f"SSM::{instanceData['InstanceId']}"] = obj.getInfo() |
| 744 | + |
668 | 745 | if self.getChartGenCost(): |
669 | 746 | self.setChartData({"EC2 Instance Family Pricing": self.getChartGenCost()}) |
670 | 747 |
|
|
0 commit comments