├── awsthreatprep ├── __init__.py ├── config.py ├── common.py ├── cloudtrail_checks.py ├── misc_checks.py ├── s3_checks.py ├── checker.py └── iam_checks.py ├── requirements.txt ├── docs ├── _static │ └── threatresponse_logo.png ├── index.rst ├── about.rst ├── installing.rst ├── Makefile ├── quickstart.rst └── conf.py ├── setup.py ├── LICENSE ├── .gitignore └── README.md /awsthreatprep/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | boto3 2 | -------------------------------------------------------------------------------- /docs/_static/threatresponse_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThreatResponse/ThreatPrep/HEAD/docs/_static/threatresponse_logo.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | 2 | ========== 3 | ThreatPrep 4 | ========== 5 | 6 | Configuration and Preparedness Auditing for AWS Accounts 7 | 8 | .. toctree:: 9 | :maxdepth: 2 10 | 11 | quickstart 12 | installing 13 | about 14 | 15 | -------------------------------------------------------------------------------- /awsthreatprep/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | config = { 4 | #iam 5 | 'ACCOUNT_INACTIVE_DAYS': 30, #Accounts are inactive if not used for 30 days 6 | 'PASSWORD_ROTATION_DAYS': 90, #Paswords should be rotated every 90 days 7 | 'ACCESS_KEY_ROTATION_DAYS': 90 #Access Keys should be rotated every 90 days 8 | } 9 | -------------------------------------------------------------------------------- /docs/about.rst: -------------------------------------------------------------------------------- 1 | 2 | ===== 3 | About 4 | ===== 5 | 6 | ThreatPrep is a part of the `Threat Response project `_. 7 | 8 | License 9 | ******* 10 | 11 | ThreatPrep is distributed under `the MIT License (MIT) `_. 12 | 13 | .. image:: _static/threatresponse_logo.png 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.command.build import build 2 | from setuptools import setup 3 | from setuptools.command.install import install as _install 4 | 5 | 6 | class install(_install): 7 | def run(self): 8 | self.run_command('build') 9 | _install.run(self) 10 | 11 | setup(name="awsthreatprep", 12 | version="0.1.1", 13 | author="Alex McCormack", 14 | author_email="developer@amccormack.net", 15 | packages=["awsthreatprep"], 16 | license="MIT", 17 | description="TODO", 18 | use_2to3=True, 19 | install_requires=['boto3']) 20 | -------------------------------------------------------------------------------- /docs/installing.rst: -------------------------------------------------------------------------------- 1 | 2 | ============ 3 | Installation 4 | ============ 5 | 6 | Install From PyPi 7 | ***************** 8 | 9 | .. code-block:: bash 10 | 11 | $ pip install awsthreatprep 12 | 13 | Install From Github 14 | ******************* 15 | 16 | .. code-block:: bash 17 | 18 | $ pip install git+git://github.com/ThreatResponse/ThreatPrep@master 19 | 20 | 21 | Docker Example 22 | ************** 23 | 24 | Below we show a working installation procedure in a minimized `python docker container `__. 25 | 26 | .. code-block:: bash 27 | 28 | $ docker run -it -e AWS_ACCESS_KEY_ID=AWSACCESSKEYHERE -e AWS_SECRET_ACCESS_KEY=AWSSECRETACCESSKEYHERE python:2 bash 29 | root@3009:/# pip install awsthreatprep 30 | 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 ThreatResponse 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /awsthreatprep/common.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | class CheckState(object): 4 | """Enum for describing the state of a check.""" 5 | UNTESTED = 'UNTESTED' 6 | PASS = 'PASS' 7 | FAIL = 'FAIL' 8 | ERROR = 'ERROR' 9 | 10 | class BaseCheck(object): 11 | """Base class for checks.""" 12 | def __init__(self, 13 | resource_name='', category='', status=CheckState.UNTESTED, 14 | subchecks=None, reason='' ): 15 | self.resource_name = resource_name 16 | self.category = category 17 | self.status = status 18 | self.subchecks = subchecks or [] 19 | self.reason = reason 20 | def get_description(self): 21 | """Gets the docstring of the instance.""" 22 | return re.sub('\n\W+',' ', self.__doc__) 23 | 24 | def get_check_name(self): 25 | """Gets the class name without the module and trims the '> at the end. 26 | """ 27 | return str(self.__class__).split('.')[-1][:-2] 28 | def __str__(self): 29 | check_name = self.get_check_name() 30 | if len(self.subchecks)==0: 31 | reason = self.reason 32 | else: 33 | passing_checks = filter( 34 | lambda x: x.status==CheckState.PASS, 35 | self.subchecks 36 | ) 37 | reason = '{0}/{1} PASS'.format( 38 | len(passing_checks), 39 | len(self.subchecks) 40 | ) 41 | return '{status:<8} {check_name:<12} {resource_name} {reason}'.format( 42 | status=self.status, 43 | check_name=check_name, 44 | resource_name=self.resource_name, 45 | reason=reason 46 | ) 47 | def to_dict(self): 48 | return dict( 49 | status = self.status, 50 | category = self.category, 51 | check_name = self.get_check_name(), 52 | description = self.get_description(), 53 | resource_name = self.resource_name, 54 | reason = self.reason, 55 | subchecks = [ x.to_dict() for x in self.subchecks] 56 | ) 57 | -------------------------------------------------------------------------------- /awsthreatprep/cloudtrail_checks.py: -------------------------------------------------------------------------------- 1 | 2 | import common 3 | 4 | class CloudTrailCheckCollection(common.BaseCheck): 5 | """Checks for basic CloudTrail security settings.""" 6 | 7 | def __init__(self): 8 | return super(CloudTrailCheckCollection, self).__init__(category='CloudTrail') 9 | 10 | def collect_tests(self, trail): 11 | self.resource_name = trail['Name'] 12 | self.subchecks.append(CloudTrailFileValidationCheck(trail)) 13 | if all(x==common.CheckState.PASS for x in self.subchecks): 14 | self.status = common.CheckState.PASS 15 | else: 16 | self.status = common.CheckState.FAIL 17 | 18 | class CloudTrailCheck(common.BaseCheck): 19 | """Baseclass for CloudTrail checks.""" 20 | 21 | def __init__(self, trail): 22 | self.category='CloudTrail' 23 | self.trail = trail 24 | self.resource_name = self.trail['Name'] 25 | self.status = common.CheckState.UNTESTED 26 | self.subchecks = [] 27 | self.reason = '' 28 | self.test() 29 | 30 | def test(self): 31 | raise NotImplemented('Subclasses should be used') 32 | 33 | class CloudTrailFileValidationCheck(CloudTrailCheck): 34 | """Check if LogFileValidation is enabled.""" 35 | 36 | def test(self): 37 | if self.trail['LogFileValidationEnabled']: 38 | self.reason = 'LogFileValidation is enabled.' 39 | self.status = common.CheckState.PASS 40 | else: 41 | self.reason = 'LogFileValidation is not enabled.' 42 | self.status = common.CheckState.FAIL 43 | 44 | class CloudTrailLogAllCheck(common.BaseCheck): 45 | """Checks if a logs exists that is MultiRegional and includes global service 46 | events.""" 47 | 48 | def __init__(self, trails): 49 | self.category='CloudTrail' 50 | self.trails = trails 51 | self.resource_name = '' 52 | self.subchecks = [] 53 | self.status = common.CheckState.UNTESTED 54 | self.reason = '' 55 | self.test() 56 | 57 | def test(self): 58 | multiregional_trails = filter( 59 | lambda x: x['IsMultiRegionTrail'] == True and \ 60 | x['IncludeGlobalServiceEvents'] == True, 61 | self.trails 62 | ) 63 | if len(multiregional_trails) > 0: 64 | self.reason = 'Multiregional trails are enabled.' 65 | self.status = common.CheckState.PASS 66 | else: 67 | self.reason = 'No multiregional trails.' 68 | self.status = common.CheckState.FAIL 69 | -------------------------------------------------------------------------------- /awsthreatprep/misc_checks.py: -------------------------------------------------------------------------------- 1 | import common 2 | import boto3 3 | 4 | class CloudWatchBillingAlertEnabledCollection(common.BaseCheck): 5 | """Checks each region to see if there is an Alert based on Billing metrics.""" 6 | def __init__(self, regions): 7 | self.category='CloudWatch' 8 | self.resource_name='' 9 | self.subchecks = [] 10 | self.regions = regions 11 | self.test() 12 | def test(self): 13 | for region in self.regions: 14 | cloudwatch=boto3.resource('cloudwatch',region_name=region) 15 | self.subchecks.append( 16 | CloudWatchBillingAlertEnabledCheck( 17 | region, 18 | cloudwatch.alarms.all() 19 | ) 20 | ) 21 | if any(x.status == common.CheckState.PASS for x in self.subchecks): 22 | self.status = common.CheckState.PASS 23 | self.reason = 'Atleast one CloudWatch Billing Alert is Enabled' 24 | else: 25 | self.status = common.CheckState.FAIL 26 | self.reason = 'No CloudWatch Billing Alerts are Enabled' 27 | 28 | 29 | class CloudWatchBillingAlertEnabledCheck(common.BaseCheck): 30 | """Checks to see if there is an Alert based on Billing metrics.""" 31 | 32 | 33 | ''' 34 | # This code does not seem to find results 35 | response = self.cloudwatch_client.describe_alarms_for_metric( 36 | MetricName = 'EstimatedCost', 37 | Namespace='AWS/Billing' 38 | ) 39 | ''' 40 | 41 | def __init__(self, region_name, all_alarms): 42 | self.category='CloudWatch' 43 | self.resource_name = region_name 44 | self.subchecks = [] 45 | self.all_alarms = all_alarms 46 | self.test() 47 | 48 | def test(self): 49 | billing_alerts = filter( 50 | lambda x: x.namespace == 'AWS/Billing' and \ 51 | x.metric_name == 'EstimatedCharges', 52 | self.all_alarms 53 | ) 54 | self.reason = '{0} Billing alerts are enabled in CloudWatch region {1}.'.format( 55 | len(billing_alerts), self.resource_name 56 | ) 57 | if len(billing_alerts) > 0: 58 | self.status = common.CheckState.PASS 59 | else: 60 | self.status = common.CheckState.FAIL 61 | 62 | class VPCFlowLogCheck(common.BaseCheck): 63 | """Checks if a VPC has flow logging enabled and if the logging occurs 64 | without error.""" 65 | 66 | def __init__(self, vpc_id, vpc_dict=None): 67 | self.category='VPC' 68 | self.vpc_id = vpc_id 69 | self.vpc_dict = vpc_dict 70 | self.resource_name = vpc_id 71 | self.status = common.CheckState.UNTESTED 72 | self.subchecks = [] 73 | self.reason = '' 74 | self.test() 75 | def test(self): 76 | if self.vpc_dict == None: 77 | self.status = common.CheckState.FAIL 78 | self.reason = "No flow log found" 79 | return 80 | error_message = self.vpc_dict.get('DeliverLogsErrorMessage', '') 81 | if error_message != '': 82 | self.status = common.CheckState.FAIL 83 | self.reason = 'Flow log has DeliverLogsErrorMessage: {0}'.format( 84 | error_message 85 | ) 86 | return 87 | flowlog_status = self.vpc_dict.get('FlowLogStatus', '') 88 | if flowlog_status != 'ACTIVE': 89 | self.status = common.CheckState.self.FAIL 90 | self.reason = 'Flow log has FlowLogStatus: {0}'.format( 91 | flowlog_status 92 | ) 93 | return 94 | self.status = common.CheckState.PASS 95 | -------------------------------------------------------------------------------- /awsthreatprep/s3_checks.py: -------------------------------------------------------------------------------- 1 | import common 2 | 3 | class S3CheckCollection(common.BaseCheck): 4 | """Checks for basic S3 security settings.""" 5 | 6 | def __init__(self): 7 | return super(S3CheckCollection, self ).__init__(category='S3') 8 | 9 | def collect_tests(self, s3_object): 10 | self.resource_name = s3_object.name 11 | self.subchecks.append(S3VersioningEnabledCheck(s3_object)) 12 | self.subchecks.append(S3LoggingEnabledCheck(s3_object)) 13 | self.subchecks.append(S3OpenPermissionCheck(s3_object, 'READ')) 14 | self.subchecks.append(S3OpenPermissionCheck(s3_object, 'WRITE')) 15 | if all(x.status==common.CheckState.PASS for x in self.subchecks): 16 | self.status = common.CheckState.PASS 17 | else: 18 | self.status = common.CheckState.FAIL 19 | 20 | class S3VersioningEnabledCheck(common.BaseCheck): 21 | """Checks if versioning is enabled on a S3 bucket.""" 22 | 23 | def __init__(self, s3_object): 24 | self.s3_name= s3_object.name 25 | self.resource_name= s3_object.name 26 | self.s3_object = s3_object 27 | self.status = common.CheckState.UNTESTED 28 | self.category = 'S3' 29 | self.reason = '' 30 | self.subchecks = [] 31 | self.test() 32 | 33 | def test(self): 34 | versioning_enabled = self.s3_object.Versioning().status == 'Enabled' 35 | if versioning_enabled: 36 | self.status = common.CheckState.FAIL 37 | self.reason = 'S3 versioning is not enabled for this bucket' 38 | else: 39 | self.status = common.CheckState.PASS 40 | self.reason = 'S3 versioning is enabled for this bucket' 41 | 42 | class S3LoggingEnabledCheck(common.BaseCheck): 43 | """Checks if logging is enabled on a S3 bucket.""" 44 | 45 | def __init__(self, s3_object): 46 | self.category = 'S3' 47 | self.s3_name= s3_object.name 48 | self.resource_name= s3_object.name 49 | self.s3_object = s3_object 50 | self.status = common.CheckState.UNTESTED 51 | self.reason = '' 52 | self.subchecks = [] 53 | self.test() 54 | 55 | def test(self): 56 | logging_enabled = self.s3_object.Logging().logging_enabled is not None 57 | if logging_enabled: 58 | self.status = common.CheckState.FAIL 59 | self.reason = 'S3 logging is not enabled for this bucket' 60 | else: 61 | self.status = common.CheckState.PASS 62 | self.reason = 'S3 logging is enabled for this bucket' 63 | 64 | class S3OpenPermissionCheck(common.BaseCheck): 65 | """Checks for a permission open to the world on a S3 bucket.""" 66 | 67 | def __init__(self, s3_object, permission="READ"): 68 | self.category = 'S3' 69 | self.s3_name= s3_object.name 70 | self.resource_name= s3_object.name 71 | self.permission = permission 72 | self.s3_object = s3_object 73 | self.status = common.CheckState.UNTESTED 74 | self.reason = '' 75 | self.subchecks = [] 76 | self.test() 77 | 78 | def grant_is_open_read(self, grant): 79 | result = grant['Grantee'].get('Type', None)== 'Group' and \ 80 | grant['Grantee'].get('URI',None) == \ 81 | 'http://acs.amazonaws.com/groups/global/AllUsers' and \ 82 | grant['Permission'] == self.permission 83 | return result 84 | 85 | def test(self): 86 | grants = self.s3_object.Acl().grants 87 | if any( self.grant_is_open_read(grant) for grant in grants ): 88 | self.status = common.CheckState.FAIL 89 | self.reason = 'S3 permission {0} is granted to AllUsers'.format( 90 | self.permission 91 | ) 92 | 93 | else: 94 | self.status = common.CheckState.PASS 95 | self.reason = 'S3 permission "{0}" is not granted to AllUsers'.format( 96 | self.permission 97 | ) 98 | -------------------------------------------------------------------------------- /awsthreatprep/checker.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import boto3 3 | import botocore 4 | import csv 5 | import datetime 6 | import dateutil 7 | import StringIO 8 | import time 9 | import os 10 | 11 | import cloudtrail_checks 12 | import s3_checks 13 | import iam_checks 14 | import misc_checks 15 | import collections 16 | import json 17 | 18 | 19 | class Checker(object): 20 | def __init__(self, region='us-east-1'): 21 | self.check_categories = ['S3','IAM', 'VPC', 'CloudWatch', 'CloudTrail'] 22 | self.ec2 = boto3.resource("ec2", region_name=region) 23 | self.ec2_client = boto3.client("ec2", region_name=region) 24 | self.cloudwatch = boto3.resource("cloudwatch", region_name=region) 25 | self.cloudwatch_client = boto3.client("cloudwatch", region_name=region) 26 | self.cloudtrail_client = boto3.client('cloudtrail', region_name=region) 27 | self.iam = boto3.resource("iam", region_name=region) 28 | self.iam_client = boto3.client("iam", region_name=region) 29 | self.s3 = boto3.resource("s3", region_name=region) 30 | 31 | self.results = [] 32 | self.results_dict = {} 33 | 34 | def append_general(self, check): 35 | self.results.append(check) 36 | if check.category not in self.results_dict: 37 | self.results_dict[check.category] =dict(generals=[], collections=[]) 38 | self.results_dict[check.category]['generals'].append(check.to_dict()) 39 | 40 | def append_collection(self, check): 41 | self.results.append(check) 42 | if check.category not in self.results_dict: 43 | self.results_dict[check.category] =dict(generals=[], collections=[]) 44 | self.results_dict[check.category]['collections'].append(check.to_dict()) 45 | 46 | def run_checks(self, check_categories=None): 47 | check_categories = check_categories or self.check_categories 48 | if type(check_categories) != list: 49 | check_categories = [ check_categories ] 50 | self.results_dict = { 51 | key:dict(generals=[],collections=[]) 52 | for key in check_categories 53 | } 54 | for category in check_categories: 55 | if category == 'S3': 56 | self.s3_check() 57 | elif category == 'IAM': 58 | self.iam_checks() 59 | elif category == 'VPC': 60 | self.check_vpcs() 61 | elif category == 'CloudWatch': 62 | self.check_cloudwatch_billing() 63 | elif category == 'CloudTrail': 64 | self.cloudtrail_checks() 65 | 66 | 67 | def get_flattened_results(self): 68 | results = [] 69 | for x in self.results: 70 | results.append(x) 71 | results.extend(x.subchecks) 72 | return results 73 | 74 | def get_category_stats(self): 75 | """Get a count of CheckState results for each category of checks. 76 | Ignore collection counts to avoid duplications""" 77 | flat_results = self.get_flattened_results() 78 | categories = list(set([x.category for x in flat_results])) 79 | metrics = {} 80 | for category in categories: 81 | metrics[category] = collections.Counter([ 82 | x.status for x in filter( 83 | lambda y: len(y.subchecks) == 0 and y.category==category, 84 | flat_results 85 | ) 86 | ]) 87 | return metrics 88 | 89 | def get_iam_credential_report(self): 90 | report = None 91 | while report == None: 92 | try: 93 | report = self.iam_client.get_credential_report() 94 | except botocore.exceptions.ClientError as e: 95 | if 'ReportNotPresent' in e.message: 96 | self.iam_client.generate_credential_report() 97 | else: 98 | raise e 99 | time.sleep(5) 100 | document = StringIO.StringIO(report['Content']) 101 | reader = csv.DictReader(document) 102 | report_rows = [] 103 | for row in reader: 104 | report_rows.append(row) 105 | return report_rows 106 | 107 | def get_flowlogs_by_vpc_id(self, ec2_client): 108 | ''' Returns a dict of vpc_id:flow_log_dict ''' 109 | response = ec2_client.describe_flow_logs() 110 | return { x['ResourceId']:x for x in response['FlowLogs'] } 111 | 112 | def check_vpcs(self): 113 | #collect vpc ids 114 | regions = get_regions() 115 | for region in regions: 116 | ec2 = boto3.resource('ec2', region_name=region) 117 | ec2_client = boto3.client('ec2', region_name=region) 118 | ids = [ x.id for x in ec2.vpcs.all() ] 119 | flowlogs = self.get_flowlogs_by_vpc_id(ec2_client) 120 | 121 | for vpc_id in ids: 122 | vpc_dict = flowlogs.get(vpc_id, None) 123 | self.append_collection( 124 | misc_checks.VPCFlowLogCheck(vpc_id, vpc_dict) 125 | ) 126 | 127 | def check_cloudwatch_billing(self): 128 | regions = get_regions() 129 | self.append_collection( 130 | misc_checks.CloudWatchBillingAlertEnabledCollection( 131 | regions 132 | ) 133 | ) 134 | def s3_check(self): 135 | for bucket in self.s3.buckets.all(): 136 | check = s3_checks.S3CheckCollection() 137 | check.collect_tests(bucket) 138 | self.append_collection(check) 139 | 140 | def iam_checks(self): 141 | report_rows = self.get_iam_credential_report() 142 | self.append_general(iam_checks.IAMRootAccessKeyDisabled(report_rows)) 143 | self.append_general( 144 | iam_checks.IAMRolesAreCreatedCheck(self.iam.roles.all()) 145 | ) 146 | for row in report_rows: 147 | check = iam_checks.IAMUserCheckCollection() 148 | check.collect_tests(row) 149 | self.append_collection(check) 150 | 151 | def cloudtrail_checks(self): 152 | trail_list = self.cloudtrail_client.describe_trails()['trailList'] 153 | self.append_general( 154 | cloudtrail_checks.CloudTrailLogAllCheck(trail_list) 155 | ) 156 | for trail in trail_list: 157 | check = cloudtrail_checks.CloudTrailCheckCollection() 158 | check.collect_tests(trail) 159 | self.append_collection(check) 160 | 161 | def print_results(self): 162 | for result in self.results: 163 | print result 164 | if len(result.subchecks) > 0: 165 | for subcheck in result.subchecks: 166 | print ' -',subcheck 167 | print '' 168 | 169 | def get_regions(): 170 | client = boto3.client('ec2', region_name='us-east-1') 171 | regions = [ x['RegionName'] for x in client.describe_regions()['Regions']] 172 | return regions 173 | 174 | if __name__ == '__main__': 175 | checker = Checker() 176 | checker.run_checks() 177 | results = checker.results_dict 178 | 179 | print json.dumps(results, indent=4) 180 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help 18 | help: 19 | @echo "Please use \`make ' where is one of" 20 | @echo " html to make standalone HTML files" 21 | @echo " dirhtml to make HTML files named index.html in directories" 22 | @echo " singlehtml to make a single large HTML file" 23 | @echo " pickle to make pickle files" 24 | @echo " json to make JSON files" 25 | @echo " htmlhelp to make HTML files and a HTML help project" 26 | @echo " qthelp to make HTML files and a qthelp project" 27 | @echo " applehelp to make an Apple Help Book" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " epub3 to make an epub3" 31 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 32 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 33 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 34 | @echo " text to make text files" 35 | @echo " man to make manual pages" 36 | @echo " texinfo to make Texinfo files" 37 | @echo " info to make Texinfo files and run them through makeinfo" 38 | @echo " gettext to make PO message catalogs" 39 | @echo " changes to make an overview of all changed/added/deprecated items" 40 | @echo " xml to make Docutils-native XML files" 41 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 42 | @echo " linkcheck to check all external links for integrity" 43 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 44 | @echo " coverage to run coverage check of the documentation (if enabled)" 45 | @echo " dummy to check syntax errors of document sources" 46 | 47 | .PHONY: clean 48 | clean: 49 | rm -rf $(BUILDDIR)/* 50 | 51 | .PHONY: html 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | .PHONY: dirhtml 58 | dirhtml: 59 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 60 | @echo 61 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 62 | 63 | .PHONY: singlehtml 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | .PHONY: pickle 70 | pickle: 71 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 72 | @echo 73 | @echo "Build finished; now you can process the pickle files." 74 | 75 | .PHONY: json 76 | json: 77 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 78 | @echo 79 | @echo "Build finished; now you can process the JSON files." 80 | 81 | .PHONY: htmlhelp 82 | htmlhelp: 83 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 84 | @echo 85 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 86 | ".hhp project file in $(BUILDDIR)/htmlhelp." 87 | 88 | .PHONY: qthelp 89 | qthelp: 90 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 91 | @echo 92 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 93 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 94 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/threat_prep.qhcp" 95 | @echo "To view the help file:" 96 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/threat_prep.qhc" 97 | 98 | .PHONY: applehelp 99 | applehelp: 100 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 101 | @echo 102 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 103 | @echo "N.B. You won't be able to view it unless you put it in" \ 104 | "~/Library/Documentation/Help or install it in your application" \ 105 | "bundle." 106 | 107 | .PHONY: devhelp 108 | devhelp: 109 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 110 | @echo 111 | @echo "Build finished." 112 | @echo "To view the help file:" 113 | @echo "# mkdir -p $$HOME/.local/share/devhelp/threat_prep" 114 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/threat_prep" 115 | @echo "# devhelp" 116 | 117 | .PHONY: epub 118 | epub: 119 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 120 | @echo 121 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 122 | 123 | .PHONY: epub3 124 | epub3: 125 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 126 | @echo 127 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 128 | 129 | .PHONY: latex 130 | latex: 131 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 132 | @echo 133 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 134 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 135 | "(use \`make latexpdf' here to do that automatically)." 136 | 137 | .PHONY: latexpdf 138 | latexpdf: 139 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 140 | @echo "Running LaTeX files through pdflatex..." 141 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 142 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 143 | 144 | .PHONY: latexpdfja 145 | latexpdfja: 146 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 147 | @echo "Running LaTeX files through platex and dvipdfmx..." 148 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 149 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 150 | 151 | .PHONY: text 152 | text: 153 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 154 | @echo 155 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 156 | 157 | .PHONY: man 158 | man: 159 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 160 | @echo 161 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 162 | 163 | .PHONY: texinfo 164 | texinfo: 165 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 166 | @echo 167 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 168 | @echo "Run \`make' in that directory to run these through makeinfo" \ 169 | "(use \`make info' here to do that automatically)." 170 | 171 | .PHONY: info 172 | info: 173 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 174 | @echo "Running Texinfo files through makeinfo..." 175 | make -C $(BUILDDIR)/texinfo info 176 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 177 | 178 | .PHONY: gettext 179 | gettext: 180 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 181 | @echo 182 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 183 | 184 | .PHONY: changes 185 | changes: 186 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 187 | @echo 188 | @echo "The overview file is in $(BUILDDIR)/changes." 189 | 190 | .PHONY: linkcheck 191 | linkcheck: 192 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 193 | @echo 194 | @echo "Link check complete; look for any errors in the above output " \ 195 | "or in $(BUILDDIR)/linkcheck/output.txt." 196 | 197 | .PHONY: doctest 198 | doctest: 199 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 200 | @echo "Testing of doctests in the sources finished, look at the " \ 201 | "results in $(BUILDDIR)/doctest/output.txt." 202 | 203 | .PHONY: coverage 204 | coverage: 205 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 206 | @echo "Testing of coverage in the sources finished, look at the " \ 207 | "results in $(BUILDDIR)/coverage/python.txt." 208 | 209 | .PHONY: xml 210 | xml: 211 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 212 | @echo 213 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 214 | 215 | .PHONY: pseudoxml 216 | pseudoxml: 217 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 218 | @echo 219 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 220 | 221 | .PHONY: dummy 222 | dummy: 223 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 224 | @echo 225 | @echo "Build finished. Dummy builder generates no files." 226 | -------------------------------------------------------------------------------- /docs/quickstart.rst: -------------------------------------------------------------------------------- 1 | 2 | =========== 3 | Quick Start 4 | =========== 5 | 6 | First, :doc:`Install awsthreatprep `. 7 | 8 | IAM Policy 9 | ********** 10 | 11 | The following policy can be used to run ThreatPrep. It is a reduced version of the ReadOnlyAccess policy (arn:aws:iam::aws:policy/ReadOnlyAccess). 12 | 13 | .. code-block:: python 14 | 15 | { 16 | "Version": "2012-10-17", 17 | "Statement": [ 18 | { 19 | "Effect": "Allow", 20 | "Action": [ 21 | "cloudtrail:DescribeTrails", 22 | "cloudwatch:DescribeAlarms", 23 | "ec2:DescribeFlowLogs", 24 | "ec2:DescribeRegions", 25 | "ec2:DescribeVpcs", 26 | "iam:GenerateCredentialReport", 27 | "iam:GetCredentialReport", 28 | "iam:ListAttachedUserPolicies", 29 | "iam:ListRoles", 30 | "s3:GetBucketAcl", 31 | "s3:GetBucketLogging", 32 | "s3:GetBucketVersioning", 33 | "s3:ListAllMyBuckets", 34 | "s3:ListBucket" 35 | ], 36 | "Resource": "*" 37 | } 38 | ] 39 | } 40 | 41 | Docker Example 42 | ************** 43 | 44 | Below we show a working installation procedure in a minimized `python docker container `__. 45 | 46 | .. code-block:: bash 47 | 48 | $ docker run -it -e AWS_ACCESS_KEY_ID=AWSACCESSKEYHERE -e AWS_SECRET_ACCESS_KEY=AWSSECRETACCESSKEYHERE python:2 bash 49 | 50 | root@3009:/# pip install awsthreatprep 51 | Collecting awsthreatprep 52 | Collecting boto3 (from awsthreatprep==0.1.1) 53 | Downloading boto3-1.4.0-py2.py3-none-any.whl (117kB) 54 | 100% |████████████████████████████████| 122kB 1.2MB/s 55 | Collecting jmespath<1.0.0,>=0.7.1 (from boto3->awsthreatprep==0.1.1) 56 | Downloading jmespath-0.9.0-py2.py3-none-any.whl 57 | Collecting s3transfer<0.2.0,>=0.1.0 (from boto3->awsthreatprep==0.1.1) 58 | Downloading s3transfer-0.1.2-py2.py3-none-any.whl (49kB) 59 | 100% |████████████████████████████████| 51kB 1.1MB/s 60 | Collecting botocore<1.5.0,>=1.4.1 (from boto3->awsthreatprep==0.1.1) 61 | Downloading botocore-1.4.49-py2.py3-none-any.whl (2.5MB) 62 | 100% |████████████████████████████████| 2.5MB 641kB/s 63 | Collecting futures<4.0.0,>=2.2.0; python_version == "2.6" or python_version == "2.7" (from s3transfer<0.2.0,>=0.1.0->boto3->awsthreatprep==0.1.1) 64 | Downloading futures-3.0.5-py2-none-any.whl 65 | Collecting docutils>=0.10 (from botocore<1.5.0,>=1.4.1->boto3->awsthreatprep==0.1.1) 66 | Downloading docutils-0.12.tar.gz (1.6MB) 67 | 100% |████████████████████████████████| 1.6MB 1.0MB/s 68 | Collecting python-dateutil<3.0.0,>=2.1 (from botocore<1.5.0,>=1.4.1->boto3->awsthreatprep==0.1.1) 69 | Downloading python_dateutil-2.5.3-py2.py3-none-any.whl (201kB) 70 | 100% |████████████████████████████████| 204kB 1.9MB/s 71 | Collecting six>=1.5 (from python-dateutil<3.0.0,>=2.1->botocore<1.5.0,>=1.4.1->boto3->awsthreatprep==0.1.1) 72 | Downloading six-1.10.0-py2.py3-none-any.whl 73 | Building wheels for collected packages: docutils 74 | Running setup.py bdist_wheel for docutils ... done 75 | Stored in directory: /root/.cache/pip/wheels/db/de/bd/b99b1e12d321fbc950766c58894c6576b1a73ae3131b29a151 76 | Successfully built docutils 77 | Installing collected packages: jmespath, futures, docutils, six, python-dateutil, botocore, s3transfer, boto3, awsthreatprep 78 | Running setup.py install for awsthreatprep ... done 79 | Successfully installed awsthreatprep-0.1.1 boto3-1.4.0 botocore-1.4.49 docutils-0.12 futures-3.0.5 jmespath-0.9.0 python-dateutil-2.5.3 s3transfer-0.1.2 six-1.10.0 80 | 81 | root@3009:/# python -m awsthreatprep.checker > output.json 82 | root@3009:/# head -n 30 output.json 83 | { 84 | "S3": { 85 | "generals": [], 86 | "collections": [ 87 | { 88 | "category": "S3", 89 | "status": "FAIL", 90 | "reason": "", 91 | "resource_name": "threatpreptest45", 92 | "description": "Checks for basic S3 security settings.", 93 | "check_name": "S3CheckCollection", 94 | "subchecks": [ 95 | { 96 | "category": "S3", 97 | "status": "PASS", 98 | "reason": "S3 versioning is enabled for this bucket", 99 | "resource_name": "threatpreptest45", 100 | "description": "Checks if versioning is enabled on a S3 bucket.", 101 | "check_name": "S3VersioningEnabledCheck", 102 | "subchecks": [] 103 | }, 104 | { 105 | "category": "S3", 106 | "status": "PASS", 107 | "reason": "S3 logging is enabled for this bucket", 108 | "resource_name": "threatpreptest45", 109 | "description": "Checks if logging is enabled on a S3 bucket.", 110 | "check_name": "S3LoggingEnabledCheck", 111 | "subchecks": [] 112 | }, 113 | 114 | 115 | 116 | Import from module 117 | ****************** 118 | 119 | .. code-block:: python 120 | 121 | from awsthreatprep.checker import Checker 122 | c = Checker() 123 | 124 | How to use 125 | ---------- 126 | 127 | .. code-block:: python 128 | 129 | >>> import pprint 130 | >>> from awsthreatprep.checker import Checker 131 | >>> c = Checker() 132 | >>> c.run_checks() 133 | >>> c.results_dict.keys() 134 | ['S3', 'IAM', 'CloudWatch', 'VPC', 'CloudTrail'] 135 | >>> c.results_dict['S3'].keys() 136 | ['generals', 'collections'] 137 | >>> pprint.pprint(c.results_dict['S3']['collections'][0]) 138 | {'category': 'S3', 139 | 'check_name': 'S3CheckCollection', 140 | 'description': 'Checks for basic S3 security settings.', 141 | 'reason': '', 142 | 'resource_name': 'threatpreptest45', 143 | 'status': 'FAIL', 144 | 'subchecks': [{'category': 'S3', 145 | 'check_name': 'S3VersioningEnabledCheck', 146 | 'description': 'Checks if versioning is enabled on a S3 bucket.', 147 | 'reason': 'S3 versioning is enabled for this bucket', 148 | 'resource_name': 'threatpreptest45', 149 | 'status': 'PASS', 150 | 'subchecks': []}, 151 | {'category': 'S3', 152 | 'check_name': 'S3LoggingEnabledCheck', 153 | 'description': 'Checks if logging is enabled on a S3 bucket.', 154 | 'reason': 'S3 logging is enabled for this bucket', 155 | 'resource_name': 'threatpreptest45', 156 | 'status': 'PASS', 157 | 'subchecks': []}, 158 | {'category': 'S3', 159 | 'check_name': 'S3OpenPermissionCheck', 160 | 'description': 'Checks for a permission open to the world on a S3 bucket.', 161 | 'reason': 'S3 permission READ is granted to AllUsers', 162 | 'resource_name': 'threatpreptest45', 163 | 'status': 'FAIL', 164 | 'subchecks': []}, 165 | {'category': 'S3', 166 | 'check_name': 'S3OpenPermissionCheck', 167 | 'description': 'Checks for a permission open to the world on a S3 bucket.', 168 | 'reason': 'S3 permission "WRITE" is not granted to AllUsers', 169 | 'resource_name': 'threatpreptest45', 170 | 'status': 'PASS', 171 | 'subchecks': []}]} 172 | 173 | Organization of Results 174 | *********************** 175 | 176 | After running the ``run_checks`` method, the results are organized in the ``results_dict`` property of the Checker object. There are four keys in the ``results_dict`` dictionary: ``S3``, ``IAM``, ``CloudWatch``, ``VPC``, ``CloudTrail``. Each of these keys represents an AWS service or feature where checks are run. 177 | 178 | These groups are further broken down into two more categories: ``generals`` or ``collections``. If a check is a general check, it is looking for something that is not specific to a particular resource, such as determining if any CloudTrail trails exist. If a check is a collections check, it is running (usually multiple) checks on a single resource. 179 | 180 | In the example above, the S3 bucket ``threatpreptest45`` is the resource being checked by the ``S3CheckCollection``, the first result in the ``S3`` group of ``collections`` checks. The status of a ``CheckCollection`` is ``PASS`` if all of the subchecks are ``PASS``, otherwise, it is ``FAIL``. 181 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ThreatPrep 2 | Configuration and Preparedness Auditing for AWS Accounts 3 | 4 | ## Installing 5 | You can use `pip` to install the `awsthreatprep` module from this git repository by using the command: 6 | 7 | ``` 8 | pip install git+git://github.com/ThreatResponse/ThreatPrep@master 9 | ``` 10 | 11 | ### Installation example with docker 12 | Below we show a working installation procedure in a minimized [python docker container](https://hub.docker.com/_/python/). 13 | 14 | ```sh 15 | $ docker run -it -e AWS_ACCESS_KEY_ID=AWSACCESSKEYHERE -e AWS_SECRET_ACCESS_KEY=AWSSECRETACCESSKEYHERE python:2 bash 16 | 17 | root@3009:/# pip install git+git://github.com/ThreatResponse/ThreatPrep@master 18 | Collecting git+git://github.com/ThreatResponse/ThreatPrep@master 19 | Cloning git://github.com/ThreatResponse/ThreatPrep (to master) to /tmp/pip-qwKkjA-build 20 | Collecting boto3 (from awsthreatprep==0.1.1) 21 | Downloading boto3-1.4.0-py2.py3-none-any.whl (117kB) 22 | 100% |████████████████████████████████| 122kB 1.2MB/s 23 | Collecting jmespath<1.0.0,>=0.7.1 (from boto3->awsthreatprep==0.1.1) 24 | Downloading jmespath-0.9.0-py2.py3-none-any.whl 25 | Collecting s3transfer<0.2.0,>=0.1.0 (from boto3->awsthreatprep==0.1.1) 26 | Downloading s3transfer-0.1.2-py2.py3-none-any.whl (49kB) 27 | 100% |████████████████████████████████| 51kB 1.1MB/s 28 | Collecting botocore<1.5.0,>=1.4.1 (from boto3->awsthreatprep==0.1.1) 29 | Downloading botocore-1.4.49-py2.py3-none-any.whl (2.5MB) 30 | 100% |████████████████████████████████| 2.5MB 641kB/s 31 | Collecting futures<4.0.0,>=2.2.0; python_version == "2.6" or python_version == "2.7" (from s3transfer<0.2.0,>=0.1.0->boto3->awsthreatprep==0.1.1) 32 | Downloading futures-3.0.5-py2-none-any.whl 33 | Collecting docutils>=0.10 (from botocore<1.5.0,>=1.4.1->boto3->awsthreatprep==0.1.1) 34 | Downloading docutils-0.12.tar.gz (1.6MB) 35 | 100% |████████████████████████████████| 1.6MB 1.0MB/s 36 | Collecting python-dateutil<3.0.0,>=2.1 (from botocore<1.5.0,>=1.4.1->boto3->awsthreatprep==0.1.1) 37 | Downloading python_dateutil-2.5.3-py2.py3-none-any.whl (201kB) 38 | 100% |████████████████████████████████| 204kB 1.9MB/s 39 | Collecting six>=1.5 (from python-dateutil<3.0.0,>=2.1->botocore<1.5.0,>=1.4.1->boto3->awsthreatprep==0.1.1) 40 | Downloading six-1.10.0-py2.py3-none-any.whl 41 | Building wheels for collected packages: docutils 42 | Running setup.py bdist_wheel for docutils ... done 43 | Stored in directory: /root/.cache/pip/wheels/db/de/bd/b99b1e12d321fbc950766c58894c6576b1a73ae3131b29a151 44 | Successfully built docutils 45 | Installing collected packages: jmespath, futures, docutils, six, python-dateutil, botocore, s3transfer, boto3, awsthreatprep 46 | Running setup.py install for awsthreatprep ... done 47 | Successfully installed awsthreatprep-0.1.1 boto3-1.4.0 botocore-1.4.49 docutils-0.12 futures-3.0.5 jmespath-0.9.0 python-dateutil-2.5.3 s3transfer-0.1.2 six-1.10.0 48 | 49 | root@3009:/# python -m awsthreatprep.checker > output.json 50 | 51 | root@3009:/# head -n 30 output.json 52 | { 53 | "S3": { 54 | "generals": [], 55 | "collections": [ 56 | { 57 | "category": "S3", 58 | "status": "FAIL", 59 | "reason": "", 60 | "resource_name": "threatpreptest45", 61 | "description": "Checks for basic S3 security settings.", 62 | "check_name": "S3CheckCollection", 63 | "subchecks": [ 64 | { 65 | "category": "S3", 66 | "status": "PASS", 67 | "reason": "S3 versioning is enabled for this bucket", 68 | "resource_name": "threatpreptest45", 69 | "description": "Checks if versioning is enabled on a S3 bucket.", 70 | "check_name": "S3VersioningEnabledCheck", 71 | "subchecks": [] 72 | }, 73 | { 74 | "category": "S3", 75 | "status": "PASS", 76 | "reason": "S3 logging is enabled for this bucket", 77 | "resource_name": "threatpreptest45", 78 | "description": "Checks if logging is enabled on a S3 bucket.", 79 | "check_name": "S3LoggingEnabledCheck", 80 | "subchecks": [] 81 | }, 82 | 83 | ``` 84 | 85 | ## Import from module 86 | 87 | ```python 88 | from awsthreatprep.checker import Checker 89 | c = Checker() 90 | ``` 91 | 92 | 93 | ## How to use 94 | 95 | ```sh 96 | root@3009a5bc9817:/# python 97 | Python 2.7.12 (default, Aug 26 2016, 20:43:47) 98 | [GCC 4.9.2] on linux2 99 | Type "help", "copyright", "credits" or "license" for more information. 100 | >>> import pprint 101 | >>> from awsthreatprep.checker import Checker 102 | >>> c = Checker() 103 | >>> c.run_checks() 104 | >>> c.results_dict.keys() 105 | ['S3', 'IAM', 'CloudWatch', 'VPC', 'CloudTrail'] 106 | >>> c.results_dict['S3'].keys() 107 | ['generals', 'collections'] 108 | >>> pprint.pprint(c.results_dict['S3']['collections'][0]) 109 | {'category': 'S3', 110 | 'check_name': 'S3CheckCollection', 111 | 'description': 'Checks for basic S3 security settings.', 112 | 'reason': '', 113 | 'resource_name': 'threatpreptest45', 114 | 'status': 'FAIL', 115 | 'subchecks': [{'category': 'S3', 116 | 'check_name': 'S3VersioningEnabledCheck', 117 | 'description': 'Checks if versioning is enabled on a S3 bucket.', 118 | 'reason': 'S3 versioning is enabled for this bucket', 119 | 'resource_name': 'threatpreptest45', 120 | 'status': 'PASS', 121 | 'subchecks': []}, 122 | {'category': 'S3', 123 | 'check_name': 'S3LoggingEnabledCheck', 124 | 'description': 'Checks if logging is enabled on a S3 bucket.', 125 | 'reason': 'S3 logging is enabled for this bucket', 126 | 'resource_name': 'threatpreptest45', 127 | 'status': 'PASS', 128 | 'subchecks': []}, 129 | {'category': 'S3', 130 | 'check_name': 'S3OpenPermissionCheck', 131 | 'description': 'Checks for a permission open to the world on a S3 bucket.', 132 | 'reason': 'S3 permission READ is granted to AllUsers', 133 | 'resource_name': 'threatpreptest45', 134 | 'status': 'FAIL', 135 | 'subchecks': []}, 136 | {'category': 'S3', 137 | 'check_name': 'S3OpenPermissionCheck', 138 | 'description': 'Checks for a permission open to the world on a S3 bucket.', 139 | 'reason': 'S3 permission "WRITE" is not granted to AllUsers', 140 | 'resource_name': 'threatpreptest45', 141 | 'status': 'PASS', 142 | 'subchecks': []}]} 143 | >>> 144 | ``` 145 | ### IAM Policy 146 | 147 | The following policy can be used to run ThreatPrep. It is a reduced version of the ReadOnlyAccess policy (arn:aws:iam::aws:policy/ReadOnlyAccess). 148 | 149 | ```json 150 | { 151 | "Version": "2012-10-17", 152 | "Statement": [ 153 | { 154 | "Effect": "Allow", 155 | "Action": [ 156 | "cloudtrail:DescribeTrails", 157 | "cloudwatch:DescribeAlarms", 158 | "ec2:DescribeFlowLogs", 159 | "ec2:DescribeRegions", 160 | "ec2:DescribeVpcs", 161 | "iam:GenerateCredentialReport", 162 | "iam:GetCredentialReport", 163 | "iam:ListAttachedUserPolicies", 164 | "iam:ListRoles", 165 | "s3:GetBucketAcl", 166 | "s3:GetBucketLogging", 167 | "s3:GetBucketVersioning", 168 | "s3:ListAllMyBuckets", 169 | "s3:ListBucket" 170 | ], 171 | "Resource": "*" 172 | } 173 | ] 174 | } 175 | ``` 176 | 177 | 178 | ### Organization of results 179 | 180 | After running the `run_checks` method, the results are organized in the `results_dict` property of the `Checker` 181 | object. There are four keys in the `results_dict` dictionary: `S3`, `IAM`, `CloudWatch`, `VPC`, and `CloudTrail`. Each 182 | of these keys represents an AWS service or feature where checks are run. 183 | 184 | These groups are further broken down into two more categories: `generals` or `collections`. If a check is a general 185 | check, it is looking for something that is not specific to a particular resource, such as determining if any CloudTrail 186 | trails exist. If a check is a collections check, it is running (usually multiple) checks on a single resource. 187 | 188 | In the example above, the S3 bucket `threatpreptest45` is the resource being checked by the `S3CheckCollection`, the 189 | first result in the `S3` group of `collections` checks. The status of a `CheckCollection` is `PASS` if all of the 190 | subchecks are `PASS`, otherwise, it is `FAIL`. 191 | -------------------------------------------------------------------------------- /awsthreatprep/iam_checks.py: -------------------------------------------------------------------------------- 1 | import common 2 | import datetime 3 | import dateutil 4 | import boto3 5 | 6 | import config 7 | 8 | class IAMRootAccessKeyDisabled(common.BaseCheck): 9 | """Checks to see if access keys are disabled for the root account.""" 10 | def __init__(self, rows): 11 | self.category = 'IAM' 12 | self.resource_name = '' 13 | self.rows = rows 14 | self.status = common.CheckState.UNTESTED 15 | self.subchecks = [] 16 | self.reason = '' 17 | self.test() 18 | def test(self): 19 | root_account = filter( 20 | lambda x: x['user'] == '', 21 | self.rows 22 | ) 23 | if len(root_account) != 1: 24 | self.status = common.CheckState.ERROR 25 | self.reason = 'Looking for 1 but found {0}'.format( 26 | len(root_account) 27 | ) 28 | return 29 | user_dict = root_account[0] 30 | key_keys = 'access_key_{0}_active' 31 | active_keys = filter( 32 | lambda x: user_dict[key_keys.format(x)] == 'true', 33 | range(1,3) 34 | ) 35 | if len(active_keys) != 0: 36 | self.status = common.CheckState.FAIL 37 | self.reason = 'The root account should have 0 access keys but found {0}'.format( 38 | len(active_keys) 39 | ) 40 | else: 41 | self.status = common.CheckState.PASS 42 | self.reason = 'The root account access keys are disabled.' 43 | 44 | class IAMRolesAreCreatedCheck(common.BaseCheck): 45 | """Checks to see if any roles are created.""" 46 | def __init__(self, all_roles): 47 | self.category = 'IAM' 48 | self.all_roles = all_roles 49 | self.resource_name = '' 50 | self.subchecks = [] 51 | self.test() 52 | 53 | def test(self): 54 | roles =[ x for x in self.all_roles] 55 | self.reason = '{0} roles are created.'.format( 56 | len(roles) 57 | ) 58 | if len(roles) > 0: 59 | self.status = common.CheckState.PASS 60 | else: 61 | self.status = common.CheckState.FAIL 62 | 63 | class IAMUserCheckCollection(common.BaseCheck): 64 | """Checks for all IAM User security configurations.""" 65 | 66 | def __init__(self): 67 | return super(IAMUserCheckCollection, self).__init__(category='IAM') 68 | 69 | def collect_tests(self, user_dict): 70 | self.resource_name = user_dict['user'] 71 | self.subchecks.append(IAMUserMFAEnabledCheck(user_dict)) 72 | self.subchecks.append(IAMUserPasswordChangeCheck(user_dict)) 73 | self.subchecks.append(IAMUserAccessKeyChangeCheck(user_dict)) 74 | if self.resource_name != '': 75 | self.subchecks.append( 76 | IAMUserHasAdministratorAccessPolicyCheck(user_dict) 77 | ) 78 | 79 | if all(x.status==common.CheckState.PASS for x in self.subchecks): 80 | self.status = common.CheckState.PASS 81 | else: 82 | self.status = common.CheckState.FAIL 83 | 84 | class IAMUserCheck(common.BaseCheck): 85 | def __init__(self, user_dict): 86 | self.category = 'IAM' 87 | self.user_dict= user_dict 88 | self.resource_name = self.user_dict['user'] 89 | self.status = common.CheckState.UNTESTED 90 | self.reason = '' 91 | self.subchecks = [] 92 | self.test() 93 | def test(self): 94 | raise NotImplemented('This method should be called on subclasses of this class') 95 | 96 | class IAMUserMFAEnabledCheck(IAMUserCheck): 97 | """Checks if the account has MFA enabled.""" 98 | def test(self): 99 | if self.user_dict['mfa_active'] == 'true': 100 | self.status = common.CheckState.PASS 101 | else: 102 | self.status = common.CheckState.FAIL 103 | 104 | class IAMUserHasAdministratorAccessPolicyCheck(IAMUserCheck): 105 | """Checks if the account has the AdministratorAccess policy.""" 106 | def test(self): 107 | policy = 'arn:aws:iam::aws:policy/AdministratorAccess' 108 | user = boto3.resource('iam').User(self.user_dict['user']) 109 | attached_admin_policies = filter( 110 | lambda x: x.arn == policy, 111 | user.attached_policies.all() 112 | ) 113 | if len(attached_admin_policies) > 0: 114 | self.reason = 'This user has the {0} policy.'.format(policy) 115 | self.status = common.CheckState.FAIL 116 | else: 117 | self.status = common.CheckState.PASS 118 | self.reason = 'This user does not have the {0} policy.'.format(policy) 119 | 120 | 121 | 122 | class IAMUserPasswordChangeCheck(IAMUserCheck): 123 | """Checks if the account has password changed in last X days.""" 124 | def test(self): 125 | if self.user_dict['password_enabled'] == 'true' : 126 | last_changed = dateutil.parser.parse(self.user_dict['password_last_changed']) 127 | now = datetime.datetime.utcnow().replace(tzinfo=last_changed.tzinfo) 128 | diff = now - last_changed 129 | delta = datetime.timedelta( 130 | days=config.config['PASSWORD_ROTATION_DAYS'] 131 | ) 132 | if diff > delta: 133 | self.reason = 'Password has not been changed in {0} days'.format( 134 | delta.days 135 | ) 136 | self.status = common.CheckState.FAIL 137 | else: 138 | self.status = common.CheckState.PASS 139 | elif self.user_dict['password_last_changed'] == 'not_supported': 140 | self.reason = 'password_last_changed field is not supported' 141 | self.status = common.CheckState.ERROR 142 | else: 143 | self.reason = 'Password is not enabled' 144 | self.status = common.CheckState.PASS 145 | 146 | 147 | 148 | class IAMUserAccessKeyChangeCheck(IAMUserCheck): 149 | '''Tests if the account has access keys changed in X days''' 150 | 151 | def key_rotated(self, key_id): 152 | active_key = 'access_key_{0}_active'.format(key_id) 153 | if self.user_dict[active_key] != 'true': 154 | return True #since the key is not active, call it rotated 155 | last_rotated_key = 'access_key_{0}_last_rotated'.format(key_id) 156 | last_rotated = self.user_dict[last_rotated_key] 157 | try: 158 | last_rotated_date = dateutil.parser.parse(last_rotated) 159 | except ValueError as e: 160 | return False #The key has not been rotated so the value is N/A 161 | delta = datetime.timedelta(days=config.config['ACCESS_KEY_ROTATION_DAYS']) 162 | now = datetime.datetime.now().replace(tzinfo=last_rotated_date.tzinfo) 163 | diff = now-last_rotated_date 164 | if diff > delta: 165 | return False 166 | return True 167 | 168 | 169 | def test(self): 170 | rotated_keys = { 171 | k:self.key_rotated(k) 172 | for k in [1,2] 173 | } 174 | if all(rotated_keys.values()): 175 | self.status = common.CheckState.PASS 176 | self.reason = 'Active keys have been rotated within {0} days'.format( 177 | config.config['ACCESS_KEY_ROTATION_DAYS'] 178 | ) 179 | else: 180 | self.status = common.CheckState.FAIL 181 | self.reason = '{0} active keys have not been rotated within {1} days'.format( 182 | len(filter(lambda x: x==False, rotated_keys.values())), 183 | config.config['ACCESS_KEY_ROTATION_DAYS'] 184 | ) 185 | 186 | 187 | class IAMRecentAccountActivity(IAMUserCheck): 188 | """Tests if the account has been used recently.""" 189 | def test(self): 190 | last_used_times = [] 191 | if self.user_dict['access_key_1_active'] == 'true': 192 | last_used_times.append( 193 | dateutil.parser.parse( 194 | self.user_dict['access_key_1_last_used_date'] 195 | ) 196 | ) 197 | if self.user_dict['access_key_2_active'] == 'true': 198 | last_used_times.append( 199 | dateutil.parser.parse( 200 | self.user_dict['access_key_2_last_used_date'] 201 | ) 202 | ) 203 | if self.user_dict['password_enabled'] in ['true', 'not_supported'] and \ 204 | self.user_dict['password_last_used'] != 'no_information': 205 | last_used_times.append( 206 | dateutil.parser.parse( 207 | self.user_dict['password_last_used'] 208 | ) 209 | ) 210 | if len(last_used_times) == 0: 211 | self.reason = 'Account has never been used' 212 | self.status = common.CheckState.FAIL 213 | return 214 | last_used = max(last_used_times) 215 | now = datetime.datetime.utcnow() 216 | now = now.replace(tzinfo=last_used.tzinfo) 217 | delta = datetime.timedelta(days=config.config['ACCOUNT_INACTIVE_DAYS']) 218 | difference = now - last_used 219 | if delta < difference: 220 | self.reason = 'Account last used {0} days ago.'.format(difference.days) 221 | self.status = common.CheckState.FAIL 222 | else: 223 | self.status = common.CheckState.PASS 224 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # threat_prep documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Sep 15 11:37:04 2016. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | # 19 | # import os 20 | # import sys 21 | # sys.path.insert(0, os.path.abspath('.')) 22 | 23 | import sphinx_rtd_theme 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | # 29 | # needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # The suffix(es) of source filenames. 40 | # You can specify multiple suffix as a list of string: 41 | # 42 | # source_suffix = ['.rst', '.md'] 43 | source_suffix = '.rst' 44 | 45 | # The encoding of source files. 46 | # 47 | # source_encoding = 'utf-8-sig' 48 | 49 | # The master toctree document. 50 | master_doc = 'index' 51 | 52 | # General information about the project. 53 | project = u'ThreatPrep' 54 | copyright = u'2016, Alex McCormack' 55 | author = u'Alex McCormack' 56 | 57 | # The version info for the project you're documenting, acts as replacement for 58 | # |version| and |release|, also used in various other places throughout the 59 | # built documents. 60 | # 61 | # The short X.Y version. 62 | version = u'0.1.1' 63 | # The full version, including alpha/beta/rc tags. 64 | release = u'0.1.1' 65 | 66 | # The language for content autogenerated by Sphinx. Refer to documentation 67 | # for a list of supported languages. 68 | # 69 | # This is also used if you do content translation via gettext catalogs. 70 | # Usually you set "language" from the command line for these cases. 71 | language = None 72 | 73 | # There are two options for replacing |today|: either, you set today to some 74 | # non-false value, then it is used: 75 | # 76 | # today = '' 77 | # 78 | # Else, today_fmt is used as the format for a strftime call. 79 | # 80 | # today_fmt = '%B %d, %Y' 81 | 82 | # List of patterns, relative to source directory, that match files and 83 | # directories to ignore when looking for source files. 84 | # This patterns also effect to html_static_path and html_extra_path 85 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 86 | 87 | # The reST default role (used for this markup: `text`) to use for all 88 | # documents. 89 | # 90 | # default_role = None 91 | 92 | # If true, '()' will be appended to :func: etc. cross-reference text. 93 | # 94 | # add_function_parentheses = True 95 | 96 | # If true, the current module name will be prepended to all description 97 | # unit titles (such as .. function::). 98 | # 99 | # add_module_names = True 100 | 101 | # If true, sectionauthor and moduleauthor directives will be shown in the 102 | # output. They are ignored by default. 103 | # 104 | # show_authors = False 105 | 106 | # The name of the Pygments (syntax highlighting) style to use. 107 | pygments_style = 'sphinx' 108 | 109 | # A list of ignored prefixes for module index sorting. 110 | # modindex_common_prefix = [] 111 | 112 | # If true, keep warnings as "system message" paragraphs in the built documents. 113 | # keep_warnings = False 114 | 115 | # If true, `todo` and `todoList` produce output, else they produce nothing. 116 | todo_include_todos = False 117 | 118 | 119 | # -- Options for HTML output ---------------------------------------------- 120 | 121 | # The theme to use for HTML and HTML Help pages. See the documentation for 122 | # a list of builtin themes. 123 | # 124 | html_theme = 'sphinx_rtd_theme' 125 | 126 | # Theme options are theme-specific and customize the look and feel of a theme 127 | # further. For a list of options available for each theme, see the 128 | # documentation. 129 | # 130 | # html_theme_options = {} 131 | 132 | # Add any paths that contain custom themes here, relative to this directory. 133 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 134 | 135 | # The name for this set of Sphinx documents. 136 | # " v documentation" by default. 137 | # 138 | # html_title = u'threat_prep v0.1' 139 | 140 | # A shorter title for the navigation bar. Default is the same as html_title. 141 | # 142 | # html_short_title = None 143 | 144 | # The name of an image file (relative to this directory) to place at the top 145 | # of the sidebar. 146 | # 147 | # html_logo = None 148 | 149 | # The name of an image file (relative to this directory) to use as a favicon of 150 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 151 | # pixels large. 152 | # 153 | # html_favicon = None 154 | 155 | # Add any paths that contain custom static files (such as style sheets) here, 156 | # relative to this directory. They are copied after the builtin static files, 157 | # so a file named "default.css" will overwrite the builtin "default.css". 158 | html_static_path = ['_static'] 159 | 160 | # Add any extra paths that contain custom files (such as robots.txt or 161 | # .htaccess) here, relative to this directory. These files are copied 162 | # directly to the root of the documentation. 163 | # 164 | # html_extra_path = [] 165 | 166 | # If not None, a 'Last updated on:' timestamp is inserted at every page 167 | # bottom, using the given strftime format. 168 | # The empty string is equivalent to '%b %d, %Y'. 169 | # 170 | # html_last_updated_fmt = None 171 | 172 | # If true, SmartyPants will be used to convert quotes and dashes to 173 | # typographically correct entities. 174 | # 175 | # html_use_smartypants = True 176 | 177 | # Custom sidebar templates, maps document names to template names. 178 | # 179 | # html_sidebars = {} 180 | 181 | # Additional templates that should be rendered to pages, maps page names to 182 | # template names. 183 | # 184 | # html_additional_pages = {} 185 | 186 | # If false, no module index is generated. 187 | # 188 | # html_domain_indices = True 189 | 190 | # If false, no index is generated. 191 | # 192 | # html_use_index = True 193 | 194 | # If true, the index is split into individual pages for each letter. 195 | # 196 | # html_split_index = False 197 | 198 | # If true, links to the reST sources are added to the pages. 199 | # 200 | # html_show_sourcelink = True 201 | 202 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 203 | # 204 | # html_show_sphinx = True 205 | 206 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 207 | # 208 | # html_show_copyright = True 209 | 210 | # If true, an OpenSearch description file will be output, and all pages will 211 | # contain a tag referring to it. The value of this option must be the 212 | # base URL from which the finished HTML is served. 213 | # 214 | # html_use_opensearch = '' 215 | 216 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 217 | # html_file_suffix = None 218 | 219 | # Language to be used for generating the HTML full-text search index. 220 | # Sphinx supports the following languages: 221 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 222 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' 223 | # 224 | # html_search_language = 'en' 225 | 226 | # A dictionary with options for the search language support, empty by default. 227 | # 'ja' uses this config value. 228 | # 'zh' user can custom change `jieba` dictionary path. 229 | # 230 | # html_search_options = {'type': 'default'} 231 | 232 | # The name of a javascript file (relative to the configuration directory) that 233 | # implements a search results scorer. If empty, the default will be used. 234 | # 235 | # html_search_scorer = 'scorer.js' 236 | 237 | # Output file base name for HTML help builder. 238 | htmlhelp_basename = 'threat_prepdoc' 239 | 240 | # -- Options for LaTeX output --------------------------------------------- 241 | 242 | latex_elements = { 243 | # The paper size ('letterpaper' or 'a4paper'). 244 | # 245 | # 'papersize': 'letterpaper', 246 | 247 | # The font size ('10pt', '11pt' or '12pt'). 248 | # 249 | # 'pointsize': '10pt', 250 | 251 | # Additional stuff for the LaTeX preamble. 252 | # 253 | # 'preamble': '', 254 | 255 | # Latex figure (float) alignment 256 | # 257 | # 'figure_align': 'htbp', 258 | } 259 | 260 | # Grouping the document tree into LaTeX files. List of tuples 261 | # (source start file, target name, title, 262 | # author, documentclass [howto, manual, or own class]). 263 | latex_documents = [ 264 | (master_doc, 'threat_prep.tex', u'threat\\_prep Documentation', 265 | u'Alex McCormack', 'manual'), 266 | ] 267 | 268 | # The name of an image file (relative to this directory) to place at the top of 269 | # the title page. 270 | # 271 | # latex_logo = None 272 | 273 | # For "manual" documents, if this is true, then toplevel headings are parts, 274 | # not chapters. 275 | # 276 | # latex_use_parts = False 277 | 278 | # If true, show page references after internal links. 279 | # 280 | # latex_show_pagerefs = False 281 | 282 | # If true, show URL addresses after external links. 283 | # 284 | # latex_show_urls = False 285 | 286 | # Documents to append as an appendix to all manuals. 287 | # 288 | # latex_appendices = [] 289 | 290 | # It false, will not define \strong, \code, itleref, \crossref ... but only 291 | # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added 292 | # packages. 293 | # 294 | # latex_keep_old_macro_names = True 295 | 296 | # If false, no module index is generated. 297 | # 298 | # latex_domain_indices = True 299 | 300 | 301 | # -- Options for manual page output --------------------------------------- 302 | 303 | # One entry per manual page. List of tuples 304 | # (source start file, name, description, authors, manual section). 305 | man_pages = [ 306 | (master_doc, 'threat_prep', u'threat_prep Documentation', 307 | [author], 1) 308 | ] 309 | 310 | # If true, show URL addresses after external links. 311 | # 312 | # man_show_urls = False 313 | 314 | 315 | # -- Options for Texinfo output ------------------------------------------- 316 | 317 | # Grouping the document tree into Texinfo files. List of tuples 318 | # (source start file, target name, title, author, 319 | # dir menu entry, description, category) 320 | texinfo_documents = [ 321 | (master_doc, 'threat_prep', u'threat_prep Documentation', 322 | author, 'threat_prep', 'One line description of project.', 323 | 'Miscellaneous'), 324 | ] 325 | 326 | # Documents to append as an appendix to all manuals. 327 | # 328 | # texinfo_appendices = [] 329 | 330 | # If false, no module index is generated. 331 | # 332 | # texinfo_domain_indices = True 333 | 334 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 335 | # 336 | # texinfo_show_urls = 'footnote' 337 | 338 | # If true, do not generate a @detailmenu in the "Top" node's menu. 339 | # 340 | # texinfo_no_detailmenu = False 341 | --------------------------------------------------------------------------------