Skip to content

Commit

Permalink
feat: find and verify security groups (#85)
Browse files Browse the repository at this point in the history
* Updated with enhancements due to case-sensitivity in the API

* Updated to remove region being separate from the ocredentials object. Now it's a field within the object.

* Updated exception logic to find elastic load balancers with no security groups attached

* Brought files in line with both Inventory_Script repos
  • Loading branch information
paulbayer authored Sep 25, 2024
1 parent 3c0b152 commit 800c514
Show file tree
Hide file tree
Showing 8 changed files with 1,094 additions and 119 deletions.
7 changes: 5 additions & 2 deletions inventory-scripts/ArgumentsClass.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
verbose = args.loglevel
"""
__version__ = "2024.05.09"
__version__ = "2024.09.24"

import os


class CommonArguments:
Expand Down Expand Up @@ -186,7 +188,8 @@ def multiregion(self):
nargs="*",
dest="Regions",
metavar="region name string",
default=["us-east-1"],
# default=["us-east-1"],
default=[os.getenv("AWS_DEFAULT_REGION","us-east-1")],
help="String fragment of the region(s) you want to check for resources. You can supply multiple fragments.\n"
"Use 'all' for everything you've opted into, and 'global' for everything, regardless of opted-in status")

Expand Down
279 changes: 212 additions & 67 deletions inventory-scripts/Inventory_Modules.py

Large diffs are not rendered by default.

6 changes: 0 additions & 6 deletions inventory-scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,16 +199,10 @@ This script identifies all roles across the examined accounts, addressing the co

### [all_my_saml_providers.py](./all_my_saml_providers.py)

This script aims to locate all Identity Providers (IDPs) across the user's organizational accounts. While the script also includes the capability to delete these IDPs, it is generally not a common requirement, as most users prefer to maintain visibility and control over their configured IDPs.

The primary purpose of this tool is to provide a comprehensive inventory of the IDPs within the user's environment. This information can be valuable for understanding the identity and access management landscape, ensuring compliance, and managing the overall security posture of the cloud infrastructure.

### [all_my_saml_providers.py](./all_my_saml_providers.py)

This script is designed to locate all SAML providers within the user's organization. While the script also includes the capability to delete these SAML providers, this functionality should be used with caution, as it can significantly impact access to the affected accounts, potentially making it challenging to regain access.

The primary purpose of this tool is to provide a comprehensive inventory of the SAML providers configured across the organization. This information can be valuable for understanding the identity and access management landscape, ensuring compliance, and managing the overall security posture of the cloud infrastructure.

By offering the ability to identify all configured SAML providers, this script empowers users to have a centralized view of their identity-related resources. The deletion functionality is included as a safeguard, but its usage should be carefully considered, as it can have significant consequences on the organization's access and authentication mechanisms.

### [all_my_subnets.py](./all_my_subnets.py)
Expand Down
231 changes: 231 additions & 0 deletions inventory-scripts/all_my_ecs_clusters_and_tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
# !/usr/bin/env python3

import sys
from os.path import split
import Inventory_Modules
from Inventory_Modules import get_all_credentials, display_results, find_account_ecs_clusters_services_and_tasks2
from ArgumentsClass import CommonArguments
from colorama import init, Fore
from botocore.exceptions import ClientError
from queue import Queue
from threading import Thread
from tqdm.auto import tqdm
from time import time

import logging

init()
__version__ = "2024.09.06"
ERASE_LINE = '\x1b[2K'
begin_time = time()

# TODO: Need a table at the bottom that summarizes the results, by instance-type, by running/ stopped, maybe by account and region


##################
# Functions
##################

def parse_args(f_arguments):
"""
Description: Parses the arguments passed into the script
@param f_arguments: args represents the list of arguments passed in
@return: returns an object namespace that contains the individualized parameters passed in
"""
script_path, script_name = split(sys.argv[0])
parser = CommonArguments()
parser.my_parser.description = "We're going to find all instances within any of the accounts we have access to, given the profile(s) provided."
parser.multiprofile()
parser.multiregion()
parser.extendedargs()
parser.rolestouse()
parser.rootOnly()
parser.save_to_file()
parser.timing()
parser.verbosity()
parser.version(__version__)
local = parser.my_parser.add_argument_group(script_name, 'Parameters specific to this script')
local.add_argument(
"-s", "--status",
dest="pStatus",
choices=['running', 'stopped'],
type=str,
default=None,
help="Whether you want to limit the instances returned to either 'running', 'stopped'. Default is both")
return parser.my_parser.parse_args(f_arguments)


# The parameters passed to this function should be the dictionary of attributes that will be examined within the thread.
def find_all_clusters_and_tasks(fAllCredentials: list, fStatus: str = None) -> list:
"""
Description: Finds all the instances from all the accounts/ regions within the Credentials supplied
@param fAllCredentials: list of all credentials for all member accounts supplied
@param fStatus: string determining whether you're looking for "running" or "stopped" instances
@return: Returns a list of ECS Clusters, Services and Tasks
"""

# This function is called
class FindInstances(Thread):

def __init__(self, queue):
Thread.__init__(self)
self.queue = queue

def run(self):
while True:
# Get the work from the queue and expand the tuple
c_account_credentials = self.queue.get()
logging.info(f"De-queued info for account number {c_account_credentials['AccountId']}")
try:
# Now go through those stacksets and determine the instances, made up of accounts and regions
# Most time spent in this loop
EcsInfo = Inventory_Modules.find_account_ecs_clusters_services_and_tasks2(c_account_credentials)
logging.info(f"Account: {c_account_credentials['AccountId']} Region: {c_account_credentials['Region']} | Found {len(Instances['Reservations'])} instances")
State = InstanceType = InstanceId = PublicDnsName = Name = ""
if 'Reservations' in Instances.keys():
for y in range(len(Instances['Reservations'])):
for z in range(len(Instances['Reservations'][y]['Instances'])):
InstanceType = Instances['Reservations'][y]['Instances'][z]['InstanceType']
InstanceId = Instances['Reservations'][y]['Instances'][z]['InstanceId']
PublicDnsName = Instances['Reservations'][y]['Instances'][z]['PublicDnsName'] if 'PublicDnsName' in Instances['Reservations'][y]['Instances'][z] else "No Public DNS Name"
State = Instances['Reservations'][y]['Instances'][z]['State']['Name']
Name = "No Name Tag"
try:
for x in range(len(Instances['Reservations'][y]['Instances'][z]['Tags'])):
if Instances['Reservations'][y]['Instances'][z]['Tags'][x]['Key'] == "Name":
Name = Instances['Reservations'][y]['Instances'][z]['Tags'][x]['Value']
except KeyError as my_Error: # This is needed for when there is no "Tags" key within the describe-instances output
logging.info(my_Error)
pass
if fStatus is None or fStatus == State:
AllInstances.append({'MgmtAccount' : c_account_credentials['MgmtAccount'],
'AccountId' : c_account_credentials['AccountId'],
'Region' : c_account_credentials['Region'],
'State' : State,
'InstanceType' : InstanceType,
'InstanceId' : InstanceId,
'PublicDNSName': PublicDnsName,
'ParentProfile': c_account_credentials['ParentProfile'],
'Name' : Name,
})
else:
continue
except KeyError as my_Error:
logging.error(f"Account Access failed - trying to access {c_account_credentials['AccountId']}")
logging.info(f"Actual Error: {my_Error}")
pass
except AttributeError as my_Error:
logging.error(f"Error: Likely that one of the supplied profiles was wrong")
logging.warning(my_Error)
continue
except ClientError as my_Error:
if 'AuthFailure' in str(my_Error):
logging.error(f"Authorization Failure accessing account {c_account_credentials['AccountId']} in {c_account_credentials['Region']} region")
logging.warning(f"It's possible that the region {c_account_credentials['Region']} hasn't been opted-into")
continue
else:
logging.error(f"Error: Likely throttling errors from too much activity")
logging.warning(my_Error)
continue
finally:
pbar.update()
self.queue.task_done()

###########

checkqueue = Queue()

AllInstances = []
WorkerThreads = min(len(fAllCredentials), 25)

pbar = tqdm(desc=f'Finding instances from {len(fAllCredentials)} accounts / regions',
total=len(fAllCredentials), unit=' locations'
)

for x in range(WorkerThreads):
worker = FindInstances(checkqueue)
# Setting daemon to True will let the main thread exit even though the workers are blocking
worker.daemon = True
worker.start()

for credential in fAllCredentials:
logging.info(f"Beginning to queue data - starting with {credential['AccountId']}")
try:
# I don't know why - but double parens are necessary below. If you remove them, only the first parameter is queued.
checkqueue.put(credential)
except ClientError as my_Error:
if "AuthFailure" in str(my_Error):
logging.error(f"Authorization Failure accessing account {credential['AccountId']} in {credential['Region']} region")
logging.warning(f"It's possible that the region {credential['Region']} hasn't been opted-into")
pass
checkqueue.join()
pbar.close()
return AllInstances


##################
# Main
##################

if __name__ == '__main__':
args = parse_args(sys.argv[1:])
pProfiles = args.Profiles
pRegionList = args.Regions
pAccounts = args.Accounts
pSkipAccounts = args.SkipAccounts
pSkipProfiles = args.SkipProfiles
pAccessRoles = args.AccessRoles
pStatus = args.pStatus
pRootOnly = args.RootOnly
pFilename = args.Filename
pTiming = args.Time
verbose = args.loglevel
# Setup logging levels
logging.basicConfig(level=verbose, format="[%(filename)s:%(lineno)s - %(funcName)20s() ] %(message)s")
logging.getLogger("boto3").setLevel(logging.CRITICAL)
logging.getLogger("botocore").setLevel(logging.CRITICAL)
logging.getLogger("s3transfer").setLevel(logging.CRITICAL)
logging.getLogger("urllib3").setLevel(logging.CRITICAL)

print()
print(f"Checking for instances... ")
print()

# Find credentials for all Child Accounts
# CredentialList = get_credentials(pProfiles, pRegionList, pSkipProfiles, pSkipAccounts, pRootOnly, pAccounts, pAccessRoles, pTiming)
CredentialList = get_all_credentials(pProfiles, pTiming, pSkipProfiles, pSkipAccounts, pRootOnly, pAccounts, pRegionList, pAccessRoles)
AccountNum = len(set([acct['AccountId'] for acct in CredentialList]))
RegionNum = len(set([acct['Region'] for acct in CredentialList]))
print()
print(f"Searching total of {AccountNum} accounts and {RegionNum} regions")
if pTiming:
print()
milestone_time1 = time()
print(f"{Fore.GREEN}\t\tFiguring out what regions are available to your accounts, and capturing credentials for all accounts in those regions took: {(milestone_time1 - begin_time):.3f} seconds{Fore.RESET}")
print()
print(f"Now running through all accounts and regions identified to find resources...")
# Collect all the instances from the credentials found
AllInstances = find_all_clusters_and_tasks(CredentialList, pStatus)
# Display the information we've found thus far
display_dict = {'ParentProfile': {'DisplayOrder': 1, 'Heading': 'Parent Profile'},
'MgmtAccount' : {'DisplayOrder': 2, 'Heading': 'Mgmt Acct'},
'AccountId' : {'DisplayOrder': 3, 'Heading': 'Acct Number'},
'Region' : {'DisplayOrder': 4, 'Heading': 'Region'},
'InstanceType' : {'DisplayOrder': 5, 'Heading': 'Instance Type'},
'Name' : {'DisplayOrder': 6, 'Heading': 'Name'},
'InstanceId' : {'DisplayOrder': 7, 'Heading': 'Instance ID'},
'PublicDNSName': {'DisplayOrder': 8, 'Heading': 'Public Name'},
'State' : {'DisplayOrder': 9, 'Heading': 'State', 'Condition': ['running']}}

sorted_all_instances = sorted(AllInstances, key=lambda d: (d['ParentProfile'], d['MgmtAccount'], d['Region'], d['AccountId']))
display_results(sorted_all_instances, display_dict, None, pFilename)

if pTiming:
print(ERASE_LINE)
print(f"{Fore.GREEN}This script took {time() - begin_time:.2f} seconds{Fore.RESET}")
print(ERASE_LINE)

print(f"Found {len(AllInstances)} instances across {AccountNum} accounts across {RegionNum} regions")
print()
print("Thank you for using this script")
print()
4 changes: 2 additions & 2 deletions inventory-scripts/all_my_instances.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import logging

init()
__version__ = "2024.05.09"
__version__ = "2024.09.24"
ERASE_LINE = '\x1b[2K'
begin_time = time()

Expand Down Expand Up @@ -79,7 +79,7 @@ def run(self):
try:
# Now go through those stacksets and determine the instances, made up of accounts and regions
# Most time spent in this loop
Instances = Inventory_Modules.find_account_instances2(c_account_credentials, c_account_credentials['Region'])
Instances = Inventory_Modules.find_account_instances2(c_account_credentials)
logging.info(f"Account: {c_account_credentials['AccountId']} Region: {c_account_credentials['Region']} | Found {len(Instances['Reservations'])} instances")
State = InstanceType = InstanceId = PublicDnsName = Name = ""
if 'Reservations' in Instances.keys():
Expand Down
4 changes: 2 additions & 2 deletions inventory-scripts/all_my_rds_instances.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

init()

__version__ = '2023.11.06'
__version__ = '2024.09.23'

def parse_args(args):
"""
Expand Down Expand Up @@ -44,7 +44,7 @@ def check_accounts_for_instances(faws_acct: aws_acct_access, fRegionList: list =
AllInstances = []
Instances = dict()
if fRegionList is None:
fRegionList = ['us-east-1']
fRegionList = [faws_acct.Region]
for account in ChildAccounts:
acct_instances = []
logging.info(f"Connecting to account {account['AccountId']}")
Expand Down
2 changes: 1 addition & 1 deletion inventory-scripts/find_security_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import logging

init()
__version__ = '2024.08.24'
__version__ = '2024.09.24'
ERASE_LINE = '\x1b[2K'
begin_time = time()

Expand Down
Loading

0 comments on commit 800c514

Please sign in to comment.