├── README.md ├── Gemfile ├── challenge3.rb ├── config.json.sample ├── challenge1.rb ├── Gemfile.lock ├── challenge2.rb ├── cloudtrail_alarm.rb └── sec_dev_ops.rb /README.md: -------------------------------------------------------------------------------- 1 | BlackHatLabs 2 | ============ 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'aws-sdk', '~>2.0.30' 4 | gem 'aws-sdk-core', '~>2.0.30' 5 | gem 'pry', '0.10.1' 6 | gem 'netaddr', '1.5.0' 7 | gem 'ridley', '4.1.2' 8 | gem 'json', '1.8.2' 9 | gem 'pry-nav', '0.2.4' -------------------------------------------------------------------------------- /challenge3.rb: -------------------------------------------------------------------------------- 1 | # This material was created by rmogull@securosis.com for the Black Hat trainings run by Securosis. 2 | # Copyright 2014 Rich Mogull and Securosis, LLC. with a Creative Commons Attribution, NonCommercial, Share Alike license- http://creativecommons.org/licenses/by-nc-sa/4.0/ 3 | 4 | # Install the listed gems. 5 | 6 | require "rubygems" 7 | require "aws-sdk" 8 | require "ridley" 9 | require "json" 10 | 11 | # This code snippet includes the pieces to pull information from your Chef server. The rest is up to you. As with the other code snippets, you can always see how we did it by looking at the SecuritySquirrel tool on GitHub. 12 | 13 | chefconfig = config["chef"] 14 | 15 | #supress errors since Ridley is buggy; switch to "fatal" if it keeps showing up. 16 | Ridley::Logging.logger.level = Logger.const_get 'ERROR' 17 | ridley = Ridley.new( 18 | server_url: "#{config["chef"]["chefserver"]}", 19 | client_name: "#{config["chef"]["clientname"]}", 20 | client_key: "#{config["chef"]["keylocation"]}", 21 | ssl: { verify: false } 22 | ) 23 | 24 | # Ridley has a bug, so we need to work on the node name, which in our case is the same as the EC2 private DNS. For some reason the node.all doesn't pull IP addresses (it's supposed to) which is what we would prefer to use. 25 | nodes = ridley.node.all 26 | nodenames = nodes.map { |node| node.name } 27 | -------------------------------------------------------------------------------- /config.json.sample: -------------------------------------------------------------------------------- 1 | # This is still a work in progress, you need to replace values to your own and properly configure the security groups and IAM rights. We will update the template as the code improves. Delete this line and renamce the file to config.json. 2 | { 3 | "aws": { 4 | "AccessKey": "your access key", 5 | "SecretKey": "your secret key", 6 | "DefaultRegion": "us-west-2", 7 | "RegionSettings": { 8 | "us-west-2":{ 9 | "QuarantineSecurityGroup": "sg-xxx", 10 | "AnalysisSecurityGroup": "sg-xxx", 11 | "SSHKey": "host key", 12 | "User": "ec2-user **replace if you want, this is amazon Linux default user**", 13 | "AMI": "ami-ccf297fc **replace if you want, this is amazon Linux AMI**" 14 | }, 15 | "us-west-2":{ 16 | "QuarantineSecurityGroup": "sg-xxx", 17 | "AnalysisSecurityGroup": "sg-xxx", 18 | "SSHKey": "host key", 19 | "User": "ec2-user **replace if you want, this is amazon Linux default user**", 20 | "AMI": "ami-ccf297fc **replace**" 21 | } 22 | } 23 | }, 24 | "chef": { 25 | "chefserver": "http://your-chef-server-ip:4000", 26 | "clientname": "your client name", 27 | "keylocation": "./client1.pem" 28 | }, 29 | "qualys": { 30 | "username": "your-username", 31 | "password": "your-password", 32 | "scanner_IP": "your qualys virtual scanner IP" 33 | }, 34 | "cloudpassage": { 35 | "id": "halo API id", 36 | "secret": "secret", 37 | "VA_scanner_zone": "the zone (in CIDR) of your qualys scanner. Must be set as a Halo IP zone" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /challenge1.rb: -------------------------------------------------------------------------------- 1 | # This material was created by rmogull@securosis.com for the Black Hat trainings run by Securosis. 2 | # Copyright 2016 Rich Mogull and Securosis, LLC. with a Creative Commons Attribution, NonCommercial, Share Alike license- http://creativecommons.org/licenses/by-nc-sa/4.0/ 3 | 4 | # Install the listed gems. 5 | 6 | require "rubygems" 7 | require 'aws-sdk' 8 | require "json" 9 | 10 | # class for incident resposne functions like quarantine. 11 | class Challenge1 12 | def initialize(instance_id) 13 | @instance_id = instance_id 14 | 15 | # Load configuration and credentials from a JSON file. Right now hardcoded to config.json in the app drectory. 16 | # Note that we use the same configuration file for multiple code snippet files, but this will only pull what you need. 17 | 18 | # configfile = File.read('config.json') 19 | # config = JSON.parse(configfile) 20 | 21 | # Aws.config = { access_key_id: "#{config["aws"]["AccessKey"]}", secret_access_key: "#{config["aws"]["SecretKey"]}", region: "us-west-2" } 22 | 23 | @@ec2 = Aws::EC2::Client.new(region: "#{$region}") 24 | end 25 | 26 | def list 27 | # method to pull all details of all instances in the region. 28 | 29 | instancelist = @@ec2.describe_instances() 30 | 31 | end 32 | 33 | def list_by_tag 34 | 35 | instancelist = @@ec2.describe_tags( 36 | filters: [ 37 | { 38 | name: "key", 39 | values: ["SecurityStatus"] 40 | } 41 | ] 42 | ) 43 | 44 | end 45 | 46 | def change_sec_group 47 | # this method moves the provided instance into the Quarantine security group defined in the config file. 48 | instance_id = "" 49 | sec_group = "" 50 | 51 | newsecgroup = @@ec2.modify_instance_attribute(instance_id: "#{instance_id}", groups: ["#{sec_group}"]) 52 | 53 | end 54 | 55 | def store_metadata 56 | # Method collects the instance metadata before making changes and appends to a local file. 57 | # Note- currently not working right, need fo convert the has to JSON 58 | data = @@ec2.describe_instances(instance_ids: ["#{@instance_id}"]) 59 | 60 | end 61 | 62 | $region = "us-west-2" 63 | 64 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.3.8) 5 | aws-sdk (2.0.30) 6 | aws-sdk-resources (= 2.0.30) 7 | aws-sdk-core (2.0.30) 8 | builder (~> 3.0) 9 | jmespath (~> 1.0) 10 | multi_json (~> 1.0) 11 | multi_xml (~> 0.5) 12 | aws-sdk-resources (2.0.30) 13 | aws-sdk-core (= 2.0.30) 14 | buff-config (1.0.1) 15 | buff-extensions (~> 1.0) 16 | varia_model (~> 0.4) 17 | buff-extensions (1.0.0) 18 | buff-ignore (1.1.1) 19 | buff-ruby_engine (0.1.0) 20 | buff-shell_out (0.2.0) 21 | buff-ruby_engine (~> 0.1.0) 22 | builder (3.2.2) 23 | celluloid (0.16.0) 24 | timers (~> 4.0.0) 25 | celluloid-io (0.16.2) 26 | celluloid (>= 0.16.0) 27 | nio4r (>= 1.1.0) 28 | coderay (1.1.0) 29 | erubis (2.7.0) 30 | faraday (0.9.1) 31 | multipart-post (>= 1.2, < 3) 32 | hashie (2.1.2) 33 | hitimes (1.2.2) 34 | jmespath (1.0.2) 35 | multi_json (~> 1.0) 36 | json (1.8.2) 37 | method_source (0.8.2) 38 | mixlib-authentication (1.3.0) 39 | mixlib-log 40 | mixlib-log (1.6.0) 41 | multi_json (1.11.0) 42 | multi_xml (0.5.5) 43 | multipart-post (2.0.0) 44 | net-http-persistent (2.9.4) 45 | netaddr (1.5.0) 46 | nio4r (1.1.0) 47 | pry (0.10.1) 48 | coderay (~> 1.1.0) 49 | method_source (~> 0.8.1) 50 | slop (~> 3.4) 51 | pry-nav (0.2.4) 52 | pry (>= 0.9.10, < 0.11.0) 53 | retryable (2.0.1) 54 | ridley (4.1.2) 55 | addressable 56 | buff-config (~> 1.0) 57 | buff-extensions (~> 1.0) 58 | buff-ignore (~> 1.1) 59 | buff-shell_out (~> 0.1) 60 | celluloid (~> 0.16.0) 61 | celluloid-io (~> 0.16.1) 62 | erubis 63 | faraday (~> 0.9.0) 64 | hashie (>= 2.0.2, < 3.0.0) 65 | json (>= 1.7.7) 66 | mixlib-authentication (>= 1.3.0) 67 | net-http-persistent (>= 2.8) 68 | retryable (>= 2.0.0) 69 | semverse (~> 1.1) 70 | varia_model (~> 0.4) 71 | semverse (1.2.1) 72 | slop (3.6.0) 73 | timers (4.0.1) 74 | hitimes 75 | varia_model (0.4.0) 76 | buff-extensions (~> 1.0) 77 | hashie (>= 2.0.2, < 3.0.0) 78 | 79 | PLATFORMS 80 | ruby 81 | 82 | DEPENDENCIES 83 | aws-sdk (~> 2.0.30) 84 | aws-sdk-core (~> 2.0.30) 85 | json (= 1.8.2) 86 | netaddr (= 1.5.0) 87 | pry (= 0.10.1) 88 | pry-nav (= 0.2.4) 89 | ridley (= 4.1.2) 90 | -------------------------------------------------------------------------------- /challenge2.rb: -------------------------------------------------------------------------------- 1 | # This material was created by rmogull@securosis.com for the Black Hat trainings run by Securosis. 2 | # Copyright 2014 Rich Mogull and Securosis, LLC. with a Creative Commons Attribution, NonCommercial, Share Alike license- http://creativecommons.org/licenses/by-nc-sa/4.0/ 3 | 4 | # Install the listed gems. 5 | 6 | require "rubygems" 7 | require 'aws-sdk' 8 | require "json" 9 | 10 | # class for incident response functions like quarantine. 11 | class IncidentResponse 12 | def initialize(instance_id) 13 | @instance_id = instance_id 14 | 15 | # Load configuration and credentials from a JSON file. Right now hardcoded to config.json in the app drectory. 16 | # configfile = File.read('config.json') 17 | # config = JSON.parse(configfile) 18 | 19 | @QuarantineGroup = "#{config["aws"]["RegionSettings"]["us-west-2"]["QuarantineSecurityGroup"]}" 20 | @ForensicsAMI = "#{config["aws"]["RegionSettings"]["us-west-2"]["AMI"]}" 21 | @AnalysisSecurityGroup = "#{config["aws"]["RegionSettings"]["us-west-2"]["AnalysisSecurityGroup"]}" 22 | @ForensicsSSHKey = "#{config["aws"]["RegionSettings"]["us-west-2"]["SSHKey"]}" 23 | @ForensicsUser = "#{config["aws"]["RegionSettings"]["us-west-2"]["User"]}" 24 | 25 | # Aws.config = { access_key_id: "#{config["aws"]["AccessKey"]}", secret_access_key: "#{config["aws"]["SecretKey"]}", region: "us-west-2" } 26 | 27 | @@ec2 = Aws::EC2.new(region: "#{$region}") 28 | end 29 | 30 | def quarantine 31 | # this method moves the provided instance into the Quarantine security group defined in the config file. 32 | puts "" 33 | puts "Quarantining #{@instance_id}..." 34 | quarantine = @@ec2.modify_instance_attribute(instance_id: "#{@instance_id}", groups: ["#{@QuarantineGroup}"]) 35 | puts "#{@instance_id} moved to the Quarantine security group from your configuration settings." 36 | end 37 | 38 | def tag 39 | # this method adds an "status => IR" tag to the instance. 40 | # If you properly configure your IAM policies, this will move ownership fo the instance to the security 41 | # team and isolate it so no one else can terminate/stop/modify/etc. 42 | puts "Tagging instance with 'IR'..." 43 | tag = @@ec2.create_tags(resources: ["#{@instance_id}"], tags: [ 44 | { 45 | key: "SecurityStatus", 46 | value: "IR", 47 | }, 48 | ],) 49 | puts "Instance tagged and IAM restrictions applied." 50 | end 51 | 52 | def snapshot 53 | # This method determines the volume IDs for the instance, then creates snapshots of those def volumes(args) 54 | # Get the instance details for the instance 55 | instance_details = @@ec2.describe_instances( 56 | instance_ids: ["#{@instance_id}"], 57 | ) 58 | # find the attached block devices, then the ebs volumes, then the volume ID for each EBS volume. This involves walking the response tree. 59 | # There is probably a better way to do this in Ruby, but I'm still learning. 60 | puts "Identifying attached volumes..." 61 | block_devices = instance_details.reservations.first.instances.first.block_device_mappings 62 | ebs = block_devices.map(&:ebs) 63 | volumes = ebs.map(&:volume_id) 64 | # start an empty array to later track and attach the snapshot to a forensics storage volume 65 | @snap = [] 66 | volumes.each do |vol| 67 | puts "Volume #{vol} identified; creating snapshot" 68 | # Create a snapshot of each volume and add the volume and instance ID to the description. 69 | # We do this since you can't apply a name tag until the snapshot is created, and we don't want to slow down the process. 70 | timestamp = Time.new 71 | snap = @@ec2.create_snapshot( 72 | volume_id: "#{vol}", 73 | description: "IR volume #{vol} of instance #{@instance_id} at #{timestamp}", 74 | ) 75 | puts "Snapshots complete with description: IR volume #{vol} of instance #{@instance_id} at #{timestamp}" 76 | # get the snapshot id and add it to an array for this instance of the class so we can use it later for forensics 77 | @snap = @snap += snap.map(&:snapshot_id) 78 | 79 | end 80 | end 81 | 82 | 83 | def forensics_analysis 84 | # This method launches an instance and then creates and attaches storage volumes of the IR snapshots. 85 | # It also opens Security Group access between the forensics and target instance. 86 | # Right now it is in Main, but later I will update to run it as a thread, after I get the code working. 87 | 88 | # set starting variables 89 | alpha = ("f".."z").to_a 90 | count = 0 91 | block_device_map = Array.new 92 | 93 | # Build the content for the block device mappings to add each snapshot as a volume. 94 | # Device mappings start as sdf and continue up to sdz, which is way more than you will ever need. 95 | @snap.each do |snapshot_id| 96 | count += 1 97 | # pull details to get the volume size 98 | snap_details = @@ec2.describe_snapshots(snapshot_ids: ["#{snapshot_id}"]) 99 | vol_size = snap_details.snapshots.first.volume_size 100 | # create the string for the device mapping 101 | device = "/dev/sd" + alpha[count].to_s 102 | # build the hash we will need later for the bock device mappings 103 | temphash = Hash.new 104 | temphash = { 105 | device_name: "#{device}", 106 | ebs: { 107 | snapshot_id: "#{snapshot_id}", 108 | volume_size: vol_size, 109 | volume_type: "standard", 110 | } 111 | } 112 | # add the hash to our array 113 | block_device_map << temphash 114 | 115 | end 116 | 117 | # Notify user that this will run in the background in case the snapshots are large and it takes a while 118 | 119 | puts "A forensics analysis server is being launched in the background in #{@region} with the name" 120 | puts "'Forensics' and the snapshots attached as volumes starting at /dev/sdf " 121 | puts "(which may show as /dev/xvdf). Use host key #{@ForensicsSSHKey} for user #{@ForensicsUser}" 122 | puts "" 123 | puts "Press Return to return to the main menu" 124 | blah = gets.chomp 125 | 126 | # Create array to get the snapshot status via API 127 | 128 | snaparray = Array.new 129 | @snap.each do |snap_id| 130 | snaparray << "#{snap_id}" 131 | end 132 | 133 | # Launch the rest as a thread since waiting for the snapshot may otherwise slow the program down. 134 | 135 | thread = Thread.new do 136 | # Get status of snapshots and check to see if any of them are still pending. Loop until they are all ready. 137 | status = false 138 | until status == true do 139 | snap_details = @@ec2.describe_snapshots(snapshot_ids: snaparray) 140 | snap_details.each do |snapID| 141 | if snap_details.snapshots.first.state == "completed" 142 | status = true 143 | else 144 | status = false 145 | end 146 | end 147 | end 148 | 149 | forensic_instance = @@ec2.run_instances( 150 | image_id: "#{ @ForensicsAMI}", 151 | min_count: 1, 152 | max_count: 1, 153 | instance_type: "t1.micro", 154 | key_name: "#{@ForensicsSSHKey}", 155 | security_group_ids: ["#{@AnalysisSecurityGroup}"], 156 | placement: { 157 | availability_zone: "us-west-2a" 158 | }, 159 | block_device_mappings: block_device_map 160 | ) 161 | # Tag the instance so you can find it later 162 | temp_id = forensic_instance.instances.first.instance_id 163 | tag = @@ec2.create_tags( 164 | resources: ["#{temp_id}"], 165 | tags: [ 166 | { 167 | key: "SecurityStatus", 168 | value: "Forensic Analysis Server for #{@instance_id}", 169 | }, 170 | { 171 | key: "Name", 172 | value: "Forensics", 173 | }, 174 | ], 175 | ) 176 | end 177 | 178 | end 179 | 180 | def store_metadata 181 | # Method collects the instance metadata before making changes and appends to a local file. 182 | # Note- currently not working right, need fo convert the has to JSON 183 | data = @@ec2.describe_instances(instance_ids: ["#{@instance_id}"]) 184 | timestamp = Time.new 185 | File.open("ForensicMetadataLog.txt", "a") do |log| 186 | log.puts "****************************************************************************************" 187 | log.puts "Incident for instance #{@instance_id} at #{timestamp}" 188 | log.puts "****************************************************************************************" 189 | log.puts "" 190 | metadata = data.to_h 191 | metadata = metadata.to_json 192 | log.puts metadata 193 | end 194 | puts "Metadata for #{@instance_id} appended to ForensicMetadataLog.txt" 195 | end 196 | 197 | 198 | end 199 | 200 | $region = "us-west-2" 201 | -------------------------------------------------------------------------------- /cloudtrail_alarm.rb: -------------------------------------------------------------------------------- 1 | # This workflow creates a CloudWatch alarm that sends email to the designated address if there are any changes to an IAM policy 2 | # It's pretty static at this point, but easy to modify for other alarm needs. 3 | # It is designed for a single region, and doesn't have a lot of pretty stuff you would want if using in production 4 | # **WARNING WARNING WARNING** This also does *not* check to see if CloudTrail (and all the other pieces) already exist. It builds it from scratch and WILL cause errors if you run it in a region that is already configured for CloudTrail 5 | # Copyright Securosis, LLC 2015 6 | 7 | require "rubygems" 8 | # require 'bundler/setup' 9 | require "aws-sdk" 10 | require "json" 11 | # require "pry" 12 | 13 | class CloudTrailAlarm 14 | def initialize 15 | 16 | # Load configuration and credentials from a JSON file. Right now hardcoded to config.json in the app drectory. 17 | # TODO update to pull credentials from Trinity DB 18 | # TODO update to be able to handle multiple accounts and regions 19 | 20 | # Load from config file in same directory as code 21 | # In the future, we will need to adjust this to rotate through all accounts and regions for the user. AssumeRole should help. 22 | # config = JSON.load(File.read('config.json')) 23 | $region = "us-east-1" 24 | # credentials... using hard coded for this PoC, but really should be an assumerole in the future. 25 | 26 | # creds = Aws::Credentials.new("#{config["aws"]["AccessKey"]}", "#{config["aws"]["SecretKey"]}") 27 | # Create clients for the various services we need. Loading them all here and setting them as Class variables. 28 | @ec2 = Aws::EC2::Client.new(region: "#{$region}") 29 | # hard code s3 to us standard region or some calls will later fail 30 | @s3 = Aws::S3::Client.new(region: "us-east-1") 31 | @iam = Aws::IAM::Client.new(region: $region) 32 | @sns = Aws::SNS::Client.new(region: $region) 33 | @cloudwatchlogs = Aws::CloudWatchLogs::Client.new(region: $region) 34 | @cloudwatch = Aws::CloudWatch::Client.new(region: $region) 35 | @cloudtrail = Aws::CloudTrail::Client.new(region: $region) 36 | 37 | # generate an 8 character random string to make carious names unique 38 | @random = rand(36**8).to_s(36) 39 | end 40 | 41 | def disable_cloudtrail 42 | # DO NOT RUN THIS IN PRODUCTION!!! It will delete the trail for the current region, and is only for lab purposes 43 | list = @cloudtrail.describe_trails() 44 | trail = list.trail_list.first.name 45 | @cloudtrail.delete_trail(name: trail) 46 | puts "Trail #{trail} deleted" 47 | end 48 | 49 | def create_private_s3_bucket(bucket_name) 50 | # add the string to the end of the requested name 51 | bucket_name = bucket_name + "-" + @random 52 | bucket = @s3.create_bucket( 53 | acl: "private", 54 | bucket: bucket_name 55 | ) 56 | puts "created bucket #{bucket_name}" 57 | return bucket_name 58 | end 59 | 60 | def get_account_id() 61 | # pull creds for the account, the account ID will be in the result 62 | roles = @iam.get_account_authorization_details(filter: ["Role"]) 63 | # parse out the account ID 64 | account_id = /(?<=arn:aws:iam::)(.{1,12})/.match(roles.role_detail_list.first.arn) 65 | puts "The account ID for the current credentials is #{account_id}" 66 | return account_id 67 | end 68 | 69 | def set_cloudtrail_s3_iam_policy(account_id, bucket_name) 70 | 71 | # create the json policy, substituting in the account ID and bucket name 72 | policy = %Q<{ 73 | "Version": "2012-10-17", 74 | "Statement": [ 75 | { 76 | "Sid": "AWSCloudTrailAclCheck20131101", 77 | "Effect": "Allow", 78 | "Principal": { 79 | "AWS": [ 80 | "arn:aws:iam::903692715234:root", 81 | "arn:aws:iam::859597730677:root", 82 | "arn:aws:iam::814480443879:root", 83 | "arn:aws:iam::216624486486:root", 84 | "arn:aws:iam::086441151436:root", 85 | "arn:aws:iam::388731089494:root", 86 | "arn:aws:iam::284668455005:root", 87 | "arn:aws:iam::113285607260:root", 88 | "arn:aws:iam::035351147821:root" 89 | ] 90 | }, 91 | "Action": "s3:GetBucketAcl", 92 | "Resource": "arn:aws:s3:::#{bucket_name}" 93 | }, 94 | { 95 | "Sid": "AWSCloudTrailWrite20131101", 96 | "Effect": "Allow", 97 | "Principal": { 98 | "AWS": [ 99 | "arn:aws:iam::903692715234:root", 100 | "arn:aws:iam::859597730677:root", 101 | "arn:aws:iam::814480443879:root", 102 | "arn:aws:iam::216624486486:root", 103 | "arn:aws:iam::086441151436:root", 104 | "arn:aws:iam::388731089494:root", 105 | "arn:aws:iam::284668455005:root", 106 | "arn:aws:iam::113285607260:root", 107 | "arn:aws:iam::035351147821:root" 108 | ] 109 | }, 110 | "Action": "s3:PutObject", 111 | "Resource": "arn:aws:s3:::#{bucket_name}/AWSLogs/#{account_id}/*", 112 | "Condition": { 113 | "StringEquals": { 114 | "s3:x-amz-acl": "bucket-owner-full-control" 115 | } 116 | } 117 | } 118 | ] 119 | } > 120 | 121 | # replace the existing bucket policy with the new one since it's a new bucket 122 | @s3.put_bucket_policy( 123 | bucket: bucket_name, 124 | policy: policy, 125 | ) 126 | puts "Amazon S3 bucket policy for CloudTrail applied to bucket #{bucket_name}" 127 | end 128 | 129 | def create_cloudtrail_cloudwatch_log(account_id) 130 | # This creates a single cloudwatch log group 131 | @cloudwatchlogs.create_log_group( 132 | log_group_name: "CloudTrail/logs", 133 | ) 134 | # pull the ARN 135 | log_group = @cloudwatchlogs.describe_log_groups(log_group_name_prefix: "CloudTrail/logs") 136 | log_group = log_group.log_groups.first.arn 137 | puts "Cloudwatch log group CloudTrail_#{@random}/logs created" 138 | 139 | # start by creating the assume role policy, which is needed to tell AWS who is allowed to use the role. Had to go to support to figure that one out. 140 | 141 | assume_role_policy = %Q<{ 142 | "Version": "2012-10-17", 143 | "Statement": [ 144 | { 145 | "Sid": "", 146 | "Effect": "Allow", 147 | "Principal": { 148 | "Service": "cloudtrail.amazonaws.com" 149 | }, 150 | "Action": "sts:AssumeRole" 151 | } 152 | ] 153 | }> 154 | 155 | # build the policy needed for the IAM role 156 | role_policy = %Q<{ 157 | "Version": "2012-10-17", 158 | "Statement": [ 159 | { 160 | 161 | "Sid": "AWSCloudTrailCreateLogStream2014110", 162 | "Effect": "Allow", 163 | "Action": [ 164 | "logs:CreateLogStream" 165 | ], 166 | "Resource": [ 167 | "arn:aws:logs:#{$region}:#{account_id}:log-group:CloudTrail/logs:log-stream:#{account_id}_CloudTrail_#{$region}*" 168 | ] 169 | 170 | }, 171 | { 172 | "Sid": "AWSCloudTrailPutLogEvents20141101", 173 | "Effect": "Allow", 174 | "Action": [ 175 | "logs:PutLogEvents" 176 | ], 177 | "Resource": [ 178 | "arn:aws:logs:#{$region}:#{account_id}:log-group:CloudTrail/logs:log-stream:#{account_id}_CloudTrail_#{$region}*" 179 | ] 180 | } 181 | ] 182 | }> 183 | # create the required IAM role and set the assume role policy 184 | role = @iam.create_role( 185 | role_name: "DELETE_CloudTrail_CloudWatchLogs_Role", 186 | assume_role_policy_document: assume_role_policy, 187 | ) 188 | cloudwatch_log_hash = {} 189 | cloudwatch_log_hash = {:log_group_arn => log_group, :role_arn => role.role.arn} 190 | 191 | # now set the role policy to allow cloudtrail access 192 | @iam.put_role_policy( 193 | # required 194 | role_name: "DELETE_CloudTrail_CloudWatchLogs_Role", 195 | # required 196 | policy_name: "AllowCloudTrailCloudwatchAccess", 197 | # required 198 | policy_document: role_policy, 199 | ) 200 | puts "IAM role CloudTrail_CloudWatchLogs_Role_#{@random} created for CloudTrail to post the logs." 201 | return cloudwatch_log_hash 202 | end 203 | 204 | def create_cloudtrail(bucket_name, cloudwatch_log_hash, account_id) 205 | # Wait 10 seconds for the IAM policy to propagate 206 | sleep 10 207 | # create a cloudtrail with the name of the region and the account ID 208 | name = "#{$region}-#{account_id}" 209 | trail = @cloudtrail.create_trail( 210 | name: name, 211 | s3_bucket_name: bucket_name, 212 | include_global_service_events: true, 213 | cloud_watch_logs_log_group_arn: cloudwatch_log_hash[:log_group_arn], 214 | cloud_watch_logs_role_arn: cloudwatch_log_hash[:role_arn], 215 | ) 216 | puts "CloudTrail #{name} created." 217 | # wait a few seconds, then start logging 218 | sleep 5 219 | @cloudtrail.start_logging( 220 | name: name, 221 | ) 222 | puts "CloudTrail logging enabled." 223 | sleep 5 224 | end 225 | 226 | def create_cloudwatch_alarm_topic(email) 227 | # This creates a new topic and sets the subscription to the provided email address 228 | # you could easily convert it to send to SMS, SQS, http, or another destination 229 | topic = @sns.create_topic( 230 | name: "cloudtrail", 231 | ) 232 | puts "SNS topic cloudtrail_#{@random} created to send notifications." 233 | @sns.subscribe( 234 | topic_arn: topic.topic_arn, 235 | protocol: "email", 236 | endpoint: email, 237 | ) 238 | # Check to make sure the user set up their email correctly to receive the messages 239 | puts "Please check your email to confirm the subscription, then hit Enter" 240 | blah = gets.chomp() 241 | puts "Sending test message. Please check your email, and hit enter when you receive it" 242 | # Send a test message 243 | @sns.publish( 244 | topic_arn: topic.topic_arn, 245 | message: "If you can read this, your CloudTrail alarm is set to receive messages.", 246 | subject: "CloudTrail Subscription Test", 247 | ) 248 | blah = gets.chomp() 249 | return topic.topic_arn 250 | end 251 | 252 | def create_cloudwatch_cloudtrail_filter(filter, filter_name) 253 | # Creates the filter we will use to alarm on. The name is currently hard coded for CloudTrail. 254 | @cloudwatchlogs.put_metric_filter( 255 | # required 256 | log_group_name: "CloudTrail/logs", 257 | # required 258 | filter_name: filter_name, 259 | # required 260 | filter_pattern: filter, 261 | # required 262 | metric_transformations: [ 263 | { 264 | # required 265 | metric_name: filter_name, 266 | # required 267 | metric_namespace: "CloudTrailMetrics", 268 | # required 269 | metric_value: "1", 270 | }, 271 | ], 272 | ) 273 | puts "CloudWatch Logs filter #{filter_name}_#{@random} created." 274 | end 275 | 276 | def create_cloudwatch_cloudtrail_alarm(filter_name, topic_arn) 277 | # Creates the alarm based on the cloudwatch filter. some values currently hardcoded that might be better as variables later. 278 | @cloudwatch.put_metric_alarm( 279 | # required 280 | alarm_name: filter_name, 281 | alarm_description: "An alarm set for the #{filter_name} CloudTrail filter", 282 | actions_enabled: true, 283 | alarm_actions: ["#{topic_arn}"], 284 | # required 285 | metric_name: filter_name, 286 | # required 287 | namespace: "CloudTrailMetrics", 288 | # required 289 | statistic: "Sum", 290 | # required 291 | period: 60, 292 | # required 293 | evaluation_periods: 1, 294 | # required 295 | threshold: 1, 296 | # required 297 | comparison_operator: "GreaterThanOrEqualToThreshold", 298 | ) 299 | puts "CloudWatch alarm for puts CloudWatch Logs filter #{filter_name}_#{@random} created." 300 | end 301 | 302 | end 303 | 304 | menuselect = 0 305 | until menuselect == 7 do 306 | puts "\e[H\e[2J" 307 | puts "Welcome to AlarmSquirrel. This application creates a CloudWatch Alarm for any IAM metrics." 308 | puts "" 309 | puts "1. Run the workflow" 310 | puts "5. Delete your existing trail" 311 | puts "7. Exit" 312 | print "Select: " 313 | menuselect = gets.chomp 314 | if menuselect == "1" 315 | cloudtrail = CloudTrailAlarm.new() 316 | # create a cloudtrail bucket. 317 | bucket_name = cloudtrail.create_private_s3_bucket("deletecloudtrail") 318 | account_id = cloudtrail.get_account_id 319 | cloudtrail.set_cloudtrail_s3_iam_policy(account_id, bucket_name) 320 | cloudwatch_log_hash = cloudtrail.create_cloudtrail_cloudwatch_log(account_id) 321 | cloudtrail.create_cloudtrail(bucket_name, cloudwatch_log_hash, account_id) 322 | puts "Please enter your email to receive alarms:" 323 | email = gets.chomp() 324 | topic_arn = cloudtrail.create_cloudwatch_alarm_topic(email) 325 | 326 | filter = %Q<{ ( ($.eventSource = "iam.amazonaws.com") && (($.eventName = "Add*") || ($.eventName = "Change*") || ($.eventName = "Create*") || ($.eventName = "Deactivate*") || ($.eventName = "Delete*") || ($.eventName = "Enable*") || ($.eventName = "Put*") || ($.eventName = "Remove*") || ($.eventName = "Update*") || ($.eventName = "Upload*")) ) }> 327 | 328 | puts "Enter a name for your filter and alarm. For this demo, the filter is hard coded to IAM changes" 329 | filter_name = gets.chomp 330 | cloudtrail.create_cloudwatch_cloudtrail_filter(filter, filter_name) 331 | cloudtrail.create_cloudwatch_cloudtrail_alarm(filter_name, topic_arn) 332 | 333 | puts "If you see this, it probably worked. Wait a minute, make an IAM change, and you should see an alarm within 15 minutes." 334 | puts "Please remember to shut down the services at the end of the lab:" 335 | puts " - disable the CloudTrail logging" 336 | puts " - Delete the S3 bucket" 337 | puts " - Delete the CloudWatch log" 338 | puts " - Delete the IAM role" 339 | puts " - Delete the SNS topic" 340 | elsif menuselect == "5" 341 | cloudtrail = CloudTrailAlarm.new() 342 | puts "\e[H\e[2J" 343 | puts "This will delete the trail in your current region." 344 | puts "No data will be deleted, but you will lose all settings." 345 | puts "Hit Enter if you really want to do this: " 346 | blah = gets.chomp() 347 | cloudtrail.disable_cloudtrail 348 | puts "Press Return to return to the main menu" 349 | blah = gets.chomp 350 | elsif menuselect == "7" 351 | menuselect = 7 352 | else 353 | puts "Error, please select a valid option" 354 | end 355 | end 356 | 357 | -------------------------------------------------------------------------------- /sec_dev_ops.rb: -------------------------------------------------------------------------------- 1 | # SecDevOps Toolkit code by rmogull@securosis.com 2 | # Copyright 2016 Rich Mogull and Securosis, LLC. with a Creative Commons Attribution, NonCommercial, Share Alike license- http://creativecommons.org/licenses/by-nc-sa/4.0/ 3 | 4 | # These code samples are meant to accompany Securosis SecDevOps, CCSK, and other training programs and workshops. 5 | # This is not a complete, functional application. 6 | 7 | # To function, you need a properly-formatted config.json file in the same directory as where this code runs. 8 | 9 | # You must install the listed gems.. 10 | 11 | 12 | require "rubygems" 13 | require "aws-sdk" 14 | require "json" 15 | require 'open-uri' 16 | require 'netaddr' 17 | 18 | # Optional- only needed if you set up a Chef server 19 | # require 'ridley' 20 | 21 | # Optional if you want to use pry for debugging 22 | # require 'pry' 23 | 24 | # The class below is only useful/needed if you have installed a Chef server per other lab instructions. 25 | # This is from an older lab that is no longer directly supported, but should still work. 26 | # Remove the "=begin" and "=end" to uncomment the block. 27 | 28 | =begin 29 | class ConfigManagement 30 | # This class integrates with Chef for configuration management. Right now it only has one method. 31 | def analyze 32 | # This method polls EC2 and polls Chef to identify any unmanaged instances. 33 | # Right now it uses the instance name since there is a bug in the Ridley SDK that limits pulling alternate attribures, but plan is to fix that soon 34 | 35 | # Load configuration and credentials from a JSON file 36 | 37 | # Load from config file in same directory as code 38 | # In the future, we will need to adjust this to rotate through all accounts and regions for the user. AssumeRole should help. 39 | config = JSON.load(File.read('config.json')) 40 | # credentials... using hard coded for this PoC, but really should be an assumerole in the future. 41 | # creds = Aws::Credentials.new("#{config["aws"]["AccessKey"]}", "#{config["aws"]["SecretKey"]}") 42 | # Create clients for the various services we need. Loading them all here and setting them as Class variables. 43 | @ec2 = Aws::EC2::Client.new(credentials: creds, region: "#{$region}") 44 | 45 | # Pull all instances in the region and create an empty array to hold their private DNS names 46 | instances = @ec2.describe_instances() 47 | instancelist = [] 48 | # go through each reservation, then each instance, and add the DNS name to the array 49 | instances.reservations.each do |reservation| 50 | reservation.instances.each do |instance| 51 | instancelist << instance.private_dns_name 52 | end 53 | end 54 | 55 | 56 | # Start a ridley connection to our Chef server. Pull the configuration from our file. 57 | 58 | chefconfig = config["chef"] 59 | 60 | #supress errors since Ridley is buggy; switch to "fatal" if it keeps showing up. 61 | Ridley::Logging.logger.level = Logger.const_get 'ERROR' 62 | ridley = Ridley.new( 63 | server_url: "#{config["chef"]["chefserver"]}", 64 | client_name: "#{config["chef"]["clientname"]}", 65 | client_key: "#{config["chef"]["keylocation"]}", 66 | ssl: { verify: false } 67 | ) 68 | 69 | # Ridley has a bug, so we need to work on the node name, which in our case is the same as the EC2 private DNS. For some reason the node.all doesn't pull IP addresses (it's supposed to) which is what we would prefer to use. 70 | nodes = ridley.node.all 71 | nodenames = nodes.map { |node| node.name } 72 | 73 | # For every EC2 instance, see if there is a corresponding Chef node. 74 | 75 | puts "" 76 | puts "" 77 | puts "Instance => managed?" 78 | puts "" 79 | instancelist.each do |thisinstance| 80 | managed = nodenames.include?(thisinstance) 81 | puts " #{thisinstance} #{managed} " 82 | end 83 | end 84 | end 85 | 86 | =end 87 | 88 | # class for incident response functions like quarantine. 89 | class IncidentResponse 90 | def initialize() 91 | # Load configuration and credentials from a JSON file. Right now hardcoded to config.json in the app drectory 92 | config = JSON.load(File.read('config.json')) 93 | 94 | # set the credentials based on the configuration file. Use this code if you are not running 95 | # on an isntance with an IAM role 96 | # creds = Aws::Credentials.new("#{config["aws"]["AccessKey"]}", "#{config["aws"]["SecretKey"]}") 97 | 98 | # Create clients for the various services we need. Loading them all here and setting them as Class variables. 99 | # Uncomment and use the next line if pulling credentials from the configuration file 100 | # @ec2 = Aws::EC2::Client.new(credentials: creds, region: "#{$region}") 101 | 102 | # The next line will set the client when running on an instance with an IAM role set 103 | @ec2 = Aws::EC2::Client.new(region: "#{$region}") 104 | 105 | # Set application configuration variables. 106 | # Remember that not all AWS services are available in all regions. Everything in this version of the tool should work. 107 | 108 | if $region == "us-west-1" 109 | @QuarantineGroup = "#{config["aws"]["RegionSettings"]["us-west-1"]["QuarantineSecurityGroup"]}" 110 | @ForensicsAMI = "#{config["aws"]["RegionSettings"]["us-west-1"]["AMI"]}" 111 | @AnalysisSecurityGroup = "#{config["aws"]["RegionSettings"]["us-west-1"]["AnalysisSecurityGroup"]}" 112 | @ForensicsSSHKey = "#{config["aws"]["RegionSettings"]["us-west-1"]["SSHKey"]}" 113 | @ForensicsUser = "#{config["aws"]["RegionSettings"]["us-west-1"]["User"]}" 114 | elsif $region == "us-west-2" 115 | @QuarantineGroup = "#{config["aws"]["RegionSettings"]["us-west-2"]["QuarantineSecurityGroup"]}" 116 | @ForensicsAMI = "#{config["aws"]["RegionSettings"]["us-west-2"]["AMI"]}" 117 | @AnalysisSecurityGroup = "#{config["aws"]["RegionSettings"]["us-west-2"]["AnalysisSecurityGroup"]}" 118 | @ForensicsSSHKey = "#{config["aws"]["RegionSettings"]["us-west-2"]["SSHKey"]}" 119 | @ForensicsUser = "#{config["aws"]["RegionSettings"]["us-west-2"]["User"]}" 120 | elsif $region == "us-east-1" 121 | @QuarantineGroup = "#{config["aws"]["RegionSettings"]["us-east-1"]["QuarantineSecurityGroup"]}" 122 | @ForensicsAMI = "#{config["aws"]["RegionSettings"]["us-east-1"]["AMI"]}" 123 | @AnalysisSecurityGroup = "#{config["aws"]["RegionSettings"]["us-east-1"]["AnalysisSecurityGroup"]}" 124 | @ForensicsSSHKey = "#{config["aws"]["RegionSettings"]["us-east-1"]["SSHKey"]}" 125 | @ForensicsUser = "#{config["aws"]["RegionSettings"]["us-east-1"]["User"]}" 126 | elsif $region == "eu-west-1" 127 | @QuarantineGroup = "#{config["aws"]["RegionSettings"]["eu-west-1"]["QuarantineSecurityGroup"]}" 128 | @ForensicsAMI = "#{config["aws"]["RegionSettings"]["eu-west-1"]["AMI"]}" 129 | @AnalysisSecurityGroup = "#{config["aws"]["RegionSettings"]["eu-west-1"]["AnalysisSecurityGroup"]}" 130 | @ForensicsSSHKey = "#{config["aws"]["RegionSettings"]["eu-west-1"]["SSHKey"]}" 131 | @ForensicsUser = "#{config["aws"]["RegionSettings"]["eu-west-1"]["User"]}" 132 | elsif $region == "ap-southeast-1" 133 | @QuarantineGroup = "#{config["aws"]["RegionSettings"]["ap-southeast-1"]["QuarantineSecurityGroup"]}" 134 | @ForensicsAMI = "#{config["aws"]["RegionSettings"]["ap-southeast-1"]["AMI"]}" 135 | @AnalysisSecurityGroup = "#{config["aws"]["RegionSettings"]["ap-southeast-1"]["AnalysisSecurityGroup"]}" 136 | @ForensicsSSHKey = "#{config["aws"]["RegionSettings"]["ap-southeast-1"]["SSHKey"]}" 137 | @ForensicsUser = "#{config["aws"]["RegionSettings"]["ap-southeast-1"]["User"]}" 138 | elsif $region == "ap-southeast-2" 139 | @QuarantineGroup = "#{config["aws"]["RegionSettings"]["ap-southeast-2"]["QuarantineSecurityGroup"]}" 140 | @ForensicsAMI = "#{config["aws"]["RegionSettings"]["ap-southeast-2"]["AMI"]}" 141 | @AnalysisSecurityGroup = "#{config["aws"]["RegionSettings"]["ap-southeast-2"]["AnalysisSecurityGroup"]}" 142 | @ForensicsSSHKey = "#{config["aws"]["RegionSettings"]["ap-southeast-2"]["SSHKey"]}" 143 | @ForensicsUser = "#{config["aws"]["RegionSettings"]["ap-southeast-2"]["User"]}" 144 | elsif $region == "ap-northeast-1" 145 | @QuarantineGroup = "#{config["aws"]["RegionSettings"]["ap-northeast-1"]["QuarantineSecurityGroup"]}" 146 | @ForensicsAMI = "#{config["aws"]["RegionSettings"]["ap-northeast-1"]["AMI"]}" 147 | @AnalysisSecurityGroup = "#{config["aws"]["RegionSettings"]["ap-northeast-1"]["AnalysisSecurityGroup"]}" 148 | @ForensicsSSHKey = "#{config["aws"]["RegionSettings"]["ap-northeast-1"]["SSHKey"]}" 149 | @ForensicsUser = "#{config["aws"]["RegionSettings"]["ap-northeast-1"]["User"]}" 150 | elsif $region == "sa-east-1" 151 | @QuarantineGroup = "#{config["aws"]["RegionSettings"]["sa-east-1"]["QuarantineSecurityGroup"]}" 152 | @ForensicsAMI = "#{config["aws"]["RegionSettings"]["sa-east-1"]["AMI"]}" 153 | @AnalysisSecurityGroup = "#{config["aws"]["RegionSettings"]["sa-east-1"]["AnalysisSecurityGroup"]}" 154 | @ForensicsSSHKey = "#{config["aws"]["RegionSettings"]["sa-east-1"]["SSHKey"]}" 155 | @ForensicsUser = "#{config["aws"]["RegionSettings"]["sa-east-1"]["User"]}" 156 | else 157 | #default to us-east-1 in case something fails 158 | @QuarantineGroup = "#{config["aws"]["RegionSettings"]["us-east-1"]["QuarantineSecurityGroup"]}" 159 | @ForensicsAMI = "#{config["aws"]["RegionSettings"]["us-east-1"]["AMI"]}" 160 | @AnalysisSecurityGroup = "#{config["aws"]["RegionSettings"]["us-east-1"]["AnalysisSecurityGroup"]}" 161 | @ForensicsSSHKey = "#{config["aws"]["RegionSettings"]["us-east-1"]["SSHKey"]}" 162 | @ForensicsUser = "#{config["aws"]["RegionSettings"]["us-east-1"]["User"]}" 163 | end 164 | end 165 | 166 | def quarantine(instance_id) 167 | # this method moves the provided instance into the Quarantine security group defined in the config file. 168 | puts "" 169 | puts "Quarantining #{instance_id}..." 170 | quarantine = @ec2.modify_instance_attribute(instance_id: "#{instance_id}", groups: ["#{@QuarantineGroup}"]) 171 | puts "#{instance_id} moved to the Quarantine security group from your configuration settings." 172 | end 173 | 174 | def tag(instance_id) 175 | # this method adds an "status => IR" tag to the instance. 176 | # If you properly configure your IAM policies, this will move ownership fo the instance to the security 177 | # team and isolate it so no one else can terminate/stop/modify/etc. 178 | puts "Tagging instance with 'IR'..." 179 | tag = @ec2.create_tags(resources: ["#{instance_id}"], tags: [ 180 | { 181 | key: "SecurityStatus", 182 | value: "IR", 183 | }, 184 | ],) 185 | puts "Instance tagged and IAM restrictions applied." 186 | end 187 | 188 | def snapshot(instance_id) 189 | # This method determines the volume IDs for the instance, then creates snapshots of those def volumes(args) 190 | # Get the instance details for the instance 191 | instance_details = @ec2.describe_instances( 192 | instance_ids: ["#{instance_id}"], 193 | ) 194 | 195 | # find the attached block devices, then the ebs volumes, then the volume ID for each EBS volume. This involves walking the response tree. 196 | 197 | puts "Identifying attached volumes..." 198 | block_devices = instance_details.reservations.first.instances.first.block_device_mappings 199 | ebs = block_devices.map(&:ebs) 200 | volumes = ebs.map(&:volume_id) 201 | # start an empty array to later track and attach the snapshot to a forensics storage volume 202 | @snap = [] 203 | volumes.each do |vol| 204 | puts "Volume #{vol} identified; creating snapshot" 205 | # Create a snapshot of each volume and add the volume and instance ID to the description. 206 | # We do this since you can't apply a name tag until the snapshot is created, and we don't want to slow down the process. 207 | timestamp = Time.new 208 | snap = @ec2.create_snapshot( 209 | volume_id: "#{vol}", 210 | description: "IR volume #{vol} of instance #{instance_id} at #{timestamp}", 211 | ) 212 | puts "Snapshots complete with description: IR volume #{vol} of instance #{instance_id} at #{timestamp}" 213 | # get the snapshot id and add it to an array for this instance of the class so we can use it later for forensics 214 | @snap << snap.snapshot_id 215 | end 216 | # Launch a thread to tag the snapshots with "IR" to restrict to the security team. 217 | # We do this since we need to wait until the snapshot is created for the tags to work. 218 | 219 | snapthread = Thread.new do 220 | snap_array = Array.new 221 | @snap.each do |snap_id| 222 | snap_array << "#{snap_id}" 223 | end 224 | 225 | status = false 226 | until status == true do 227 | snap_details = @ec2.describe_snapshots(snapshot_ids: snap_array) 228 | snap_details.each do |snapID| 229 | if snap_details.snapshots.first.state == "completed" 230 | status = true 231 | else 232 | status = false 233 | end 234 | end 235 | end 236 | # Tag the snapshot 237 | @ec2.create_tags( 238 | resources: snap_array, 239 | tags: [ 240 | { 241 | key: "SecurityStatus", 242 | value: "IR", 243 | }, 244 | ], 245 | ) 246 | 247 | end 248 | return @snap 249 | end 250 | 251 | 252 | def forensics_analysis(snapshot_array) 253 | # This method launches an instance and then creates and attaches storage volumes of the IR snapshots. 254 | # It also opens Security Group access between the forensics and target instance. 255 | 256 | # set starting variables 257 | alpha = ("f".."z").to_a 258 | count = 0 259 | block_device_map = Array.new 260 | 261 | # Build the content for the block device mappings to add each snapshot as a volume. 262 | # Device mappings start as sdf and continue up to sdz, which is way more than you will ever need. 263 | snapshot_array.each do |snapshot_id| 264 | count += 1 265 | # pull details to get the volume size 266 | snap_details = @ec2.describe_snapshots(snapshot_ids: ["#{snapshot_id}"]) 267 | vol_size = snap_details.snapshots.first.volume_size 268 | # create the string for the device mapping 269 | device = "/dev/sd" + alpha[count].to_s 270 | # build the hash we will need later for the bock device mappings 271 | temphash = Hash.new 272 | temphash = { 273 | device_name: "#{device}", 274 | ebs: { 275 | snapshot_id: "#{snapshot_id}", 276 | volume_size: vol_size, 277 | volume_type: "standard", 278 | } 279 | } 280 | # add the hash to our array 281 | block_device_map << temphash 282 | 283 | end 284 | 285 | # Notify user that this will run in the background in case the snapshots are large and it takes a while 286 | 287 | puts "A forensics analysis server is being launched in the background in #{@region} with the name" 288 | puts "'Forensics' and the snapshots attached as volumes starting at /dev/sdf " 289 | puts "(which may show as /dev/xvdf). Use host key #{@ForensicsSSHKey} for user #{@ForensicsUser}" 290 | puts "" 291 | 292 | # Create array to get the snapshot status via API 293 | 294 | snaparray = Array.new 295 | snapshot_array.each do |snap_id| 296 | snaparray << "#{snap_id}" 297 | end 298 | 299 | # Launch the rest as a thread since waiting for the snapshot may otherwise slow the program down. 300 | 301 | thread = Thread.new do 302 | # Get status of snapshots and check to see if any of them are still pending. Loop until they are all ready. 303 | status = false 304 | until status == true do 305 | # wait 5 seconds to reduce API call load 306 | sleep 5 307 | snap_details = @ec2.describe_snapshots(snapshot_ids: snaparray) 308 | snap_details.each do |snapID| 309 | if snap_details.snapshots.first.state == "completed" 310 | status = true 311 | else 312 | status = false 313 | end 314 | end 315 | end 316 | 317 | forensic_instance = @ec2.run_instances( 318 | image_id: "#{ @ForensicsAMI}", 319 | min_count: 1, 320 | max_count: 1, 321 | instance_type: "t1.micro", 322 | key_name: "#{@ForensicsSSHKey}", 323 | security_group_ids: ["#{@AnalysisSecurityGroup}"], 324 | placement: { 325 | availability_zone: "us-west-2a" 326 | }, 327 | block_device_mappings: block_device_map 328 | ) 329 | # Tag the instance so you can find it later 330 | temp_id = forensic_instance.instances.first.instance_id 331 | 332 | tag = @ec2.create_tags( 333 | resources: ["#{temp_id}"], 334 | tags: [ 335 | { 336 | key: "IncidentResponseID", 337 | value: "Forensic Analysis Server for #{instance_id}", 338 | }, 339 | { 340 | key: "SecurityStatus", 341 | value: "IR", 342 | }, 343 | { 344 | key: "Name", 345 | value: "Forensics", 346 | }, 347 | ], 348 | ) 349 | 350 | # create variable to store the IR server in the Trinity database 351 | # TODO store this variable in the database to track later for the incident 352 | ir_server_details = {:instance_id => "#{instance_id}", :timestamp => timestamp, :incident_id => "placeholder"} 353 | end 354 | 355 | end 356 | 357 | def store_metadata(instance_id) 358 | # Method collects the instance metadata and stores as a JSON variable 359 | 360 | data = @ec2.describe_instances(instance_ids: ["#{instance_id}"]) 361 | timestamp = Time.new 362 | incident_id = {:timestamp => timestamp, :incident_id => "placeholder"} 363 | metadata = data.to_h 364 | metadata = metadata.to_json 365 | puts "Instance metadata recorded" 366 | end 367 | 368 | def add_remove_security_group(instance_id, secgroup_id, action) 369 | # add a security group to the instance 370 | 371 | # get instance details 372 | instance_details = @ec2.describe_instances( 373 | instance_ids: ["#{instance_id}"], 374 | ) 375 | 376 | # identify IP and security groups 377 | puts "Identifying internal IP address..." 378 | instance_IP = instance_details.reservations.first.instances.first.private_ip_address 379 | puts "IP address is #{instance_IP}" 380 | puts "" 381 | puts "Identifying current security groups..." 382 | securitygroups = instance_details.reservations.first.instances.first.security_groups 383 | secgroupID = securitygroups.map(&:group_id) 384 | puts secgroupID 385 | puts "" 386 | if action == "add" 387 | puts "Adding the new security group" 388 | secgroupID << secgroup_id 389 | update_sec_group = @ec2.modify_instance_attribute(instance_id: "#{instance_id}", groups: secgroupID) 390 | puts "Security group added, instance is now in: #{secgroupID}" 391 | elsif action == "remove" 392 | puts "Removing the new security group" 393 | secgroupID.delete(secgroup_id) 394 | update_sec_group = @ec2.modify_instance_attribute(instance_id: "#{instance_id}", groups: secgroupID) 395 | puts "Security group removed, instance is no longer in: #{secgroupID}" 396 | else 397 | puts "Invalid action: #{action}" 398 | end 399 | end 400 | 401 | def block_ip (instance_id, cidr) 402 | puts "Determining current subnet for the instance. (Note: this version only works for the primary network interface):" 403 | metadata = @ec2.describe_instances(instance_ids: ["#{instance_id}"]) 404 | subnet = metadata.reservations.first.instances.first.subnet_id 405 | vpc = metadata.reservations.first.instances.first.vpc_id 406 | puts "Instance is located in subnet #{subnet} of VPC #{vpc}" 407 | # pull any ACL associated with the VPC 408 | acl_list = @ec2.describe_network_acls( 409 | filters: [ 410 | { 411 | name: "vpc-id", 412 | values: ["#{vpc}"] 413 | }, 414 | ], 415 | ) 416 | # find one associated with the subnet 417 | acl_list.network_acls.each do |acl| 418 | # pull the lowest number entry to place our rule before it. 419 | # TODO eventually, this needs to be smarter and dynamically move the rules around. 420 | rule_number = 1000000000 421 | acl.entries.each do |entry| 422 | if entry.rule_number < rule_number 423 | rule_number = entry.rule_number 424 | end 425 | end 426 | rule_number = rule_number - 1 427 | acl.associations.each do |association| 428 | if association.subnet_id == subnet 429 | @ec2.create_network_acl_entry( 430 | network_acl_id: acl.network_acl_id, 431 | # required 432 | rule_number: rule_number, 433 | # required 434 | protocol: "-1", 435 | # required 436 | rule_action: "deny", 437 | # required 438 | egress: false, 439 | # required 440 | cidr_block: cidr, 441 | ) 442 | puts "All traffic from #{cidr} blocked for ACL #{acl.network_acl_id} on subnet #{subnet}" 443 | end 444 | end 445 | end 446 | 447 | end 448 | end 449 | 450 | # class for incident analysis 451 | 452 | class InstanceAnalysis 453 | def initialize(instance_id) 454 | instance_id = instance_id 455 | 456 | # Load configuration and credentials from a JSON file. Right now hardcoded to config.json in the app drectory. 457 | config = JSON.load(File.read('config.json')) 458 | 459 | # Uncomment the lines below to pull credentials from the config file and set the service clients. Otherwise the IAM role is used. 460 | =begin 461 | creds = Aws::Credentials.new("#{config["aws"]["AccessKey"]}", "#{config["aws"]["SecretKey"]}") 462 | # Create clients for the various services we need. Loading them all here and setting them as Class variables. 463 | @ec2 = Aws::EC2::Client.new(credentials: creds, region: "#{$region}") 464 | @@autoscaling = Aws::AutoScaling::Client.new(credentials: creds, region: "#{$region}") 465 | @@loadbalance = elasticloadbalancing = Aws::ElasticLoadBalancing::Client.new(credentials: creds, region: "#{$region}") 466 | =end 467 | # Created needed service endpoints using the IAM role assigned to the instance the code is running on 468 | 469 | @ec2 = Aws::EC2::Client.new(region: "#{$region}") 470 | @@autoscaling = Aws::AutoScaling::Client.new(region: "#{$region}") 471 | @@loadbalance = elasticloadbalancing = Aws::ElasticLoadBalancing::Client.new(region: "#{$region}") 472 | 473 | # Load the analysis rules 474 | @@rules = JSON.load(File.read('analysis_rules.json')) 475 | end 476 | 477 | # method to determine if instance is in an autoscaling group 478 | def autoscale(instance_id) 479 | metadata = @ec2.describe_instances(instance_ids: ["#{instance_id}"]) 480 | tags = metadata.reservations.first.instances.first 481 | # covert to hash to make this easier 482 | tags = tags.to_h 483 | tags = tags[:tags] 484 | # quick check to avoid having to iterate through all the tags to see if the one we need is there. 485 | temp_tags = tags.to_s 486 | if temp_tags.include?("aws:autoscaling:groupName") 487 | tags.each do |curtag| 488 | if curtag[:key] == "aws:autoscaling:groupName" 489 | @autoscaling = curtag[:value] 490 | end 491 | end 492 | else 493 | @autoscaling = "false" 494 | end 495 | end 496 | 497 | def get_security_groups(instance_id) 498 | # This method determines the security groups for an instance. It does not check multiple network interfaces. 499 | # It also determines all the open ports for those groups, and the destinations 500 | 501 | # Pull the security groups for our instance 502 | metadata = @ec2.describe_instances(instance_ids: ["#{instance_id}"]) 503 | 504 | secgroups = metadata.reservations.first.instances.first.security_groups 505 | # Get the group IDs 506 | secgroups = secgroups.map(&:group_id) 507 | # Now pull the details for all those groups 508 | secgroups = @ec2.describe_security_groups(group_ids: secgroups) 509 | 510 | @portlist = {} 511 | @secgrouplist = [] 512 | # interate through each security group 513 | secgroups.security_groups.each do |group| 514 | # pull the security group IDs so we can use them later to find connections 515 | @secgrouplist << group.group_id 516 | # now pull all the ports into a hash. Start by seeing if port is already on list, if not, add the key 517 | group.ip_permissions.each do |port| 518 | if @portlist.has_key?(port.from_port.to_s) == false 519 | @portlist[port.from_port.to_s] = [] 520 | end 521 | # Now iterate through the ip ranges to get the ip list 522 | port.ip_ranges.each do |cidr| 523 | if cidr.cidr_ip != nil 524 | tempport = @portlist[port.from_port.to_s] 525 | tempport << cidr.cidr_ip 526 | @portlist[port.from_port.to_s] = tempport 527 | end 528 | end 529 | # pull other security groups allowed to connect to this one 530 | port.user_id_group_pairs.each do |internalsg| 531 | if internalsg.group_id != nil 532 | tempport = @portlist[port.from_port.to_s] 533 | tempport << internalsg.group_id 534 | @portlist[port.from_port.to_s] = tempport 535 | # this may be redundent, keeping it for now in case we just want a short list of connected security groups 536 | end 537 | end 538 | end 539 | 540 | end 541 | end 542 | end 543 | 544 | def region 545 | # A method for setting the availability zone 546 | # Pull the configuration so we only show regions that are configured 547 | configfile = File.read('config.json') 548 | config = JSON.parse(configfile) 549 | 550 | puts "\e[H\e[2J" 551 | puts "Current region: #{$region}. Select a new region:" 552 | puts "(Only regions you have configured are shown)" 553 | puts "" 554 | puts "" 555 | 556 | if config["aws"]["RegionSettings"].has_key?('us-east-1') 557 | puts "1. us-east-1 (Virginia)" 558 | end 559 | if config["aws"]["RegionSettings"].has_key?('us-west-1') 560 | puts "2. us-west-1 (California)" 561 | end 562 | if config["aws"]["RegionSettings"].has_key?('us-west-2') 563 | puts "3. us-west-2 (Oregon)" 564 | end 565 | if config["aws"]["RegionSettings"].has_key?('eu-west-1') 566 | puts "4. eu-west-1 (Ireland)" 567 | end 568 | if config["aws"]["RegionSettings"].has_key?('ap-southeast-1') 569 | puts "5. ap-southeast-1 (Singapore)" 570 | end 571 | if config["aws"]["RegionSettings"].has_key?('ap-southeast-2') 572 | puts "6. ap-southeast-2 (Sydney)" 573 | end 574 | if config["aws"]["RegionSettings"].has_key?('ap-northeast-1') 575 | puts "7. ap-northeast-1 (Tokyo)" 576 | end 577 | if config["aws"]["RegionSettings"].has_key?('sa-east-1') 578 | puts "8. sa-east-1 (Sao Paulo)" 579 | end 580 | 581 | 582 | puts "" 583 | print "New region: " 584 | option = gets.chomp 585 | $region = case option 586 | when "1" then "us-east-1" 587 | when "2" then "us-west-1" 588 | when "3" then "us-west-2" 589 | when "4" then "eu-west-1" 590 | when "5" then "ap-southeast-1" 591 | when "6" then "ap-southeast-2" 592 | when "7" then "ap-northeast-1" 593 | when "8" then "sa-east-1" 594 | else puts "Error, select again:" 595 | end 596 | 597 | end 598 | 599 | # Body code 600 | # Load defaults. Rightnow, just the region. 601 | configfile = File.read('config.json') 602 | config = JSON.parse(configfile) 603 | $region = "#{config["aws"]["DefaultRegion"]}" 604 | 605 | 606 | menuselect = 0 607 | until menuselect == 7 do 608 | puts "\e[H\e[2J" 609 | puts "Welcome to the Securosis SecDevOps Learning Lab. Please select an action:" 610 | puts "Current region is #{$region}" 611 | puts "" 612 | puts "1. Run it" 613 | puts "2. Config Management" 614 | puts "3. " 615 | puts "4. " 616 | puts "5. " 617 | puts "6. Change region" 618 | puts "7. Exit" 619 | puts "" 620 | print "Select: " 621 | menuselect = gets.chomp 622 | if menuselect == "1" 623 | puts "\e[H\e[2J" 624 | print "Enter instance ID: " 625 | instance_id = gets.chomp 626 | 627 | incident_response = IncidentResponse.new() 628 | incident_response.quarantine(instance_id) 629 | incident_response.tag(instance_id) 630 | snap_array = incident_response.snapshot(instance_id) 631 | incident_response.forensics_analysis(snap_array) 632 | incident_response.store_metadata(instance_id) 633 | print "Enter security group ID: " 634 | secgroup_id = gets.chomp() 635 | print "Add or remove the security group? (add/remove): " 636 | action = gets.chomp() 637 | incident_response.add_remove_security_group(instance_id, secgroup_id, action) 638 | print "Enter the CIDR to block in the Access Control List: " 639 | cidr = gets.chomp() 640 | incident_response.block_ip(instance_id, cidr) 641 | 642 | puts "" 643 | puts "Press Return to return to the main menu" 644 | blah = gets.chomp 645 | elsif menuselect == "2" 646 | puts "\e[H\e[2J" 647 | config = ConfigManagement.new() 648 | config.analyze 649 | puts "Press Return to return to the main menu" 650 | blah = gets.chomp 651 | elsif menuselect == "3" 652 | puts "\e[H\e[2J" 653 | 654 | puts "Press Return to return to the main menu" 655 | blah = gets.chomp 656 | elsif menuselect == "4" 657 | puts "\e[H\e[2J" 658 | puts "Press Return to return to the main menu" 659 | blah = gets.chomp 660 | elsif menuselect == "5" 661 | 662 | puts "Press Return to return to the main menu" 663 | blah = gets.chomp 664 | elsif menuselect == "6" 665 | region 666 | elsif menuselect == "7" 667 | menuselect = 7 668 | else 669 | puts "Error, please select a valid option" 670 | end 671 | end 672 | --------------------------------------------------------------------------------