├── .gitignore ├── ANFSamplesOverview.png ├── Abuse.ch ├── cfn-templates │ ├── ANFAbuseHostfile.yaml │ ├── AbuseCH.yaml │ └── AbuseCHJA3.yaml └── src │ ├── ANFAbuseHostfile.js │ ├── AbuseCH.js │ └── AbuseCHJA3.js ├── AllowListGenerator ├── README.md ├── cloudwatch-logs │ ├── README.md │ ├── cfn-template │ │ └── template.yaml │ ├── images │ │ ├── arch-diagram.png │ │ ├── cloudwatch-metrics-example.png │ │ ├── dynamodb-table-example.png │ │ └── nfw-rule-group-example.png │ └── src │ │ └── lambda.py └── s3-logs │ ├── README.md │ ├── cfn-templates │ ├── template-without-nfw-rule-group.yaml │ └── template.yaml │ ├── images │ ├── arch-diagram.png │ ├── dynamodb-table-example.png │ └── nfw-rule-group-example.png │ └── src │ └── s3lambda.py ├── Alphasoc ├── cfn-templates │ └── AlphasocEncryptedDNS.yaml └── src │ └── EncryptedDNS.js ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── EmergingThreats ├── cfn-templates │ ├── EmergingThreatsBotCC.yaml │ └── EmergingThreatsIPFiltering.yaml └── src │ ├── EmergingBotCC.js │ └── EmergingThreats.js ├── LICENSE ├── LinodeAddresses ├── cfn-templates │ └── LinodeAddresses.yaml └── src │ └── LinodeAddresses.js ├── NfwSlackIntegration ├── README.md ├── docs │ └── NfwAlerts_Slack_Integration.docx ├── src │ ├── base.yml │ ├── decentralized-deployment.yml │ ├── igw-ingress-route.yml │ ├── protected-subnet-route.yml │ ├── slack-lambda.py │ ├── slack-lambda.py.zip │ └── slackLambda.yml └── test │ └── TestSteps.txt ├── README.md ├── SFTP-FQDN ├── src │ └── SFTP-FQDN.js └── templates │ └── SFTP-FQDN.yaml ├── SpamHaus ├── cfn-templates │ ├── SpamHausDropIPFiltering.yaml │ └── SpamHausEDropIPFiltering-Deprecated.yaml └── src │ ├── SpamHausDropIPFiltering.py │ ├── SpamHauseDropIPFiltering-Deprecated.js │ └── SpamHauseEDropIPFiltering-Deprecated.js ├── TLSFingerprint ├── cnf-templates │ └── TLSFingerprint.yaml └── src │ ├── TLSFingerprint-Deprecated.js │ └── TLSFingerprint.py └── TorProject ├── cfn-templates └── TorProjectIPFiltering.yaml └── src ├── TorProjectIPFiltering-Deprecated.js └── TorProjectIPFiltering.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /ANFSamplesOverview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-network-firewall-automation-examples/ffb519a62508d00886deb839a07e70f628eccbb8/ANFSamplesOverview.png -------------------------------------------------------------------------------- /Abuse.ch/cfn-templates/ANFAbuseHostfile.yaml: -------------------------------------------------------------------------------- 1 | Parameters: 2 | RGName: 3 | Type: String 4 | Default: "AbuseCH-Hostfile-DomainList" 5 | 6 | Resources: 7 | SNIRuleGroup: 8 | Type: 'AWS::NetworkFirewall::RuleGroup' 9 | Properties: 10 | RuleGroupName: !Sub "${RGName}-${AWS::StackName}" 11 | Type: STATEFUL 12 | RuleGroup: 13 | RulesSource: 14 | RulesSourceList: 15 | GeneratedRulesType: DENYLIST 16 | Targets: 17 | - www.this-domain-will-be-updated-by-lambda.com 18 | TargetTypes: 19 | - TLS_SNI 20 | - HTTP_HOST 21 | Capacity: 5000 22 | Description: !Sub ' -- The CloudWatch Events Rule: AbuseCHHostfileRulegroupHourlyTrigger, triggers a daily update of this list.' 23 | Tags: 24 | - Key: "ProjectName" 25 | Value: "AbuseCH-HostfileDomainList" 26 | - Key: "downloaded-from" 27 | Value: "https://urlhaus.abuse.ch/downloads/hostfile/" 28 | - Key: "description" 29 | Value: "abuse.ch URLhaus Host file" 30 | - Key: "terms-of-use" 31 | Value: "https://urlhaus.abuse.ch/api/" 32 | LambdaExecutionRole: 33 | Type: AWS::IAM::Role 34 | Properties: 35 | AssumeRolePolicyDocument: 36 | Version: '2012-10-17' 37 | Statement: 38 | - Effect: Allow 39 | Principal: 40 | Service: 41 | - lambda.amazonaws.com 42 | Action: 43 | - sts:AssumeRole 44 | Path: "/" 45 | Policies: 46 | - PolicyName: LambdaLogs 47 | PolicyDocument: 48 | Version: '2012-10-17' 49 | Statement: 50 | - Effect: Allow 51 | Action: 52 | - 'logs:CreateLogStream' 53 | - 'logs:PutLogEvents' 54 | Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*' 55 | - PolicyName: NetworkFirewall 56 | PolicyDocument: 57 | Version: '2012-10-17' 58 | Statement: 59 | - Effect: Allow 60 | Action: 61 | - 'network-firewall:*' 62 | Resource: 63 | - !GetAtt SNIRuleGroup.RuleGroupArn 64 | Tags: 65 | - Key: "ProjectName" 66 | Value: "AbuseCH-HostfileDomainList" 67 | ScheduledRule: 68 | Type: AWS::Events::Rule 69 | Properties: 70 | Description: "AbuseCHHostfileRulegroupHourlyTrigger" 71 | ScheduleExpression: "cron(0 0 * * ? *)" 72 | State: "ENABLED" 73 | Targets: 74 | - Arn: 75 | Fn::GetAtt: 76 | - "LambdaFunction" 77 | - "Arn" 78 | Id: "TargetFunctionV1" 79 | LambdaLogGroup: 80 | Type: AWS::Logs::LogGroup 81 | Properties: 82 | LogGroupName: !Sub "/aws/lambda/${LambdaFunction}" 83 | DeletionPolicy: Retain 84 | PermissionForEventsToInvokeLambda: 85 | Type: AWS::Lambda::Permission 86 | Properties: 87 | FunctionName: !Ref "LambdaFunction" 88 | Action: "lambda:InvokeFunction" 89 | Principal: "events.amazonaws.com" 90 | SourceArn: 91 | Fn::GetAtt: 92 | - "ScheduledRule" 93 | - "Arn" 94 | LambdaInvoke: 95 | Type: AWS::CloudFormation::CustomResource 96 | Version: "1.0" 97 | Properties: 98 | ServiceToken: !GetAtt LambdaFunction.Arn 99 | LambdaFunction: 100 | Type: AWS::Lambda::Function 101 | Properties: 102 | Role: !GetAtt LambdaExecutionRole.Arn 103 | Runtime: nodejs16.x 104 | Handler: index.handler 105 | Timeout: 60 106 | Description: Used to download the hostfile list from Abuse.ch daily 107 | Tags: 108 | - 109 | Key: "ProjectName" 110 | Value: "AbuseCH-HostfileDomainList" 111 | Code: 112 | ZipFile: !Sub | 113 | var AWS = require("aws-sdk"); 114 | var response = require('cfn-response'); 115 | const https = require("https"); 116 | 117 | const hostfileUrl = "https://urlhaus.abuse.ch/downloads/hostfile/"; 118 | 119 | const nf = new AWS.NetworkFirewall(); 120 | 121 | async function getDomains (){ 122 | var listOfDomains = []; 123 | console.log("Fetching the list of domains from " + hostfileUrl); 124 | return new Promise((resolve, reject) => { 125 | let dataString = ''; 126 | let post_req = https.request(hostfileUrl, (res) => { 127 | res.setEncoding("utf8"); 128 | res.on('data', chunk => { 129 | dataString += chunk; 130 | }); 131 | res.on('end', () => { 132 | //console.log(dataString); 133 | listOfDomains = dataString 134 | .split(/\r?\n/) 135 | .filter((line) => line.match(/^\d+/)) 136 | .map((line)=> {return line.replace(/127.0.0.1\t/,'').toLowerCase()}) 137 | .filter((line) => line.match(/^(?!:\/\/)(?=.{1,255}$)((.{1,63}\.){1,127}(?![0-9]*$)[a-z0-9-]+\.?)$/)); 138 | console.log("Fetched " + listOfDomains.length + " Domains"); 139 | resolve(listOfDomains); 140 | }); 141 | res.on('error', (err) => { 142 | reject(err); 143 | }); 144 | }); 145 | post_req.end(); 146 | }); 147 | } 148 | 149 | async function updateRuleGroup(arn, domains){ 150 | let params = {Type: "STATEFUL", RuleGroupArn: arn}; 151 | let res = await nf.describeRuleGroup(params).promise(); 152 | if (res.RuleGroupResponse) { 153 | console.log("Found destination rulegroup"); 154 | res.RuleGroup.RulesSource.RulesSourceList.Targets = domains; 155 | res.RuleGroupName = res.RuleGroupResponse.RuleGroupName; 156 | res.Description = "Last updated: " + new Date().toUTCString() + " -- The CloudWatch Events Rule: AbuseCHHostfileRulegroupHourlyTrigger, triggers a daily update of this list."; 157 | res.Type = res.RuleGroupResponse.Type; 158 | delete res.Capacity; 159 | delete res.RuleGroupResponse; 160 | 161 | console.log("Updating rules"); 162 | let result = await nf.updateRuleGroup(res).promise(); 163 | if (result) { 164 | console.log("Updated '" + res.RuleGroupName); 165 | } else { 166 | console.log("Error updating '" + res.RuleGroupName + "'..."); 167 | } 168 | } else { 169 | console.log("No matching Rule Group found"); 170 | } 171 | return; 172 | } 173 | 174 | exports.handler = async (event, context) => { 175 | if (event.RequestType == "Delete") { 176 | await response.send(event, context, "SUCCESS"); 177 | return; 178 | } 179 | 180 | let sourceArn = '${SNIRuleGroup.RuleGroupArn}'; 181 | let domains = await getDomains(sourceArn); 182 | 183 | if (domains) { 184 | console.log("Using a list of: " + domains.length + " domains"); 185 | 186 | await updateRuleGroup(sourceArn, domains); 187 | if (event.ResponseURL) await response.send(event, context, "SUCCESS"); 188 | } else { 189 | console.log("Error fetching a list of domains from: ", sourceArn); 190 | if (event.ResponseURL) await response.send(event, context, "FAILED"); 191 | } 192 | 193 | return; 194 | }; 195 | -------------------------------------------------------------------------------- /Abuse.ch/cfn-templates/AbuseCH.yaml: -------------------------------------------------------------------------------- 1 | Parameters: 2 | AbuseRG1Name: 3 | Description: "The name of the first Rule Group created to hold the rules" 4 | Type: String 5 | Default: 'AutoUpdating-AbuseCH-List1' 6 | AbuseRG2Name: 7 | Description: "The name of the second Rule Group created to hold the rules" 8 | Type: String 9 | Default: 'AutoUpdating-AbuseCH-List2' 10 | AbuseRuleGroupAction: 11 | Type: String 12 | Description: "Used to define the action to take on a matching rule if found" 13 | Default : 'drop' 14 | AllowedValues: 15 | - 'alert' 16 | - 'drop' 17 | 18 | Resources: 19 | AbuseRG1: 20 | Type: 'AWS::NetworkFirewall::RuleGroup' 21 | Properties: 22 | RuleGroupName: !Sub "${AWS::StackName}-${AbuseRG1Name}-${AbuseRuleGroupAction}" 23 | Type: STATEFUL 24 | RuleGroup: 25 | RulesSource: 26 | RulesString: '#This will be updated via the Lambda function' 27 | Capacity: 2500 28 | Description: >- 29 | Used to track a list of Suricata rules from https://sslbl.abuse.ch/blacklist/sslblacklist_tls_cert.rules. It is updated daily by the CloudWatch Event: AbuseCHDailyTrigger. List 1 of 2. 30 | Tags: 31 | - 32 | Key: "ProjectName" 33 | Value: "AbuseCHFiltering" 34 | AbuseRG2: 35 | Type: 'AWS::NetworkFirewall::RuleGroup' 36 | Properties: 37 | RuleGroupName: !Sub "${AWS::StackName}-${AbuseRG2Name}-${AbuseRuleGroupAction}" 38 | Type: STATEFUL 39 | RuleGroup: 40 | RulesSource: 41 | RulesString: '#This will be updated via the Lambda function' 42 | Capacity: 2500 43 | Description: >- 44 | Used to track a list of Suricata rules from https://sslbl.abuse.ch/blacklist/sslblacklist_tls_cert.rules. It is updated daily by the CloudWatch Event: AbuseCHDailyTrigger. List 2 of 2. 45 | Tags: 46 | - 47 | Key: "ProjectName" 48 | Value: "AbuseCHFiltering" 49 | LambdaExecutionRole: 50 | Type: AWS::IAM::Role 51 | Properties: 52 | AssumeRolePolicyDocument: 53 | Version: '2012-10-17' 54 | Statement: 55 | - Effect: Allow 56 | Principal: 57 | Service: 58 | - lambda.amazonaws.com 59 | Action: 60 | - sts:AssumeRole 61 | Path: "/" 62 | Policies: 63 | - PolicyName: LambdaLogs 64 | PolicyDocument: 65 | Version: '2012-10-17' 66 | Statement: 67 | - Effect: Allow 68 | Action: 69 | - 'logs:CreateLogStream' 70 | - 'logs:PutLogEvents' 71 | Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*' 72 | - PolicyName: NetworkFirewall 73 | PolicyDocument: 74 | Version: '2012-10-17' 75 | Statement: 76 | - Effect: Allow 77 | Action: 78 | - 'network-firewall:*' 79 | Resource: 80 | - !GetAtt AbuseRG1.RuleGroupArn 81 | - !GetAtt AbuseRG2.RuleGroupArn 82 | Tags: 83 | - 84 | Key: "ProjectName" 85 | Value: "AbuseCHFiltering" 86 | LambdaLogGroup: 87 | Type: AWS::Logs::LogGroup 88 | Properties: 89 | LogGroupName: !Sub "/aws/lambda/${LambdaFunction}" 90 | DeletionPolicy: Retain 91 | ScheduledRule: 92 | Type: AWS::Events::Rule 93 | Properties: 94 | Description: "AbuseCHDailyTrigger" 95 | ScheduleExpression: "cron(0 0 * * ? *)" 96 | State: "ENABLED" 97 | Targets: 98 | - 99 | Arn: 100 | Fn::GetAtt: 101 | - "LambdaFunction" 102 | - "Arn" 103 | Id: "TargetFunctionV1" 104 | PermissionForEventsToInvokeLambda: 105 | Type: AWS::Lambda::Permission 106 | Properties: 107 | FunctionName: !Ref "LambdaFunction" 108 | Action: "lambda:InvokeFunction" 109 | Principal: "events.amazonaws.com" 110 | SourceArn: 111 | Fn::GetAtt: 112 | - "ScheduledRule" 113 | - "Arn" 114 | LambdaInvoke: 115 | Type: AWS::CloudFormation::CustomResource 116 | Version: "1.0" 117 | Properties: 118 | ServiceToken: !GetAtt LambdaFunction.Arn 119 | LambdaFunction: 120 | Type: AWS::Lambda::Function 121 | Properties: 122 | Role: !GetAtt LambdaExecutionRole.Arn 123 | Runtime: nodejs16.x 124 | Handler: index.handler 125 | Timeout: 60 126 | Description: Used to fetch data from the abuse.ch rule list and update the associated RuleGroup 127 | Tags: 128 | - 129 | Key: "ProjectName" 130 | Value: "AbuseCHFiltering" 131 | Code: 132 | ZipFile: !Sub | 133 | var AWS = require("aws-sdk"); 134 | var response = require('cfn-response'); 135 | var https = require("https"); 136 | var listOfRules = []; 137 | var listOfRules2 = []; 138 | var ruleHeader = []; 139 | const url = "https://sslbl.abuse.ch/blacklist/sslblacklist_tls_cert.rules"; 140 | 141 | const networkfirewall = new AWS.NetworkFirewall(); 142 | 143 | function fetchRules() { 144 | console.log("Fetching the list of rules..."); 145 | return new Promise((resolve, reject) => { 146 | let dataString = ''; 147 | let post_req = https.request(url, (res) => { 148 | res.setEncoding("utf8"); 149 | res.on('data', chunk => { 150 | dataString += chunk; 151 | }); 152 | res.on('end', () => { 153 | listOfRules = dataString.split(/\r?\n/); 154 | console.log("Fetched rules..."); 155 | resolve(); 156 | }); 157 | res.on('error', (err) => { 158 | reject(err); 159 | }); 160 | }); 161 | post_req.end(); 162 | }); 163 | } 164 | 165 | let updateRules = async function (ruleGroup,newRules,part) { 166 | let params = ruleGroup; 167 | delete params.Capacity; 168 | params.RuleGroupName = params.RuleGroupResponse.RuleGroupName; 169 | params.Description = params.RuleGroupResponse.Description; 170 | params.Type = params.RuleGroupResponse.Type; 171 | delete params.RuleGroupResponse; 172 | let rulesString = "# Last autofetched by Lambda: " + new Date().toUTCString() + "\n# Part " + part + " of 2\n"; 173 | rulesString += ruleHeader.join("\n") + "\n"; 174 | rulesString += newRules.join("\n"); 175 | params.RuleGroup.RulesSource.RulesString = rulesString; 176 | 177 | console.log("Updating rules..."); 178 | let res = await networkfirewall.updateRuleGroup(params).promise(); 179 | if (res) { 180 | console.log("Updated '" + params.RuleGroupName + "'."); 181 | } else { 182 | console.log("Error updating the rules for '" + params.RuleGroupName + "'..."); 183 | } 184 | return; 185 | }; 186 | 187 | let createRules = async function (action) { 188 | if (listOfRules.length == 0) { 189 | await fetchRules(); 190 | } else { 191 | console.log("Using recently fetched list of rules..."); 192 | } 193 | 194 | if (!ruleHeader.length) { 195 | ruleHeader = listOfRules.filter(line => line.substring(0,1) == "#"); 196 | ruleHeader.splice(ruleHeader.length-2, ruleHeader.length-1); 197 | } 198 | 199 | if (action == 'drop') listOfRules = listOfRules.map(rule => rule.replace("alert ", "drop ").replace(" detected", " dropped")); 200 | listOfRules = listOfRules.slice(ruleHeader.length+1,listOfRules.length-1); //strip header and footer 201 | listOfRules2 = listOfRules.splice(Math.ceil(listOfRules.length / 2)); 202 | 203 | return; 204 | }; 205 | 206 | exports.handler = async (event, context) => { 207 | if (event.RequestType == "Delete") { 208 | await response.send(event, context, "SUCCESS"); 209 | return; 210 | } 211 | 212 | var rg1 = {Type: "STATEFUL", RuleGroupArn: '${AbuseRG1.RuleGroupArn}'}; 213 | var rg2 = {Type: "STATEFUL", RuleGroupArn: '${AbuseRG2.RuleGroupArn}'}; 214 | 215 | await createRules('${AbuseRuleGroupAction}'); 216 | 217 | console.log("Searching Rule Groups for " + rg1.RuleGroupArn + "..."); 218 | let res = await networkfirewall.describeRuleGroup(rg1).promise(); 219 | if (res.RuleGroupResponse) { 220 | console.log("Found matching Rule Group..."); 221 | await updateRules(res,listOfRules,"1"); 222 | } else { 223 | console.log("ERROR: No matching Rule Group found..."); 224 | } 225 | 226 | console.log("Searching Rule Groups for " + rg2.RuleGroupArn + "..."); 227 | res = await networkfirewall.describeRuleGroup(rg2).promise(); 228 | if (res.RuleGroupResponse) { 229 | console.log("Found matching Rule Group..."); 230 | await updateRules(res,listOfRules2,"2"); 231 | if (event.ResponseURL) await response.send(event, context, response.SUCCESS); 232 | } else { 233 | console.log("ERROR: No matching Rule Group found..."); 234 | if (event.ResponseURL) await response.send(event, context, response.FAILED); 235 | } 236 | return; 237 | }; 238 | -------------------------------------------------------------------------------- /Abuse.ch/cfn-templates/AbuseCHJA3.yaml: -------------------------------------------------------------------------------- 1 | Parameters: 2 | AbuseJA3RG1Name: 3 | Description: "The name of the first Rule Group created to hold the rules" 4 | Type: String 5 | Default: 'AutoUpdating-AbuseCH-JA3' 6 | AbuseRuleGroupAction: 7 | Type: String 8 | Description: "Used to define the action to take on a matching rule if found" 9 | Default : 'drop' 10 | AllowedValues: 11 | - 'alert' 12 | - 'drop' 13 | 14 | Resources: 15 | AbuseRG1: 16 | Type: 'AWS::NetworkFirewall::RuleGroup' 17 | Properties: 18 | RuleGroupName: !Sub "${AWS::StackName}-${AbuseJA3RG1Name}-${AbuseRuleGroupAction}" 19 | Type: STATEFUL 20 | RuleGroup: 21 | RulesSource: 22 | RulesString: '#This will be updated via the Lambda function' 23 | Capacity: 500 24 | Description: >- 25 | Used to track a list of Suricata rules from https://sslbl.abuse.ch/blacklist/ja3_fingerprints.rules. It is updated daily by the CloudWatch Event: AbuseCHJA3DailyTrigger. 26 | Tags: 27 | - 28 | Key: "ProjectName" 29 | Value: "AbuseCHJA3Filtering" 30 | LambdaExecutionRole: 31 | Type: AWS::IAM::Role 32 | Properties: 33 | AssumeRolePolicyDocument: 34 | Version: '2012-10-17' 35 | Statement: 36 | - Effect: Allow 37 | Principal: 38 | Service: 39 | - lambda.amazonaws.com 40 | Action: 41 | - sts:AssumeRole 42 | Path: "/" 43 | Policies: 44 | - PolicyName: LambdaLogs 45 | PolicyDocument: 46 | Version: '2012-10-17' 47 | Statement: 48 | - Effect: Allow 49 | Action: 50 | - 'logs:CreateLogStream' 51 | - 'logs:PutLogEvents' 52 | Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*' 53 | - PolicyName: NetworkFirewall 54 | PolicyDocument: 55 | Version: '2012-10-17' 56 | Statement: 57 | - Effect: Allow 58 | Action: 59 | - 'network-firewall:*' 60 | Resource: 61 | - !GetAtt AbuseRG1.RuleGroupArn 62 | Tags: 63 | - 64 | Key: "ProjectName" 65 | Value: "AbuseCHJA3Filtering" 66 | LambdaLogGroup: 67 | Type: AWS::Logs::LogGroup 68 | Properties: 69 | LogGroupName: !Sub "/aws/lambda/${LambdaFunction}" 70 | DeletionPolicy: Retain 71 | ScheduledRule: 72 | Type: AWS::Events::Rule 73 | Properties: 74 | Description: "AbuseCHJA3DailyTrigger" 75 | ScheduleExpression: "cron(0 0 * * ? *)" 76 | State: "ENABLED" 77 | Targets: 78 | - 79 | Arn: 80 | Fn::GetAtt: 81 | - "LambdaFunction" 82 | - "Arn" 83 | Id: "TargetFunctionV1" 84 | PermissionForEventsToInvokeLambda: 85 | Type: AWS::Lambda::Permission 86 | Properties: 87 | FunctionName: !Ref "LambdaFunction" 88 | Action: "lambda:InvokeFunction" 89 | Principal: "events.amazonaws.com" 90 | SourceArn: 91 | Fn::GetAtt: 92 | - "ScheduledRule" 93 | - "Arn" 94 | LambdaInvoke: 95 | Type: AWS::CloudFormation::CustomResource 96 | Version: "1.0" 97 | Properties: 98 | ServiceToken: !GetAtt LambdaFunction.Arn 99 | LambdaFunction: 100 | Type: AWS::Lambda::Function 101 | Properties: 102 | Role: !GetAtt LambdaExecutionRole.Arn 103 | Runtime: nodejs16.x 104 | Handler: index.handler 105 | Timeout: 60 106 | Description: Used to fetch data from the abuse.ch rule list and update the associated RuleGroup 107 | Tags: 108 | - 109 | Key: "ProjectName" 110 | Value: "AbuseCHJA3Filtering" 111 | Code: 112 | ZipFile: !Sub | 113 | var AWS = require("aws-sdk"); 114 | var response = require('cfn-response'); 115 | var https = require("https"); 116 | var listOfRules = []; 117 | 118 | const url = "https://sslbl.abuse.ch/blacklist/ja3_fingerprints.rules"; 119 | 120 | const networkfirewall = new AWS.NetworkFirewall(); 121 | 122 | function fetchRules() { 123 | console.log("Fetching the list of rules..."); 124 | return new Promise((resolve, reject) => { 125 | let dataString = ''; 126 | let post_req = https.request(url, (res) => { 127 | res.setEncoding("utf8"); 128 | res.on('data', chunk => { 129 | dataString += chunk; 130 | }); 131 | res.on('end', () => { 132 | listOfRules = dataString.split(/\r?\n/); 133 | console.log("Fetched rules..."); 134 | resolve(); 135 | }); 136 | res.on('error', (err) => { 137 | reject(err); 138 | }); 139 | }); 140 | post_req.end(); 141 | }); 142 | } 143 | 144 | let updateRules = async function (ruleGroup,newRules) { 145 | let params = ruleGroup; 146 | params.RuleGroupName = params.RuleGroupResponse.RuleGroupName; 147 | params.Description = params.RuleGroupResponse.Description; 148 | params.Type = params.RuleGroupResponse.Type; 149 | delete params.RuleGroupResponse; 150 | delete params.Capacity; 151 | let rulesString = "# Last autofetched by Lambda: " + new Date().toUTCString() + "\n"; 152 | rulesString += newRules.join("\n"); 153 | params.RuleGroup.RulesSource.RulesString = rulesString; 154 | 155 | console.log("Updating rules..."); 156 | let res = await networkfirewall.updateRuleGroup(params).promise(); 157 | if (res) { 158 | console.log("Updated '" + params.RuleGroupName + "'."); 159 | } else { 160 | console.log("Error updating the rules for '" + params.RuleGroupName + "'..."); 161 | } 162 | return; 163 | }; 164 | 165 | let createRules = async function (action) { 166 | if (listOfRules.length == 0) { 167 | await fetchRules(); 168 | } else { 169 | console.log("Using recently fetched list of rules..."); 170 | } 171 | 172 | if (action == 'drop') listOfRules = listOfRules.map(rule => rule.replace("alert ", "drop ").replace(" detected", " dropped")); 173 | 174 | return; 175 | }; 176 | 177 | exports.handler = async (event, context) => { 178 | if (event.RequestType == "Delete") { 179 | await response.send(event, context, "SUCCESS"); 180 | return; 181 | } 182 | 183 | var rg1 = {Type: "STATEFUL", RuleGroupArn: '${AbuseRG1.RuleGroupArn}'}; 184 | 185 | await createRules('${AbuseRuleGroupAction}'); 186 | 187 | console.log("Searching Rule Groups for " + rg1.RuleGroupArn + "..."); 188 | res = await networkfirewall.describeRuleGroup(rg1).promise(); 189 | if (res.RuleGroupResponse) { 190 | console.log("Found matching Rule Group..."); 191 | await updateRules(res,listOfRules); 192 | if (event.ResponseURL) await response.send(event, context, response.SUCCESS); 193 | } else { 194 | console.log("ERROR: No matching Rule Group found..."); 195 | if (event.ResponseURL) await response.send(event, context, response.FAILED); 196 | } 197 | return; 198 | }; 199 | -------------------------------------------------------------------------------- /Abuse.ch/src/ANFAbuseHostfile.js: -------------------------------------------------------------------------------- 1 | var AWS = require("aws-sdk"); 2 | const https = require("https"); 3 | 4 | const hostfileUrl = "https://urlhaus.abuse.ch/downloads/hostfile/"; 5 | 6 | const nf = new AWS.NetworkFirewall(); 7 | 8 | async function getDomains (){ 9 | var listOfDomains = []; 10 | console.log("Fetching the list of domains from " + hostfileUrl); 11 | return new Promise((resolve, reject) => { 12 | let dataString = ''; 13 | let post_req = https.request(hostfileUrl, (res) => { 14 | res.setEncoding("utf8"); 15 | res.on('data', chunk => { 16 | dataString += chunk; 17 | }); 18 | res.on('end', () => { 19 | //console.log(dataString); 20 | listOfDomains = dataString 21 | .split(/\r?\n/) 22 | .filter((line) => line.match(/^\d+/)) 23 | .map((line)=> {return line.replace(/127.0.0.1\t/,'').toLowerCase()}) 24 | .filter((line) => line.match(/^(?!:\/\/)(?=.{1,255}$)((.{1,63}\.){1,127}(?![0-9]*$)[a-z0-9-]+\.?)$/)); 25 | console.log("Fetched " + listOfDomains.length + " Domains"); 26 | resolve(listOfDomains); 27 | }); 28 | res.on('error', (err) => { 29 | reject(err); 30 | }); 31 | }); 32 | post_req.end(); 33 | }); 34 | } 35 | 36 | async function updateRuleGroup(arn, domains){ 37 | let params = {Type: "STATEFUL", RuleGroupArn: arn}; 38 | let res = await nf.describeRuleGroup(params).promise(); 39 | if (res.RuleGroupResponse) { 40 | console.log("Found destination rulegroup"); 41 | res.RuleGroup.RulesSource.RulesSourceList.Targets = domains; 42 | res.RuleGroupName = res.RuleGroupResponse.RuleGroupName; 43 | res.Description = "Last updated: " + new Date().toUTCString() + " -- The CloudWatch Events Rule: AbuseCHHostfileRulegroupHourlyTrigger, triggers a daily update of this list."; 44 | res.Type = res.RuleGroupResponse.Type; 45 | delete res.Capacity; 46 | delete res.RuleGroupResponse; 47 | 48 | console.log("Updating rules"); 49 | let result = await nf.updateRuleGroup(res).promise(); 50 | if (result) { 51 | console.log("Updated '" + res.RuleGroupName); 52 | } else { 53 | console.log("Error updating '" + res.RuleGroupName + "'..."); 54 | } 55 | } else { 56 | console.log("No matching Rule Group found"); 57 | } 58 | return; 59 | } 60 | 61 | exports.handler = async (event, context) => { 62 | 63 | let sourceArn = ''; 64 | let domains = await getDomains(sourceArn); 65 | 66 | if (domains) { 67 | console.log("Using a list of: " + domains.length + " domains"); 68 | 69 | await updateRuleGroup(sourceArn, domains); 70 | } else { 71 | console.log("Error fetching a list of domains from: ", sourceArn); 72 | } 73 | 74 | return; 75 | }; 76 | -------------------------------------------------------------------------------- /Abuse.ch/src/AbuseCH.js: -------------------------------------------------------------------------------- 1 | var AWS = require("aws-sdk"); 2 | var https = require("https"); 3 | var listOfRules = []; 4 | var listOfRules2 = []; 5 | var ruleHeader = []; 6 | const url = "https://sslbl.abuse.ch/blacklist/sslblacklist_tls_cert.rules"; 7 | 8 | const networkfirewall = new AWS.NetworkFirewall(); 9 | 10 | function fetchRules() { 11 | console.log("Fetching the list of rules..."); 12 | return new Promise((resolve, reject) => { 13 | let dataString = ''; 14 | let post_req = https.request(url, (res) => { 15 | res.setEncoding("utf8"); 16 | res.on('data', chunk => { 17 | dataString += chunk; 18 | }); 19 | res.on('end', () => { 20 | listOfRules = dataString.split(/\r?\n/); 21 | console.log("Fetched rules..."); 22 | resolve(); 23 | }); 24 | res.on('error', (err) => { 25 | reject(err); 26 | }); 27 | }); 28 | post_req.end(); 29 | }); 30 | } 31 | 32 | let updateRules = async function (ruleGroup,newRules,part) { 33 | let params = ruleGroup; 34 | delete params.Capacity; 35 | params.RuleGroupName = params.RuleGroupResponse.RuleGroupName; 36 | params.Description = params.RuleGroupResponse.Description; 37 | params.Type = params.RuleGroupResponse.Type; 38 | delete params.RuleGroupResponse; 39 | let rulesString = "# Last autofetched by Lambda: " + new Date().toUTCString() + "\n# Part " + part + " of 2\n"; 40 | rulesString += ruleHeader.join("\n") + "\n"; 41 | rulesString += newRules.join("\n"); 42 | params.RuleGroup.RulesSource.RulesString = rulesString; 43 | 44 | console.log("Updating rules..."); 45 | let res = await networkfirewall.updateRuleGroup(params).promise(); 46 | if (res) { 47 | console.log("Updated '" + params.RuleGroupName + "'."); 48 | } else { 49 | console.log("Error updating the rules for '" + params.RuleGroupName + "'..."); 50 | } 51 | return; 52 | }; 53 | 54 | let createRules = async function (action) { 55 | if (listOfRules.length == 0) { 56 | await fetchRules(); 57 | } else { 58 | console.log("Using recently fetched list of rules..."); 59 | } 60 | 61 | if (!ruleHeader.length) { 62 | ruleHeader = listOfRules.filter(line => line.substring(0,1) == "#"); 63 | ruleHeader.splice(ruleHeader.length-2, ruleHeader.length-1); 64 | } 65 | 66 | if (action == 'drop') listOfRules = listOfRules.map(rule => rule.replace("alert ", "drop ").replace(" detected", " dropped")); 67 | listOfRules = listOfRules.slice(ruleHeader.length+1,listOfRules.length-1); //strip header and footer 68 | listOfRules2 = listOfRules.splice(Math.ceil(listOfRules.length / 2)); 69 | 70 | return; 71 | }; 72 | 73 | exports.handler = async (event, context) => { 74 | 75 | var rg1 = {Type: "STATEFUL", RuleGroupArn: ''}; 76 | var rg2 = {Type: "STATEFUL", RuleGroupArn: ''}; 77 | 78 | await createRules('drop'); 79 | 80 | console.log("Searching Rule Groups for " + rg1.RuleGroupArn + "..."); 81 | let res = await networkfirewall.describeRuleGroup(rg1).promise(); 82 | if (res.RuleGroupResponse) { 83 | console.log("Found matching Rule Group..."); 84 | await updateRules(res,listOfRules,"1"); 85 | } else { 86 | console.log("ERROR: No matching Rule Group found..."); 87 | } 88 | 89 | console.log("Searching Rule Groups for " + rg2.RuleGroupArn + "..."); 90 | res = await networkfirewall.describeRuleGroup(rg2).promise(); 91 | if (res.RuleGroupResponse) { 92 | console.log("Found matching Rule Group..."); 93 | await updateRules(res,listOfRules2,"2"); 94 | } else { 95 | console.log("ERROR: No matching Rule Group found..."); 96 | } 97 | return; 98 | }; 99 | -------------------------------------------------------------------------------- /Abuse.ch/src/AbuseCHJA3.js: -------------------------------------------------------------------------------- 1 | var AWS = require("aws-sdk"); 2 | var https = require("https"); 3 | var listOfRules = []; 4 | 5 | const url = "https://sslbl.abuse.ch/blacklist/ja3_fingerprints.rules"; 6 | 7 | const networkfirewall = new AWS.NetworkFirewall(); 8 | 9 | function fetchRules() { 10 | console.log("Fetching the list of rules..."); 11 | return new Promise((resolve, reject) => { 12 | let dataString = ''; 13 | let post_req = https.request(url, (res) => { 14 | res.setEncoding("utf8"); 15 | res.on('data', chunk => { 16 | dataString += chunk; 17 | }); 18 | res.on('end', () => { 19 | listOfRules = dataString.split(/\r?\n/); 20 | console.log("Fetched rules..."); 21 | resolve(); 22 | }); 23 | res.on('error', (err) => { 24 | reject(err); 25 | }); 26 | }); 27 | post_req.end(); 28 | }); 29 | } 30 | 31 | let updateRules = async function (ruleGroup,newRules) { 32 | let params = ruleGroup; 33 | params.RuleGroupName = params.RuleGroupResponse.RuleGroupName; 34 | params.Description = params.RuleGroupResponse.Description; 35 | params.Type = params.RuleGroupResponse.Type; 36 | delete params.RuleGroupResponse; 37 | delete params.Capacity; 38 | let rulesString = "# Last autofetched by Lambda: " + new Date().toUTCString() + "\n"; 39 | rulesString += newRules.join("\n"); 40 | params.RuleGroup.RulesSource.RulesString = rulesString; 41 | 42 | console.log("Updating rules..."); 43 | let res = await networkfirewall.updateRuleGroup(params).promise(); 44 | if (res) { 45 | console.log("Updated '" + params.RuleGroupName + "'."); 46 | } else { 47 | console.log("Error updating the rules for '" + params.RuleGroupName + "'..."); 48 | } 49 | return; 50 | }; 51 | 52 | let createRules = async function (action) { 53 | if (listOfRules.length == 0) { 54 | await fetchRules(); 55 | } else { 56 | console.log("Using recently fetched list of rules..."); 57 | } 58 | 59 | if (action == 'drop') listOfRules = listOfRules.map(rule => rule.replace("alert ", "drop ").replace(" detected", " dropped")); 60 | 61 | return; 62 | }; 63 | 64 | exports.handler = async (event, context) => { 65 | 66 | var rg1 = {Type: "STATEFUL", RuleGroupArn: ''}; 67 | 68 | await createRules('drop'); 69 | 70 | console.log("Searching Rule Groups for " + rg1.RuleGroupArn + "..."); 71 | res = await networkfirewall.describeRuleGroup(rg1).promise(); 72 | if (res.RuleGroupResponse) { 73 | console.log("Found matching Rule Group..."); 74 | await updateRules(res,listOfRules); 75 | } else { 76 | console.log("ERROR: No matching Rule Group found..."); 77 | } 78 | return; 79 | }; 80 | -------------------------------------------------------------------------------- /AllowListGenerator/README.md: -------------------------------------------------------------------------------- 1 | # AWS Network Firewall Allow List Automation 2 | 3 | ## NATIVE FEATURE LAUNCH 4 | NOTE: This feature is now natively apart of AWS Network Firewall. 5 | - https://aws.amazon.com/about-aws/whats-new/2025/02/aws-network-firewall-automated-domain-lists/ 6 | - https://aws.amazon.com/blogs/security/from-log-analysis-to-rule-creation-how-aws-network-firewall-automates-domain-based-security-for-outbound-traffic/ 7 | 8 | 9 | This repository contains an AWS CloudFormation templates that help automate the allow list creation process for AWS Network Firewall based on network traffic logs. The solution analyzes the Network Firewall alert logs in Amazon S3, or CloudWatch logs, identifies the Server Name Indication (SNI) values associated with TLS traffic + the hostname associated with HTTP traffic and generates the corresponding allow rules in Suricata format. 10 | 11 | This solution is intended to help with building an allow list-based architecture for controlling outbound HTTP/TLS traffic from your workloads. It is not a fully automated solution, but rather a tool to surface the domains your workloads are reaching via HTTP/TLS, which can then be used to build out allow list rules. While this solution does not provide a fully automated allow list configuration, it aims to simplify the process of building and maintaining an allow list by providing visibility into the domains being accessed and generating rule recommendations based on the observed traffic patterns. 12 | 13 | - If you store your AWS Network Firewall alert logs in Amazon S3, select the folder above named **s3-logs**. 14 | - If you store your AWS Network Firewall alert logs in Amazon CloudWatch logs, select the folder above named **cloudwatch-logs**. 15 | 16 | -------------------------------------------------------------------------------- /AllowListGenerator/cloudwatch-logs/README.md: -------------------------------------------------------------------------------- 1 | # AWS Network Firewall Allow List Automation 2 | 3 | This repository contains an AWS CloudFormation template that helps automate the allow list creation process for AWS Network Firewall based on network traffic logs. The solution analyzes the Network Firewall alert logs in Amazon CloudWatch Logs, identifies the Server Name Indication (SNI) values associated with TLS traffic + the hostname associated with HTTP traffic and generates the corresponding allow rules in Suricata format. 4 | 5 | This solution is intended to help with building an allow list-based architecture for controlling outbound HTTP/TLS traffic from your workloads. It is not a fully automated solution, but rather a tool to surface the domains your workloads are reaching via HTTP/TLS, which can then be used to build out allow list rules. While this solution does not provide a fully automated allow list configuration, it aims to simplify the process of building and maintaining an allow list by providing visibility into the domains being accessed and generating rule recommendations based on the observed traffic patterns. 6 | 7 | ## Architecture 8 | 9 | ![Architecture Diagram](./images/arch-diagram.png) 10 | 11 | 12 | 13 | ## Overview 14 | The CloudFormation template creates the following resources: 15 | 16 | 1. **Amazon CloudWatch Log Subscription Filter**: Sends Network Firewall alert logs to the Lambda function for processing. 17 | 2. **Amazon CloudWatch Metrics**: Publishes CloudWatch metrics for each domain, protocol, and firewall combination. This allows you to monitor and visualize the network traffic patterns for your workloads. 18 | 3. **Amazon EventBridge Rule**: Triggers the Lambda function periodically (every hour by default) to update the Network Firewall rule group with the latest allow list rules. 19 | 4. **AWS Lambda Function**: Processes the Network Firewall alert logs and updates the DynamoDB table + CloudWatch metric, as well as generates the Suricata allow list rules. 20 | 5. **Amazon DynamoDB Table**: Stores the domains and associated metrics (unique source IP addresses + EC2 instance IDs requesting domain and total number of requests to domain) from the Network Firewall logs. 21 | 6. **AWS Network Firewall Rule Group**: Stores the generated allow list rules based on the domain values and metrics. Rules are ordered by number of requests to each domain, with the most requested domains appearing at the top of each rule group section. This rule group consists of four different sections: 22 | - **TLS Wildcard Rules**: This section contains wildcard TLS rules for domains that have reached the configured threshold of subdomains (defined by the WildcardDomainMinimum parameter). 23 | - **HTTP Wildcard Rules**: This section contains wildcard HTTP rules for domains that have reached the configured threshold of subdomains (defined by the WildcardDomainMinimum parameter). 24 | - **TLS Specific Rules**: This section contains TLS rules for every specific domains that has been reached. 25 | - **HTTP Specific Rules**: This section contains HTTP rules for each specific domains that has been reached. 26 | 27 | **Note:** This solution does not automatically attach the created rule groups to a Network Firewall policy. It is up to the user to decide how they want to use the rule groups generated by this solution. The user is responsible for attaching the relevant rule groups to their Network Firewall policies as needed. 28 | 29 | ## Before You Begin 30 | 31 | The provided CloudFormation template makes the following three assumptions: 32 | 33 | 1. You have already configured an AWS Network Firewall in your VPC 34 | 2. Your AWS Network Firewall has been configured to publish firewall alert logs to a CloudWatch log group 35 | 3. Your AWS Network Firewall has the stateful rule group default action of "Alert Established" 36 | 37 | 38 | If you have not deployed AWS Network Firewall in your VPC, you can use one of the available [AWS Network Firewall Deployment Architecture](https://github.com/aws-samples/aws-networkfirewall-cfn-templates) templates to create a firewall. Once created, configure a [CloudWatch log group](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/Working-with-log-groups-and-streams.html) for the firewall alert logs, and configure your AWS Network Firewall to use the "Alert Established" default action mentioned above. 39 | 40 | 41 | ## Setup 42 | 43 | 1. Deploy the CloudFormation stack in your AWS account using the provided template. 44 | 2. Specify the required parameters during the stack deployment: 45 | - `NetworkFirewallLogGroupName`: The name of the CloudWatch Logs log group containing your Network Firewall logs. 46 | - `RuleGroupCapacity`: The maximum number of rules allowed in the Network Firewall rule group (default: 1000). 47 | - `RuleSidPrefix`: The prefix for the rule SIDs to ensure uniqueness across rule groups (default: 1). 48 | - `RuleGroupName`: The name of the Network Firewall rule group (default: "StrictAllowListRuleGroup"). 49 | - `CloudWatchMetricsNamespace`: The namespace for the CloudWatch metrics. 50 | - `RateInMinutes`: The rate (in minutes) at which the Lambda function will be invoked to update the Network Firewall rule group. This value must be greater than 1. (Default is every 60 minutes). 51 | - `AlertMessage`: The message used in the alert rules (default: "Allow-Listed-Domain"). 52 | - `WildcardDomainMinimum`: Minimum number of subdomains reached before a wildcard rule is added to allow all subdomains for the corresponding domain (default: 15). For example, if this value is set to 15, once 15 different subdomains under the same domain are reached, a rule will be added to the WildcardAllowListRuleGroup to allow the entire domain and any subdomains. 53 | 3. After successful deployment, the stack will start processing the Network Firewall logs and updating the DynamoDB table with domain values and metrics. 54 | 4. The EventBridge rule will trigger the Lambda function periodically (every hour by default) to update the Network Firewall rule group with the latest allow list rules based on the domain values and metrics in the DynamoDB table. 55 | 56 | ## Intended Usage 57 | This solution is designed to help identify the domains that your workloads are reaching out to, and assist in building out the allow list rules for those domains. It does not automatically enforce or apply the generated rules; instead, it simply adds any domain your workloads request to the allow list rule group. **It is up to you as the user to review the generated rules and decide which domains you actually want to allow access to.** 58 | 59 | 60 | ## Example 61 | To help better understand the solution, let's say this stack was deployed with the `WildcardDomainMinimum` set to 3. This means if my workloads reach 3 or more subdomains associated with the same domain via the same protocol (TLS or HTTP), that the entire domain will be added to the wildcard rule group section for that protocol. 62 | 63 | In this example, the entire `amazon.com` is added to the TLS wildcard rule group section since 3 subdomains associated with that domain have been reached via TLS. (HTTP wildcard section functions the same way) 64 | 65 | **Note:** In the CloudWatch metrics screenshot, I requested all the domains at the same time. For normal traffic patterns, these points would be more scatterred around the graph at the specific time the domains were requested. 66 | 67 | ### DynamoDB Table 68 | 69 | ![DynamoDB Table Example](./images/dynamodb-table-example.png) 70 | 71 | 72 | ### Allow List Rule Group 73 | 74 | ![Network Firewall Rule Group Example](./images/nfw-rule-group-example.png) 75 | 76 | ### CloudWatch Metrics 77 | 78 | ![CloudWatch Metrics Example](./images/cloudwatch-metrics-example.png) 79 | 80 | 81 | ## Why Use This Solution? 82 | 83 | This solution addresses several challenges and provides benefits in managing network traffic and security posture: 84 | 85 | - **Reduce Attack Surface**: Domain-based allow lists with a default deny can offer significant risk mitigation. Rather than try to maintain a constantly updated list of all current and future potential threats to detect and block, it can be much simpler to limit application workloads to only communicate with the trusted domains that are necessary for their operation. Also, domain names (unlike IP addresses) change infrequently. 86 | 87 | - **Visibility into Network Traffic**: By analyzing the Network Firewall logs, this solution provides valuable insights into the network traffic patterns, including the domains and subdomains being accessed, the timestamps of when they're being accessed, the information on which clients are accessing each domain, and the total requests to each domain. 88 | 89 | - **Lightweight and Non-Disruptive**: This solution does not require deploying additional Network Firewall resources or making changes to your existing Network Firewall configuration (besides ensuring 'Alert Established' is enabled and storing alert logs in a CloudWatch logs group). It seamlessly integrates with your existing Network Firewall deployment, leveraging the logs generated by the firewall to create the allow list rules. 90 | 91 | - **Wildcard Domain Support**: The solution includes a support for domains with a large number of subdomains (configurable threshold), allowing for more efficient and manageable rule sets. 92 | 93 | ## Limitations 94 | 95 | - The maximum capacity for the Network Firewall rule group is 30,000 rules. However, due to the size of rule strings, this template has a lower default maximum capacity of 1,000 rules. This can be changed by editing the CloudFormation `RuleGroupCapacity` parameter. 96 | - DynamoDB has a limit of 400 KB for the size of an item. This solution stores the list of source IP addresses and associated instance IDs for each SNI value in DynamoDB attributes. Assuming an average IP address string length of 15 characters (e.g., "192.168.1.100") and an average instance ID string length of 20 characters (e.g., "i-0c7265daddafaf4d9"), and considering the space required for other attributes, it's estimated that this solution can support approximately 15,000 unique source IP addresses and associated instance IDs per SNI value. If you anticipate a larger number of unique source IP addresses and instance IDs for a single SNI value, you may need to modify the solution to handle this case. 97 | 98 | ## Pricing Consideration 99 | 100 | This solution assumes that you have already deployed AWS Network Firewall and configured it to publish firewall alert logs to a CloudWatch log group. The pricing for these services are not covered here. 101 | 102 | The additional services introduced by this solution and their pricing models are: 103 | 104 | - **AWS Lambda**: Refer to the [Lambda pricing](https://aws.amazon.com/lambda/pricing/) page for more details. 105 | - **Amazon DynamoDB**: Refer to the [DynamoDB pricing](https://aws.amazon.com/dynamodb/pricing/) page for more details. 106 | - **Amazon EventBridge**: Refer to the [EventBridge pricing](https://aws.amazon.com/eventbridge/pricing/) page for more details. 107 | - **Amazon CloudWatch**: Refer to the [CloudWatch pricing](https://aws.amazon.com/cloudwatch/pricing/) page for more details. 108 | 109 | It's recommended to estimate the expected costs based on your specific usage patterns and requirements before deploying this solution. 110 | 111 | ## Cleanup 112 | 113 | To remove the resources created by this template, delete the CloudFormation stack from the AWS Management Console or using the AWS CLI. 114 | 115 | ## Authors 116 | 117 | |Name | Title| 118 | |------|------| 119 | |Bryan Van Hook | Solutions Architect III| 120 | |Lawton Pittenger | Solutions Architect I| 121 | 122 | ## Significant Contributors 123 | |Name | Title| 124 | |------|------| 125 | |Jesse Lepich | Sr WW Security Specialist SA| 126 | |Michael Leighty | Sr WW Security Specialist SA| 127 | 128 | ## License 129 | 130 | This project is licensed under the Apache-2.0 License. -------------------------------------------------------------------------------- /AllowListGenerator/cloudwatch-logs/images/arch-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-network-firewall-automation-examples/ffb519a62508d00886deb839a07e70f628eccbb8/AllowListGenerator/cloudwatch-logs/images/arch-diagram.png -------------------------------------------------------------------------------- /AllowListGenerator/cloudwatch-logs/images/cloudwatch-metrics-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-network-firewall-automation-examples/ffb519a62508d00886deb839a07e70f628eccbb8/AllowListGenerator/cloudwatch-logs/images/cloudwatch-metrics-example.png -------------------------------------------------------------------------------- /AllowListGenerator/cloudwatch-logs/images/dynamodb-table-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-network-firewall-automation-examples/ffb519a62508d00886deb839a07e70f628eccbb8/AllowListGenerator/cloudwatch-logs/images/dynamodb-table-example.png -------------------------------------------------------------------------------- /AllowListGenerator/cloudwatch-logs/images/nfw-rule-group-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-network-firewall-automation-examples/ffb519a62508d00886deb839a07e70f628eccbb8/AllowListGenerator/cloudwatch-logs/images/nfw-rule-group-example.png -------------------------------------------------------------------------------- /AllowListGenerator/s3-logs/README.md: -------------------------------------------------------------------------------- 1 | # AWS Network Firewall Allow List Automation 2 | 3 | This repository contains an AWS CloudFormation template that helps automate the allow list creation process for AWS Network Firewall based on network traffic logs. The solution analyzes the Network Firewall alert logs in Amazon S3, identifies the Server Name Indication (SNI) values associated with TLS traffic + the hostname associated with HTTP traffic and generates the corresponding allow rules in Suricata format. 4 | 5 | This solution is intended to help with building an allow list-based architecture for controlling outbound HTTP/TLS traffic from your workloads. It is not a fully automated solution, but rather a tool to surface the domains your workloads are reaching via HTTP/TLS, which can then be used to build out allow list rules. While this solution does not provide a fully automated allow list configuration, it aims to simplify the process of building and maintaining an allow list by providing visibility into the domains being accessed and generating rule recommendations based on the observed traffic patterns. 6 | 7 | ## Architecture 8 | 9 | ![Architecture Diagram](./images/arch-diagram.png) 10 | 11 | 12 | 13 | ## Overview 14 | The CloudFormation template creates the following resources: 15 | 16 | 1. **Amazon EventBridge Rule**: Triggers the Lambda function periodically (every hour by default) to update the Network Firewall rule group with the latest allow list rules. 17 | 2. **AWS Lambda Function**: Processes the Network Firewall alert logs and updates the DynamoDB table, as well as generates the Suricata allow list rules. 18 | 3. **Amazon DynamoDB Table**: Stores the domains and associated metrics (unique source IP addresses + EC2 instance IDs requesting domain and total number of requests to domain) from the Network Firewall logs. 19 | 4. **Amazon S3 Bucket**: Stores the AWS Network Firewall rules, to allow for exporting to other accounts / building into existing rule group update automation. (Same contents as what's stored in the AWS Network Firewall Rule Group) 20 | 5. **AWS Network Firewall Rule Group**: Stores the generated allow list rules based on the domain values and metrics. Rules are ordered by number of requests to each domain, with the most requested domains appearing at the top of each rule group section. This rule group consists of four different sections: 21 | - **TLS Wildcard Rules**: This section contains wildcard TLS rules for domains that have reached the configured threshold of subdomains (defined by the WildcardDomainMinimum parameter). 22 | - **HTTP Wildcard Rules**: This section contains wildcard HTTP rules for domains that have reached the configured threshold of subdomains (defined by the WildcardDomainMinimum parameter). 23 | - **TLS Specific Rules**: This section contains TLS rules for every specific domains that has been reached. 24 | - **HTTP Specific Rules**: This section contains HTTP rules for each specific domains that has been reached. 25 | 26 | **Note:** This solution does not automatically attach the created rule groups to a Network Firewall policy. It is up to the user to decide how they want to use the rule groups generated by this solution. The user is responsible for attaching the relevant rule groups to their Network Firewall policies as needed. 27 | 28 | ## Before You Begin 29 | 30 | The provided CloudFormation template makes the following three assumptions: 31 | 32 | 1. You have already configured an AWS Network Firewall in your VPC 33 | 2. Your AWS Network Firewall has been configured to publish firewall alert logs to an Amazon S3 bucket 34 | 3. Your AWS Network Firewall has the stateful rule group default action of "Alert Established" 35 | 36 | 37 | If you have not deployed AWS Network Firewall in your VPC, you can use one of the available [AWS Network Firewall Deployment Architecture](https://github.com/aws-samples/aws-networkfirewall-cfn-templates) templates to create a firewall. Once created, configure an [Amazon S3 bucket](https://docs.aws.amazon.com/network-firewall/latest/developerguide/logging-s3.html) for the firewall alert logs, and configure your AWS Network Firewall to use the "Alert Established" default action mentioned above. 38 | 39 | 40 | ## Setup 41 | 42 | 1. Deploy the CloudFormation stack in your AWS account using the provided template. 43 | 2. Specify the required parameters during the stack deployment: 44 | - `LogBucket`: The name of the S3 Bucket containing your Network Firewall logs. 45 | - `RuleGroupCapacity`: The maximum number of rules allowed in the Network Firewall rule group (default: 1000). 46 | - `RuleSidPrefix`: The prefix for the rule SIDs to ensure uniqueness across rule groups (default: 1). 47 | - `RuleGroupName`: The name of the Network Firewall rule group (default: "StrictAllowListRuleGroup"). 48 | - `RateInMinutes`: The rate (in minutes) at which the Lambda function will be invoked to update the Network Firewall rule group. This value must be greater than 1. (Default is every 60 minutes). 49 | - `AlertMessage`: The message used in the alert rules (default: "Allow-Listed-Domain"). 50 | - `WildcardDomainMinimum`: Minimum number of subdomains reached before a wildcard rule is added to allow all subdomains for the corresponding domain (default: 15). For example, if this value is set to 15, once 15 different subdomains under the same domain are reached, a rule will be added to the WildcardAllowListRuleGroup to allow the entire domain and any subdomains. 51 | 3. After successful deployment, the stack will start processing the Network Firewall logs and updating the DynamoDB table with domain values and metrics. 52 | 4. The EventBridge rule will trigger the Lambda function periodically (every hour by default) to update the Network Firewall rule group and the rules.txt file stored in S3 with the latest allow list rules based on the domain values and metrics in the DynamoDB table. 53 | 54 | ## Intended Usage 55 | This solution is designed to help identify the domains that your workloads are reaching out to, and assist in building out the allow list rules for those domains. It does not automatically enforce or apply the generated rules; instead, it simply adds any domain your workloads request to the allow list rule group. **It is up to you as the user to review the generated rules and decide which domains you actually want to allow access to.** 56 | 57 | 58 | ## Example 59 | To help better understand the solution, let's say this stack was deployed with the `WildcardDomainMinimum` set to 3. This means if my workloads reach 3 or more subdomains associated with the same domain via the same protocol (TLS or HTTP), that the entire domain will be added to the wildcard rule group section for that protocol. 60 | 61 | In this example, the entire `amazon.com` is added to the TLS wildcard rule group section since 3 subdomains associated with that domain have been reached via TLS. (HTTP wildcard section functions the same way) 62 | 63 | ### DynamoDB Table 64 | 65 | ![DynamoDB Table Example](./images/dynamodb-table-example.png) 66 | 67 | 68 | ### Allow List Rule Group 69 | 70 | ![Network Firewall Rule Group Example](./images/nfw-rule-group-example.png) 71 | 72 | 73 | 74 | ## Why Use This Solution? 75 | 76 | This solution addresses several challenges and provides benefits in managing network traffic and security posture: 77 | 78 | - **Reduce Attack Surface**: Domain-based allow lists with a default deny can offer significant risk mitigation. Rather than try to maintain a constantly updated list of all current and future potential threats to detect and block, it can be much simpler to limit application workloads to only communicate with the trusted domains that are necessary for their operation. Also, domain names (unlike IP addresses) change infrequently. 79 | 80 | - **Visibility into Network Traffic**: By analyzing the Network Firewall logs, this solution provides valuable insights into the network traffic patterns, including the domains and subdomains being accessed, the timestamps of when they're being accessed, the information on which clients are accessing each domain, and the total requests to each domain. 81 | 82 | - **Lightweight and Non-Disruptive**: This solution does not require deploying additional Network Firewall resources or making changes to your existing Network Firewall configuration (besides ensuring 'Alert Established' is enabled and storing alert logs in an Amazon S3 bucket). It seamlessly integrates with your existing Network Firewall deployment, leveraging the logs generated by the firewall to create the allow list rules. 83 | 84 | - **Wildcard Domain Support**: The solution includes a support for domains with a large number of subdomains (configurable threshold), allowing for more efficient and manageable rule sets. 85 | 86 | ## Limitations 87 | 88 | - The maximum capacity for the Network Firewall rule group is 30,000 rules. However, due to the size of rule strings, this template has a lower default maximum capacity of 1,000 rules. This can be changed by editing the CloudFormation `RuleGroupCapacity` parameter. 89 | - DynamoDB has a limit of 400 KB for the size of an item. This solution stores the list of source IP addresses and associated instance IDs for each SNI value in DynamoDB attributes. Assuming an average IP address string length of 15 characters (e.g., "192.168.1.100") and an average instance ID string length of 20 characters (e.g., "i-0c7265daddafaf4d9"), and considering the space required for other attributes, it's estimated that this solution can support approximately 15,000 unique source IP addresses and associated instance IDs per SNI value. If you anticipate a larger number of unique source IP addresses and instance IDs for a single SNI value, you may need to modify the solution to handle this case. 90 | 91 | ## Pricing Consideration 92 | 93 | This solution assumes that you have already deployed AWS Network Firewall and configured it to publish firewall alert logs to an Amazon S3 bucket. The pricing for these services are not covered here. 94 | 95 | The additional services introduced by this solution and their pricing models are: 96 | 97 | - **AWS Lambda**: Refer to the [Lambda pricing](https://aws.amazon.com/lambda/pricing/) page for more details. 98 | - **Amazon DynamoDB**: Refer to the [DynamoDB pricing](https://aws.amazon.com/dynamodb/pricing/) page for more details. 99 | - **Amazon EventBridge**: Refer to the [EventBridge pricing](https://aws.amazon.com/eventbridge/pricing/) page for more details. 100 | 101 | It's recommended to estimate the expected costs based on your specific usage patterns and requirements before deploying this solution. 102 | 103 | ## Cleanup 104 | 105 | To remove the resources created by this template, delete the CloudFormation stack from the AWS Management Console or using the AWS CLI. 106 | 107 | ## Authors 108 | 109 | |Name | Title| 110 | |------|------| 111 | |Bryan Van Hook | Solutions Architect III| 112 | |Lawton Pittenger | Security Specialist Solutions Architect| 113 | 114 | ## Significant Contributors 115 | |Name | Title| 116 | |------|------| 117 | |Jesse Lepich | Sr WW Security Specialist SA| 118 | |Michael Leighty | Sr WW Security Specialist SA| 119 | 120 | ## License 121 | 122 | This project is licensed under the Apache-2.0 License. -------------------------------------------------------------------------------- /AllowListGenerator/s3-logs/images/arch-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-network-firewall-automation-examples/ffb519a62508d00886deb839a07e70f628eccbb8/AllowListGenerator/s3-logs/images/arch-diagram.png -------------------------------------------------------------------------------- /AllowListGenerator/s3-logs/images/dynamodb-table-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-network-firewall-automation-examples/ffb519a62508d00886deb839a07e70f628eccbb8/AllowListGenerator/s3-logs/images/dynamodb-table-example.png -------------------------------------------------------------------------------- /AllowListGenerator/s3-logs/images/nfw-rule-group-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-network-firewall-automation-examples/ffb519a62508d00886deb839a07e70f628eccbb8/AllowListGenerator/s3-logs/images/nfw-rule-group-example.png -------------------------------------------------------------------------------- /Alphasoc/cfn-templates/AlphasocEncryptedDNS.yaml: -------------------------------------------------------------------------------- 1 | Parameters: 2 | RuleGroupName: 3 | Type: String 4 | Default: 'AlphasocEncryptedDNS' 5 | RuleGroupAction: 6 | Type: String 7 | Description: "Used to define the action to take on a matching rule if found" 8 | Default : 'drop' 9 | AllowedValues: 10 | - 'alert' 11 | - 'drop' 12 | 13 | Resources: 14 | StatefulRulegroup: 15 | Type: 'AWS::NetworkFirewall::RuleGroup' 16 | Properties: 17 | RuleGroupName: !Sub "${AWS::StackName}-${RuleGroupName}-${RuleGroupAction}" 18 | Type: STATEFUL 19 | RuleGroup: 20 | RulesSource: 21 | RulesString: '#This will be updated via the Lambda function' 22 | Capacity: 3000 23 | Description: >- 24 | Used to dynamically track a list of threats 25 | Tags: 26 | - Key: "ProjectName" 27 | Value: "AlphasocEncryptedDNSFiltering" 28 | LambdaExecutionRole: 29 | Type: AWS::IAM::Role 30 | Properties: 31 | AssumeRolePolicyDocument: 32 | Version: '2012-10-17' 33 | Statement: 34 | - Effect: Allow 35 | Principal: 36 | Service: 37 | - lambda.amazonaws.com 38 | Action: 39 | - sts:AssumeRole 40 | Path: "/" 41 | Policies: 42 | - PolicyName: LambdaLogs 43 | PolicyDocument: 44 | Version: '2012-10-17' 45 | Statement: 46 | - Effect: Allow 47 | Action: 48 | - 'logs:CreateLogStream' 49 | - 'logs:PutLogEvents' 50 | Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*' 51 | - PolicyName: NetworkFirewall 52 | PolicyDocument: 53 | Version: '2012-10-17' 54 | Statement: 55 | - Effect: Allow 56 | Action: 57 | - 'network-firewall:*' 58 | Resource: 59 | - !GetAtt StatefulRulegroup.RuleGroupArn 60 | Tags: 61 | - Key: "ProjectName" 62 | Value: "AlphasocEncryptedDNSFiltering" 63 | LambdaLogGroup: 64 | Type: AWS::Logs::LogGroup 65 | Properties: 66 | LogGroupName: !Sub "/aws/lambda/${LambdaFunction}" 67 | DeletionPolicy: Retain 68 | ScheduledRule: 69 | Type: AWS::Events::Rule 70 | Properties: 71 | Description: "AlphasocEncyptedDNSDailyTrigger" 72 | ScheduleExpression: "cron(0 0 * * ? *)" 73 | State: "ENABLED" 74 | Targets: 75 | - Arn: 76 | Fn::GetAtt: 77 | - "LambdaFunction" 78 | - "Arn" 79 | Id: "TargetFunctionV1" 80 | PermissionForEventsToInvokeLambda: 81 | Type: AWS::Lambda::Permission 82 | Properties: 83 | FunctionName: !Ref "LambdaFunction" 84 | Action: "lambda:InvokeFunction" 85 | Principal: "events.amazonaws.com" 86 | SourceArn: 87 | Fn::GetAtt: 88 | - "ScheduledRule" 89 | - "Arn" 90 | LambdaInvoke: 91 | Type: AWS::CloudFormation::CustomResource 92 | Version: "1.0" 93 | Properties: 94 | ServiceToken: !GetAtt LambdaFunction.Arn 95 | LambdaFunction: 96 | Type: AWS::Lambda::Function 97 | Properties: 98 | Role: !GetAtt LambdaExecutionRole.Arn 99 | Runtime: nodejs16.x 100 | Handler: index.handler 101 | Timeout: 60 102 | Description: Used to fetch data from the Alphasoc Encrypted DNS list and update the associated RuleGroup 103 | Tags: 104 | - Key: "ProjectName" 105 | Value: "AlphasocEncryptedDNSFiltering" 106 | Code: 107 | ZipFile: !Sub | 108 | const AWS = require("aws-sdk"); 109 | const response = require('cfn-response'); 110 | const https = require("https"); 111 | let listOfIps = [], listHeader = []; 112 | const networkfirewall = new AWS.NetworkFirewall(); 113 | 114 | const rulesUrl = "https://feeds.alphasoc.net/encrypted_dns.txt"; 115 | const rgARN = "${StatefulRulegroup.RuleGroupArn}" 116 | 117 | function fetchIPs() { 118 | console.log("Fetching the list of IP addresses..."); 119 | return new Promise((resolve, reject) => { 120 | let dataString = ''; 121 | let post_req = https.request(rulesUrl, (res) => { 122 | res.setEncoding("utf8"); 123 | res.on('data', chunk => { 124 | dataString += chunk; 125 | }); 126 | res.on('end', () => { 127 | //initial array creation 128 | listOfIps = dataString.split(/\r?\n/); 129 | //strip out the header 130 | listHeader = listOfIps.filter((line) => line.match(/^#+/)); 131 | //extract IPv4 lines 132 | listOfIps = listOfIps.filter((line) => line.match(/^(\d+(\.|$)){3}(\d+)/)); 133 | //create objects from the IPv4 Data 134 | listOfIps = listOfIps.map(s => { 135 | let items = s.split(","); 136 | return JSON.parse('{"ip":"' + items[0] + '","port":"' + items[1] + '","protocol":"' + items[2] + '","service":"' + items[3] + '","operator":"' + items[4] + '"}'); 137 | }); 138 | console.log("Fetched " + listOfIps.length + " IP addresses..."); 139 | resolve(); 140 | }); 141 | res.on('error', (err) => { 142 | reject(err); 143 | }); 144 | }); 145 | post_req.end(); 146 | }); 147 | } 148 | 149 | let updateRules = async function (ruleGroup) { 150 | let params = ruleGroup; 151 | delete params.Capacity; 152 | params.RuleGroupName = params.RuleGroupResponse.RuleGroupName; 153 | params.Description = "Dynamic list using data fetched from" + rulesUrl; 154 | params.Type = params.RuleGroupResponse.Type; 155 | delete params.RuleGroupResponse; 156 | 157 | console.log("Updating rules..."); 158 | let res = await networkfirewall.updateRuleGroup(params).promise(); 159 | if (res) { 160 | console.log("Updated '" + params.RuleGroupName + "'."); 161 | } else { 162 | console.log("Error updating the rules for '" + params.RuleGroupName + "'..."); 163 | } 164 | return; 165 | }; 166 | 167 | let createRules = async function (ruleGroup, type) { 168 | if (listOfIps.length == 0) { 169 | await fetchIPs(); 170 | } else { 171 | console.log("Using recently fetched list of " + listOfIps.length + " IP addresses..."); 172 | } 173 | 174 | let rulesString = "# RG Last updated: " + new Date().toUTCString() + "\n"; 175 | rulesString += "# Using a list of " + listOfIps.length + " IP addresses\n"; 176 | rulesString += "# ------------------ Following section fetched from " + rulesUrl + " -----------------------\n"; 177 | listHeader.forEach((line) => {rulesString += line + "\n"}); 178 | rulesString += "# ------------------ End section fetched from " + rulesUrl + " -----------------------\n"; 179 | 180 | listOfIps.forEach((obj, index) => { 181 | //Construct the rule (example: drop tcp $HOME_NET any -> 104.16.248.249/32 853 (msg:"Denied Cloudflare DoT"; sid:1;) 182 | rulesString += type + ` ` + obj.protocol + ` $HOME_NET any -> ` + obj.ip+"/32 " + obj.port + ' (msg:"' + type + ' resulting from ' + obj.service + ' traffic to ' + obj.operator + '"; rev:1; sid:77' + index + ';)\n'; 183 | }); 184 | 185 | ruleGroup.RuleGroup.RulesSource.RulesString = rulesString; 186 | await updateRules(ruleGroup); 187 | 188 | return; 189 | }; 190 | 191 | exports.handler = async (event, context) => { 192 | if (event.RequestType == "Delete") { 193 | await response.send(event, context, "SUCCESS"); 194 | return; 195 | } 196 | 197 | let params = {Type: "STATEFUL", RuleGroupArn: rgARN}; 198 | 199 | console.log("Searching for matching Rule Group..."); 200 | let res = await networkfirewall.describeRuleGroup(params).promise(); 201 | 202 | if (res.RuleGroupResponse) { 203 | console.log("Found Rule Group..."); 204 | await createRules(res,"${RuleGroupAction}"); 205 | if (event.ResponseURL) await response.send(event, context, response.SUCCESS); 206 | } else { 207 | console.log("ERROR: No matching Rule Group found..."); 208 | if (event.ResponseURL) await response.send(event, context, response.FAILED); 209 | } 210 | 211 | return; 212 | }; 213 | -------------------------------------------------------------------------------- /Alphasoc/src/EncryptedDNS.js: -------------------------------------------------------------------------------- 1 | const AWS = require("aws-sdk"); 2 | const response = require('cfn-response'); 3 | const https = require("https"); 4 | let listOfIps = [], listHeader = []; 5 | const networkfirewall = new AWS.NetworkFirewall(); 6 | 7 | const rulesUrl = "https://feeds.alphasoc.net/encrypted_dns.txt"; 8 | const rgARN = ""; 9 | 10 | function fetchIPs() { 11 | console.log("Fetching the list of IP addresses..."); 12 | return new Promise((resolve, reject) => { 13 | let dataString = ''; 14 | let post_req = https.request(rulesUrl, (res) => { 15 | res.setEncoding("utf8"); 16 | res.on('data', chunk => { 17 | dataString += chunk; 18 | }); 19 | res.on('end', () => { 20 | //initial array creation 21 | listOfIps = dataString.split(/\r?\n/); 22 | //strip out the header 23 | listHeader = listOfIps.filter((line) => line.match(/^#+/)); 24 | //extract IPv4 lines 25 | listOfIps = listOfIps.filter((line) => line.match(/^(\d+(\.|$)){3}(\d+)/)); 26 | //create objects from the IPv4 Data 27 | listOfIps = listOfIps.map(s => { 28 | let items = s.split(","); 29 | return JSON.parse('{"ip":"' + items[0] + '","port":"' + items[1] + '","protocol":"' + items[2] + '","service":"' + items[3] + '","operator":"' + items[4] + '"}'); 30 | }); 31 | console.log("Fetched " + listOfIps.length + " IP addresses..."); 32 | resolve(); 33 | }); 34 | res.on('error', (err) => { 35 | reject(err); 36 | }); 37 | }); 38 | post_req.end(); 39 | }); 40 | } 41 | 42 | let updateRules = async function (ruleGroup) { 43 | let params = ruleGroup; 44 | delete params.Capacity; 45 | params.RuleGroupName = params.RuleGroupResponse.RuleGroupName; 46 | params.Description = "Dynamic list using data fetched from" + rulesUrl; 47 | params.Type = params.RuleGroupResponse.Type; 48 | delete params.RuleGroupResponse; 49 | 50 | console.log("Updating rules..."); 51 | let res = await networkfirewall.updateRuleGroup(params).promise(); 52 | if (res) { 53 | console.log("Updated '" + params.RuleGroupName + "'."); 54 | } else { 55 | console.log("Error updating the rules for '" + params.RuleGroupName + "'..."); 56 | } 57 | return; 58 | }; 59 | 60 | let createRules = async function (ruleGroup, type) { 61 | if (listOfIps.length == 0) { 62 | await fetchIPs(); 63 | } else { 64 | console.log("Using recently fetched list of " + listOfIps.length + " IP addresses..."); 65 | } 66 | 67 | let rulesString = "# RG Last updated: " + new Date().toUTCString() + "\n"; 68 | rulesString += "# Using a list of " + listOfIps.length + " IP addresses\n"; 69 | rulesString += "# ------------------ Following section fetched from " + rulesUrl + " -----------------------\n"; 70 | listHeader.forEach((line) => {rulesString += line + "\n"}); 71 | rulesString += "# ------------------ End section fetched from " + rulesUrl + " -----------------------\n"; 72 | 73 | listOfIps.forEach((obj, index) => { 74 | //Construct the rule (example: drop tcp $HOME_NET any -> 104.16.248.249/32 853 (msg:"Denied Cloudflare DoT"; sid:1;) 75 | rulesString += type + ` ` + obj.protocol + ` $HOME_NET any -> ` + obj.ip+"/32 " + obj.port + ' (msg:"' + type + ' resulting from ' + obj.service + ' traffic to ' + obj.operator + '"; rev:1; sid:77' + index + ';)\n'; 76 | }); 77 | 78 | ruleGroup.RuleGroup.RulesSource.RulesString = rulesString; 79 | await updateRules(ruleGroup); 80 | 81 | return; 82 | }; 83 | 84 | exports.handler = async (event, context) => { 85 | if (event.RequestType == "Delete") { 86 | await response.send(event, context, "SUCCESS"); 87 | return; 88 | } 89 | 90 | let params = {Type: "STATEFUL", RuleGroupArn: rgARN}; 91 | 92 | console.log("Searching for matching Rule Group..."); 93 | let res = await networkfirewall.describeRuleGroup(params).promise(); 94 | 95 | if (res.RuleGroupResponse) { 96 | console.log("Found Rule Group..."); 97 | await createRules(res,"alert"); 98 | if (event.ResponseURL) await response.send(event, context, response.SUCCESS); 99 | } else { 100 | console.log("ERROR: No matching Rule Group found..."); 101 | if (event.ResponseURL) await response.send(event, context, response.FAILED); 102 | } 103 | 104 | return; 105 | }; 106 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /EmergingThreats/cfn-templates/EmergingThreatsBotCC.yaml: -------------------------------------------------------------------------------- 1 | Parameters: 2 | RuleGroupName: 3 | Type: String 4 | Default: 'AutoUpdating-EmergingThreatsBotCCList' 5 | RuleGroupAction: 6 | Type: String 7 | Description: "Used to define the action to take on a matching rule if found" 8 | Default : 'drop' 9 | AllowedValues: 10 | - 'alert' 11 | - 'drop' 12 | 13 | Resources: 14 | StatefulRulegroup: 15 | Type: 'AWS::NetworkFirewall::RuleGroup' 16 | Properties: 17 | RuleGroupName: !Sub "${AWS::StackName}-${RuleGroupName}-${RuleGroupAction}" 18 | Type: STATEFUL 19 | RuleGroup: 20 | RulesSource: 21 | RulesString: '#This will be automatically updated via the Lambda function' 22 | Capacity: 500 23 | Description: >- 24 | Used to track a list of Emerging IP Threats from 25 | https://rules.emergingthreats.net/blockrules/emerging-botcc.suricata.rules. It is updated daily by the CloudWatch Event: EmergingThreatsBotCCDailyTrigger. 26 | Tags: 27 | - Key: "ProjectName" 28 | Value: "EmergingThreatsBotCC" 29 | LambdaExecutionRole: 30 | Type: AWS::IAM::Role 31 | Properties: 32 | AssumeRolePolicyDocument: 33 | Version: '2012-10-17' 34 | Statement: 35 | - Effect: Allow 36 | Principal: 37 | Service: 38 | - lambda.amazonaws.com 39 | Action: 40 | - sts:AssumeRole 41 | Path: "/" 42 | Policies: 43 | - PolicyName: LambdaLogs 44 | PolicyDocument: 45 | Version: '2012-10-17' 46 | Statement: 47 | - Effect: Allow 48 | Action: 49 | - 'logs:CreateLogStream' 50 | - 'logs:PutLogEvents' 51 | Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*' 52 | - PolicyName: NetworkFirewall 53 | PolicyDocument: 54 | Version: '2012-10-17' 55 | Statement: 56 | - Effect: Allow 57 | Action: 58 | - 'network-firewall:*' 59 | Resource: 60 | - !GetAtt StatefulRulegroup.RuleGroupArn 61 | Tags: 62 | - Key: "ProjectName" 63 | Value: "EmergingThreatsBotCC" 64 | LambdaLogGroup: 65 | Type: AWS::Logs::LogGroup 66 | Properties: 67 | LogGroupName: !Sub "/aws/lambda/${LambdaFunction}" 68 | DeletionPolicy: Retain 69 | ScheduledRule: 70 | Type: AWS::Events::Rule 71 | Properties: 72 | Description: "EmergingThreatsBotCCDailyTrigger" 73 | ScheduleExpression: "cron(0 0 * * ? *)" 74 | State: "ENABLED" 75 | Targets: 76 | - Arn: 77 | Fn::GetAtt: 78 | - "LambdaFunction" 79 | - "Arn" 80 | Id: "TargetFunctionV1" 81 | PermissionForEventsToInvokeLambda: 82 | Type: AWS::Lambda::Permission 83 | Properties: 84 | FunctionName: !Ref "LambdaFunction" 85 | Action: "lambda:InvokeFunction" 86 | Principal: "events.amazonaws.com" 87 | SourceArn: 88 | Fn::GetAtt: 89 | - "ScheduledRule" 90 | - "Arn" 91 | LambdaInvoke: 92 | Type: AWS::CloudFormation::CustomResource 93 | Version: "1.0" 94 | Properties: 95 | ServiceToken: !GetAtt LambdaFunction.Arn 96 | LambdaFunction: 97 | Type: AWS::Lambda::Function 98 | Properties: 99 | Role: !GetAtt LambdaExecutionRole.Arn 100 | Runtime: nodejs16.x 101 | Handler: index.handler 102 | Timeout: 60 103 | Description: Used to fetch data from the Emerging Threats Bot CC list and update the associated RuleGroup 104 | Tags: 105 | - Key: "ProjectName" 106 | Value: "EmergingThreatsBotCC" 107 | Code: 108 | ZipFile: !Sub | 109 | var AWS = require("aws-sdk"); 110 | var response = require('cfn-response'); 111 | var https = require("https"); 112 | var listOfRules = []; 113 | const url = "https://rules.emergingthreats.net/blockrules/emerging-botcc.suricata.rules"; 114 | 115 | const networkfirewall = new AWS.NetworkFirewall(); 116 | 117 | function fetchRules() { 118 | console.log("Fetching the list of rules..."); 119 | return new Promise((resolve, reject) => { 120 | let dataString = ''; 121 | let post_req = https.request(url, (res) => { 122 | res.setEncoding("utf8"); 123 | res.on('data', chunk => { 124 | dataString += chunk; 125 | }); 126 | res.on('end', () => { 127 | listOfRules = dataString.split(/\r?\n/); 128 | console.log("Fetched rules..."); 129 | resolve(); 130 | }); 131 | res.on('error', (err) => { 132 | reject(err); 133 | }); 134 | }); 135 | post_req.end(); 136 | }); 137 | } 138 | 139 | let updateRules = async function (ruleGroup,newRules) { 140 | let params = ruleGroup; 141 | delete params.Capacity; 142 | params.RuleGroupName = params.RuleGroupResponse.RuleGroupName; 143 | params.Description = params.RuleGroupResponse.Description; 144 | params.Type = params.RuleGroupResponse.Type; 145 | delete params.RuleGroupResponse; 146 | let rulesString = "# Last autofetched by Lambda: " + new Date().toUTCString() + "\n"; 147 | rulesString += newRules.join("\n"); 148 | params.RuleGroup.RulesSource.RulesString = rulesString; 149 | 150 | console.log("Updating rules..."); 151 | let res = await networkfirewall.updateRuleGroup(params).promise(); 152 | if (res) { 153 | console.log("Updated '" + params.RuleGroupName + "'."); 154 | } else { 155 | console.log("Error updating the rules for '" + params.RuleGroupName + "'..."); 156 | } 157 | return; 158 | }; 159 | 160 | let createRules = async function (action) { 161 | if (listOfRules.length == 0) { 162 | await fetchRules(); 163 | } else { 164 | console.log("Using recently fetched list of rules..."); 165 | } 166 | 167 | if (action == 'drop') listOfRules = listOfRules.map(rule => rule.replace("alert ", "drop ")); 168 | 169 | return; 170 | }; 171 | 172 | exports.handler = async (event, context) => { 173 | 174 | var rg = {Type: "STATEFUL", RuleGroupArn: '${StatefulRulegroup.RuleGroupArn}'}; 175 | 176 | await createRules('${RuleGroupAction}'); 177 | 178 | console.log("Searching Rule Groups for " + rg.RuleGroupArn + "..."); 179 | let res = await networkfirewall.describeRuleGroup(rg).promise(); 180 | if (res.RuleGroupResponse) { 181 | console.log("Found matching Rule Group..."); 182 | await updateRules(res,listOfRules); 183 | if (event.ResponseURL) await response.send(event, context, response.SUCCESS); 184 | } else { 185 | console.log("ERROR: No matching Rule Group found..."); 186 | if (event.ResponseURL) await response.send(event, context, response.FAILED); 187 | } 188 | 189 | return; 190 | }; 191 | -------------------------------------------------------------------------------- /EmergingThreats/cfn-templates/EmergingThreatsIPFiltering.yaml: -------------------------------------------------------------------------------- 1 | Parameters: 2 | RuleGroupName: 3 | Type: String 4 | Default: 'AutoUpdating-EmergingThreatsIPList' 5 | RuleGroupAction: 6 | Type: String 7 | Description: "Used to define the action to take on a matching rule if found" 8 | Default : 'drop' 9 | AllowedValues: 10 | - 'alert' 11 | - 'drop' 12 | 13 | Resources: 14 | StatefulRulegroup: 15 | Type: 'AWS::NetworkFirewall::RuleGroup' 16 | Properties: 17 | RuleGroupName: !Sub "${AWS::StackName}-${RuleGroupName}-${RuleGroupAction}" 18 | Type: STATEFUL 19 | RuleGroup: 20 | RulesSource: 21 | RulesString: '#This will be automatically updated via the Lambda function' 22 | Capacity: 3000 23 | Description: >- 24 | Used to track a list of Emerging IP Threats from 25 | https://rules.emergingthreats.net/fwrules/emerging-Block-IPs.txt. It is updated daily by the CloudWatch Event: EmergingThreatsDailyTrigger. 26 | Tags: 27 | - Key: "ProjectName" 28 | Value: "EmergingThreatsIPFiltering" 29 | LambdaExecutionRole: 30 | Type: AWS::IAM::Role 31 | Properties: 32 | AssumeRolePolicyDocument: 33 | Version: '2012-10-17' 34 | Statement: 35 | - Effect: Allow 36 | Principal: 37 | Service: 38 | - lambda.amazonaws.com 39 | Action: 40 | - sts:AssumeRole 41 | Path: "/" 42 | Policies: 43 | - PolicyName: LambdaLogs 44 | PolicyDocument: 45 | Version: '2012-10-17' 46 | Statement: 47 | - Effect: Allow 48 | Action: 49 | - 'logs:CreateLogStream' 50 | - 'logs:PutLogEvents' 51 | Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*' 52 | - PolicyName: NetworkFirewall 53 | PolicyDocument: 54 | Version: '2012-10-17' 55 | Statement: 56 | - Effect: Allow 57 | Action: 58 | - 'network-firewall:*' 59 | Resource: 60 | - !GetAtt StatefulRulegroup.RuleGroupArn 61 | Tags: 62 | - Key: "ProjectName" 63 | Value: "EmergingThreatsIPFiltering" 64 | LambdaLogGroup: 65 | Type: AWS::Logs::LogGroup 66 | Properties: 67 | LogGroupName: !Sub "/aws/lambda/${LambdaFunction}" 68 | DeletionPolicy: Retain 69 | ScheduledRule: 70 | Type: AWS::Events::Rule 71 | Properties: 72 | Description: "EmergingThreatsDailyTrigger" 73 | ScheduleExpression: "cron(0 0 * * ? *)" 74 | State: "ENABLED" 75 | Targets: 76 | - Arn: 77 | Fn::GetAtt: 78 | - "LambdaFunction" 79 | - "Arn" 80 | Id: "TargetFunctionV1" 81 | PermissionForEventsToInvokeLambda: 82 | Type: AWS::Lambda::Permission 83 | Properties: 84 | FunctionName: !Ref "LambdaFunction" 85 | Action: "lambda:InvokeFunction" 86 | Principal: "events.amazonaws.com" 87 | SourceArn: 88 | Fn::GetAtt: 89 | - "ScheduledRule" 90 | - "Arn" 91 | LambdaInvoke: 92 | Type: AWS::CloudFormation::CustomResource 93 | Version: "1.0" 94 | Properties: 95 | ServiceToken: !GetAtt LambdaFunction.Arn 96 | LambdaFunction: 97 | Type: AWS::Lambda::Function 98 | Properties: 99 | Role: !GetAtt LambdaExecutionRole.Arn 100 | Runtime: nodejs16.x 101 | Handler: index.handler 102 | Timeout: 60 103 | Description: Used to fetch data from the Emerging Threats IP list and update the associated RuleGroup 104 | Tags: 105 | - Key: "ProjectName" 106 | Value: "EmergingThreatsIPFiltering" 107 | Code: 108 | ZipFile: !Sub | 109 | var AWS = require("aws-sdk"); 110 | var response = require('cfn-response'); 111 | var https = require("https"); 112 | var listOfIps = []; 113 | const url = "https://rules.emergingthreats.net/fwrules/emerging-Block-IPs.txt"; 114 | 115 | const networkfirewall = new AWS.NetworkFirewall(); 116 | 117 | function fetchIPs() { 118 | console.log("Fetching the list of IP addresses..."); 119 | return new Promise((resolve, reject) => { 120 | let dataString = ''; 121 | let post_req = https.request(url, (res) => { 122 | res.setEncoding("utf8"); 123 | res.on('data', chunk => { 124 | dataString += chunk; 125 | }); 126 | res.on('end', () => { 127 | listOfIps = dataString.split(/\r?\n/); 128 | listOfIps = listOfIps.filter((line) => line.match(/^\d+/)); 129 | console.log("Fetched " + listOfIps.length + " IP addresses..."); 130 | resolve(); 131 | }); 132 | res.on('error', (err) => { 133 | reject(err); 134 | }); 135 | }); 136 | post_req.end(); 137 | }); 138 | } 139 | 140 | let updateRules = async function (ruleGroup) { 141 | let params = ruleGroup; 142 | delete params.Capacity; 143 | params.RuleGroupName = params.RuleGroupResponse.RuleGroupName; 144 | params.Description = params.RuleGroupResponse.Description; 145 | params.Type = params.RuleGroupResponse.Type; 146 | delete params.RuleGroupResponse; 147 | 148 | console.log("Updating rules..."); 149 | let res = await networkfirewall.updateRuleGroup(params).promise(); 150 | if (res) { 151 | console.log("Updated '" + params.RuleGroupName + "'."); 152 | } else { 153 | console.log("Error updating the rules for '" + params.RuleGroupName + "'..."); 154 | } 155 | return; 156 | }; 157 | 158 | let createRules = async function (ruleGroup, type) { 159 | if (listOfIps.length == 0) { 160 | await fetchIPs(); 161 | } else { 162 | console.log("Using recently fetched list of " + listOfIps.length + " IP addresses..."); 163 | } 164 | 165 | let rulesString = "# Last updated: " + new Date().toUTCString() + "\n"; 166 | rulesString += "# Using a list of " + listOfIps.length + " IP addresses\n"; 167 | 168 | listOfIps.forEach((ip, index) => { 169 | rulesString += type + ' ip ' + ip + ' any -> any any (msg:"' + type + ' emerging threats traffic from ' + ip + '"; rev:1; sid:55' + index + ';)\n'; 170 | rulesString += type + ' ip any any -> ' + ip + ' any (msg:"' + type + ' emerging threats traffic to ' + ip + '"; rev:1; sid:66' + index + ';)\n'; 171 | }); 172 | 173 | ruleGroup.RuleGroup.RulesSource.RulesString = rulesString; 174 | await updateRules(ruleGroup); 175 | 176 | return; 177 | }; 178 | 179 | exports.handler = async (event, context) => { 180 | if (event.RequestType == "Delete") { 181 | await response.send(event, context, "SUCCESS"); 182 | return; 183 | } 184 | 185 | var params = {Type: "STATEFUL", RuleGroupArn: '${StatefulRulegroup.RuleGroupArn}'}; 186 | 187 | console.log("Searching Rule Groups for '${RuleGroupName}'..."); 188 | let res = await networkfirewall.describeRuleGroup(params).promise(); 189 | if (res.RuleGroupResponse) { 190 | console.log("Found '${RuleGroupName}'..."); 191 | await createRules(res, '${RuleGroupAction}'); 192 | if (event.ResponseURL) await response.send(event, context, response.SUCCESS); 193 | } else { 194 | console.log("ERROR: No matching Rule Group found for '${RuleGroupName}'..."); 195 | if (event.ResponseURL) await response.send(event, context, response.FAILED); 196 | } 197 | return; 198 | }; -------------------------------------------------------------------------------- /EmergingThreats/src/EmergingBotCC.js: -------------------------------------------------------------------------------- 1 | var AWS = require("aws-sdk"); 2 | var https = require("https"); 3 | var listOfRules = []; 4 | const url = "https://rules.emergingthreats.net/blockrules/emerging-botcc.suricata.rules"; 5 | 6 | const networkfirewall = new AWS.NetworkFirewall(); 7 | 8 | function fetchRules() { 9 | console.log("Fetching the list of rules..."); 10 | return new Promise((resolve, reject) => { 11 | let dataString = ''; 12 | let post_req = https.request(url, (res) => { 13 | res.setEncoding("utf8"); 14 | res.on('data', chunk => { 15 | dataString += chunk; 16 | }); 17 | res.on('end', () => { 18 | listOfRules = dataString.split(/\r?\n/); 19 | console.log("Fetched rules..."); 20 | resolve(); 21 | }); 22 | res.on('error', (err) => { 23 | reject(err); 24 | }); 25 | }); 26 | post_req.end(); 27 | }); 28 | } 29 | 30 | let updateRules = async function (ruleGroup,newRules) { 31 | let params = ruleGroup; 32 | delete params.Capacity; 33 | params.RuleGroupName = params.RuleGroupResponse.RuleGroupName; 34 | params.Description = params.RuleGroupResponse.Description; 35 | params.Type = params.RuleGroupResponse.Type; 36 | delete params.RuleGroupResponse; 37 | let rulesString = "# Last autofetched by Lambda: " + new Date().toUTCString() + "\n"; 38 | rulesString += newRules.join("\n"); 39 | params.RuleGroup.RulesSource.RulesString = rulesString; 40 | 41 | console.log("Updating rules..."); 42 | let res = await networkfirewall.updateRuleGroup(params).promise(); 43 | if (res) { 44 | console.log("Updated '" + params.RuleGroupName + "'."); 45 | } else { 46 | console.log("Error updating the rules for '" + params.RuleGroupName + "'..."); 47 | } 48 | return; 49 | }; 50 | 51 | let createRules = async function (action) { 52 | if (listOfRules.length == 0) { 53 | await fetchRules(); 54 | } else { 55 | console.log("Using recently fetched list of rules..."); 56 | } 57 | 58 | if (action == 'drop') listOfRules = listOfRules.map(rule => rule.replace("alert ", "drop ")); 59 | 60 | return; 61 | }; 62 | 63 | exports.handler = async (event, context) => { 64 | 65 | var rg = {Type: "STATEFUL", RuleGroupArn: ''}; 66 | 67 | await createRules('drop'); 68 | 69 | console.log("Searching Rule Groups for " + rg.RuleGroupArn + "..."); 70 | let res = await networkfirewall.describeRuleGroup(rg).promise(); 71 | if (res.RuleGroupResponse) { 72 | console.log("Found matching Rule Group..."); 73 | await updateRules(res,listOfRules); 74 | } else { 75 | console.log("ERROR: No matching Rule Group found..."); 76 | } 77 | 78 | return; 79 | }; 80 | -------------------------------------------------------------------------------- /EmergingThreats/src/EmergingThreats.js: -------------------------------------------------------------------------------- 1 | var AWS = require("aws-sdk"); 2 | var https = require("https"); 3 | var listOfIps = []; 4 | const url = "https://rules.emergingthreats.net/fwrules/emerging-Block-IPs.txt"; 5 | 6 | const networkfirewall = new AWS.NetworkFirewall(); 7 | 8 | function fetchIPs() { 9 | console.log("Fetching the list of IP addresses..."); 10 | return new Promise((resolve, reject) => { 11 | let dataString = ""; 12 | let post_req = https.request(url, (res) => { 13 | res.setEncoding("utf8"); 14 | res.on("data", (chunk) => { 15 | dataString += chunk; 16 | }); 17 | res.on("end", () => { 18 | listOfIps = dataString.split(/\r?\n/); 19 | listOfIps = listOfIps.filter((line) => line.match(/^\d+/)); 20 | console.log("Fetched " + listOfIps.length + " IP addresses..."); 21 | resolve(); 22 | }); 23 | res.on("error", (err) => { 24 | reject(err); 25 | }); 26 | }); 27 | post_req.end(); 28 | }); 29 | } 30 | 31 | let updateRules = async function (ruleGroup) { 32 | let params = ruleGroup; 33 | delete params.Capacity; 34 | params.RuleGroupName = params.RuleGroupResponse.RuleGroupName; 35 | params.Description = params.RuleGroupResponse.Description; 36 | params.Type = params.RuleGroupResponse.Type; 37 | delete params.RuleGroupResponse; 38 | 39 | console.log("Updating rules..."); 40 | let res = await networkfirewall.updateRuleGroup(params).promise(); 41 | if (res) { 42 | console.log("Updated '" + params.RuleGroupName + "'."); 43 | } else { 44 | console.log( 45 | "Error updating the rules for '" + params.RuleGroupName + "'..." 46 | ); 47 | } 48 | return; 49 | }; 50 | 51 | let createRules = async function (ruleGroup, type) { 52 | if (listOfIps.length == 0) { 53 | await fetchIPs(); 54 | } else { 55 | console.log( 56 | "Using recently fetched list of " + listOfIps.length + " IP addresses..." 57 | ); 58 | } 59 | 60 | let rulesString = "# Last updated: " + new Date().toUTCString() + "\n"; 61 | rulesString += "# Using a list of " + listOfIps.length + " IP addresses\n"; 62 | 63 | listOfIps.forEach((ip, index) => { 64 | rulesString += type + " ip " + ip + ' any -> any any (msg:"' + type + " emerging threats traffic from " + ip + '"; rev:1; sid:55' + index + ";)\n"; 65 | rulesString += type + " ip any any -> " + ip + ' any (msg:"' + type + " emerging threats traffic to " + ip + '"; rev:1; sid:66' + index + ";)\n"; 66 | }); 67 | 68 | ruleGroup.RuleGroup.RulesSource.RulesString = rulesString; 69 | await updateRules(ruleGroup); 70 | 71 | return; 72 | }; 73 | 74 | exports.handler = async (event, context) => { 75 | 76 | var params = { Type: "STATEFUL", RuleGroupArn: "" }; 77 | var rgName = ""; 78 | var rgAction = "drop"; 79 | 80 | console.log("Searching Rule Groups for " + rgName + "..."); 81 | let res = await networkfirewall.describeRuleGroup(params).promise(); 82 | if (res.RuleGroupResponse) { 83 | console.log("Found " + rgName + "..."); 84 | await createRules(res, rgAction); 85 | } else { 86 | console.log("ERROR: No matching Rule Group found for " + rgName + "..."); 87 | } 88 | return; 89 | }; 90 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /LinodeAddresses/cfn-templates/LinodeAddresses.yaml: -------------------------------------------------------------------------------- 1 | Parameters: 2 | RuleGroupName: 3 | Type: String 4 | Default: 'LinodeAddresses' 5 | RuleGroupAction: 6 | Type: String 7 | Description: "Used to define the action to take on a matching rule if found" 8 | Default : 'drop' 9 | AllowedValues: 10 | - 'alert' 11 | - 'drop' 12 | 13 | Resources: 14 | StatefulRulegroup: 15 | Type: 'AWS::NetworkFirewall::RuleGroup' 16 | Properties: 17 | RuleGroupName: !Sub "${AWS::StackName}-${RuleGroupName}-${RuleGroupAction}" 18 | Type: STATEFUL 19 | RuleGroup: 20 | RulesSource: 21 | RulesString: '#This will be updated via the Lambda function' 22 | Capacity: 3000 23 | Description: >- 24 | Used to dynamically track a list of addresses associated with Linode endpoints 25 | Tags: 26 | - Key: "ProjectName" 27 | Value: "LinodeAddressesFiltering" 28 | LambdaExecutionRole: 29 | Type: AWS::IAM::Role 30 | Properties: 31 | AssumeRolePolicyDocument: 32 | Version: '2012-10-17' 33 | Statement: 34 | - Effect: Allow 35 | Principal: 36 | Service: 37 | - lambda.amazonaws.com 38 | Action: 39 | - sts:AssumeRole 40 | Path: "/" 41 | Policies: 42 | - PolicyName: LambdaLogs 43 | PolicyDocument: 44 | Version: '2012-10-17' 45 | Statement: 46 | - Effect: Allow 47 | Action: 48 | - 'logs:CreateLogStream' 49 | - 'logs:PutLogEvents' 50 | Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*' 51 | - PolicyName: NetworkFirewall 52 | PolicyDocument: 53 | Version: '2012-10-17' 54 | Statement: 55 | - Effect: Allow 56 | Action: 57 | - 'network-firewall:*' 58 | Resource: 59 | - !GetAtt StatefulRulegroup.RuleGroupArn 60 | Tags: 61 | - Key: "ProjectName" 62 | Value: "LinodeAddressesFiltering" 63 | LambdaLogGroup: 64 | Type: AWS::Logs::LogGroup 65 | Properties: 66 | LogGroupName: !Sub "/aws/lambda/${LambdaFunction}" 67 | DeletionPolicy: Retain 68 | ScheduledRule: 69 | Type: AWS::Events::Rule 70 | Properties: 71 | Description: "AlphasocEncyptedDNSDailyTrigger" 72 | ScheduleExpression: "cron(0 0 * * ? *)" 73 | State: "ENABLED" 74 | Targets: 75 | - Arn: 76 | Fn::GetAtt: 77 | - "LambdaFunction" 78 | - "Arn" 79 | Id: "TargetFunctionV1" 80 | PermissionForEventsToInvokeLambda: 81 | Type: AWS::Lambda::Permission 82 | Properties: 83 | FunctionName: !Ref "LambdaFunction" 84 | Action: "lambda:InvokeFunction" 85 | Principal: "events.amazonaws.com" 86 | SourceArn: 87 | Fn::GetAtt: 88 | - "ScheduledRule" 89 | - "Arn" 90 | LambdaInvoke: 91 | Type: AWS::CloudFormation::CustomResource 92 | Version: "1.0" 93 | Properties: 94 | ServiceToken: !GetAtt LambdaFunction.Arn 95 | LambdaFunction: 96 | Type: AWS::Lambda::Function 97 | Properties: 98 | Role: !GetAtt LambdaExecutionRole.Arn 99 | Runtime: nodejs16.x 100 | Handler: index.handler 101 | Timeout: 60 102 | Description: Used to fetch data from the Linode Addresses list and update the associated RuleGroup 103 | Tags: 104 | - Key: "ProjectName" 105 | Value: "LinodeAddressesFiltering" 106 | Code: 107 | ZipFile: !Sub | 108 | const AWS = require("aws-sdk"); 109 | const response = require('cfn-response'); 110 | const https = require("https"); 111 | let listOfIps = [], listHeader = []; 112 | const networkfirewall = new AWS.NetworkFirewall(); 113 | 114 | const rulesUrl = "https://geoip.linode.com/"; 115 | const rgARN = "${StatefulRulegroup.RuleGroupArn}" 116 | 117 | function fetchIPs() { 118 | console.log("Fetching the list of IP addresses..."); 119 | return new Promise((resolve, reject) => { 120 | let dataString = ''; 121 | let post_req = https.request(rulesUrl, (res) => { 122 | res.setEncoding("utf8"); 123 | res.on('data', chunk => { 124 | dataString += chunk; 125 | }); 126 | res.on('end', () => { 127 | //initial array creation 128 | listOfIps = dataString.split(/\r?\n/); 129 | //strip out the header 130 | listHeader = listOfIps.filter((line) => line.match(/^#+/)); 131 | //extract IPv4 lines 132 | listOfIps = listOfIps.filter((line) => line.match(/^(\d+(\.|$)){3}(\d+)/)); 133 | //create objects from the IPv4 Data 134 | listOfIps = listOfIps.map(s => { 135 | let items = s.split(","); 136 | return JSON.parse('{"ip":"' + items[0] + '","country":"' + items[1] + '","subdivision":"' + items[2] + '","city":"' + items[3] + '"}'); 137 | }); 138 | console.log("Fetched " + listOfIps.length + " IP addresses..."); 139 | resolve(); 140 | }); 141 | res.on('error', (err) => { 142 | reject(err); 143 | }); 144 | }); 145 | post_req.end(); 146 | }); 147 | } 148 | 149 | let updateRules = async function (ruleGroup) { 150 | let params = ruleGroup; 151 | delete params.Capacity; 152 | params.RuleGroupName = params.RuleGroupResponse.RuleGroupName; 153 | params.Description = "Dynamic list using data fetched from" + rulesUrl; 154 | params.Type = params.RuleGroupResponse.Type; 155 | delete params.RuleGroupResponse; 156 | 157 | console.log("Updating rules..."); 158 | let res = await networkfirewall.updateRuleGroup(params).promise(); 159 | if (res) { 160 | console.log("Updated '" + params.RuleGroupName + "'."); 161 | } else { 162 | console.log("Error updating the rules for '" + params.RuleGroupName + "'..."); 163 | } 164 | return; 165 | }; 166 | 167 | let createRules = async function (ruleGroup, type) { 168 | if (listOfIps.length == 0) { 169 | await fetchIPs(); 170 | } else { 171 | console.log("Using recently fetched list of " + listOfIps.length + " IP addresses..."); 172 | } 173 | 174 | let rulesString = "# RG Last updated: " + new Date().toUTCString() + "\n"; 175 | rulesString += "# Using a list of " + listOfIps.length + " IP addresses\n"; 176 | rulesString += "# ------------------ Following section fetched from " + rulesUrl + " -----------------------\n"; 177 | listHeader.forEach((line) => {rulesString += line + "\n"}); 178 | rulesString += "# ------------------ End section fetched from " + rulesUrl + " -----------------------\n"; 179 | 180 | listOfIps.forEach((obj, index) => { 181 | //Construct the rule (example: drop ip $HOME_NET any -> 104.16.248.0/24 any (msg:"drop resulting from traffic relating to a Linode address from US-Atlanta"; sid:1;) 182 | rulesString += type + ` ip $HOME_NET any -> ` + obj.ip + ' any (msg:"' + type + ' resulting from traffic relating to a Linode address from ' + obj.country + '-' + obj.city + '"; rev:1; sid:102' + index + ';)\n'; 183 | }); 184 | 185 | ruleGroup.RuleGroup.RulesSource.RulesString = rulesString; 186 | await updateRules(ruleGroup); 187 | 188 | return; 189 | }; 190 | 191 | exports.handler = async (event, context) => { 192 | if (event.RequestType == "Delete") { 193 | await response.send(event, context, "SUCCESS"); 194 | return; 195 | } 196 | 197 | let params = {Type: "STATEFUL", RuleGroupArn: rgARN}; 198 | 199 | console.log("Searching for matching Rule Group..."); 200 | let res = await networkfirewall.describeRuleGroup(params).promise(); 201 | 202 | if (res.RuleGroupResponse) { 203 | console.log("Found Rule Group..."); 204 | await createRules(res,"${RuleGroupAction}"); 205 | if (event.ResponseURL) await response.send(event, context, response.SUCCESS); 206 | } else { 207 | console.log("ERROR: No matching Rule Group found..."); 208 | if (event.ResponseURL) await response.send(event, context, response.FAILED); 209 | } 210 | 211 | return; 212 | }; 213 | -------------------------------------------------------------------------------- /LinodeAddresses/src/LinodeAddresses.js: -------------------------------------------------------------------------------- 1 | const AWS = require("aws-sdk"); 2 | const response = require('cfn-response'); 3 | const https = require("https"); 4 | let listOfIps = [], listHeader = []; 5 | const networkfirewall = new AWS.NetworkFirewall(); 6 | 7 | const rulesUrl = "https://geoip.linode.com/"; 8 | const rgARN = "" 9 | 10 | function fetchIPs() { 11 | console.log("Fetching the list of IP addresses..."); 12 | return new Promise((resolve, reject) => { 13 | let dataString = ''; 14 | let post_req = https.request(rulesUrl, (res) => { 15 | res.setEncoding("utf8"); 16 | res.on('data', chunk => { 17 | dataString += chunk; 18 | }); 19 | res.on('end', () => { 20 | //initial array creation 21 | listOfIps = dataString.split(/\r?\n/); 22 | //strip out the header 23 | listHeader = listOfIps.filter((line) => line.match(/^#+/)); 24 | //extract IPv4 lines 25 | listOfIps = listOfIps.filter((line) => line.match(/^(\d+(\.|$)){3}(\d+)/)); 26 | //create objects from the IPv4 Data 27 | listOfIps = listOfIps.map(s => { 28 | let items = s.split(","); 29 | return JSON.parse('{"ip":"' + items[0] + '","country":"' + items[1] + '","subdivision":"' + items[2] + '","city":"' + items[3] + '"}'); 30 | }); 31 | console.log("Fetched " + listOfIps.length + " IP addresses..."); 32 | resolve(); 33 | }); 34 | res.on('error', (err) => { 35 | reject(err); 36 | }); 37 | }); 38 | post_req.end(); 39 | }); 40 | } 41 | 42 | let updateRules = async function (ruleGroup) { 43 | let params = ruleGroup; 44 | delete params.Capacity; 45 | params.RuleGroupName = params.RuleGroupResponse.RuleGroupName; 46 | params.Description = "Dynamic list using data fetched from" + rulesUrl; 47 | params.Type = params.RuleGroupResponse.Type; 48 | delete params.RuleGroupResponse; 49 | 50 | console.log("Updating rules..."); 51 | let res = await networkfirewall.updateRuleGroup(params).promise(); 52 | if (res) { 53 | console.log("Updated '" + params.RuleGroupName + "'."); 54 | } else { 55 | console.log("Error updating the rules for '" + params.RuleGroupName + "'..."); 56 | } 57 | return; 58 | }; 59 | 60 | let createRules = async function (ruleGroup, type) { 61 | if (listOfIps.length == 0) { 62 | await fetchIPs(); 63 | } else { 64 | console.log("Using recently fetched list of " + listOfIps.length + " IP addresses..."); 65 | } 66 | 67 | let rulesString = "# RG Last updated: " + new Date().toUTCString() + "\n"; 68 | rulesString += "# Using a list of " + listOfIps.length + " IP addresses\n"; 69 | rulesString += "# ------------------ Following section fetched from " + rulesUrl + " -----------------------\n"; 70 | listHeader.forEach((line) => {rulesString += line + "\n"}); 71 | rulesString += "# ------------------ End section fetched from " + rulesUrl + " -----------------------\n"; 72 | 73 | listOfIps.forEach((obj, index) => { 74 | //Construct the rule (example: drop ip $HOME_NET any -> 104.16.248.0/24 any (msg:"drop resulting from traffic relating to a Linode address from US-Atlanta"; sid:1;) 75 | rulesString += type + ` ip $HOME_NET any -> ` + obj.ip + ' any (msg:"' + type + ' resulting from traffic relating to a Linode address from ' + obj.country + '-' + obj.city + '"; rev:1; sid:102' + index + ';)\n'; 76 | }); 77 | 78 | ruleGroup.RuleGroup.RulesSource.RulesString = rulesString; 79 | await updateRules(ruleGroup); 80 | 81 | return; 82 | }; 83 | 84 | exports.handler = async (event, context) => { 85 | if (event.RequestType == "Delete") { 86 | await response.send(event, context, "SUCCESS"); 87 | return; 88 | } 89 | 90 | let params = {Type: "STATEFUL", RuleGroupArn: rgARN}; 91 | 92 | console.log("Searching for matching Rule Group..."); 93 | let res = await networkfirewall.describeRuleGroup(params).promise(); 94 | 95 | if (res.RuleGroupResponse) { 96 | console.log("Found Rule Group..."); 97 | await createRules(res,"alert"); 98 | if (event.ResponseURL) await response.send(event, context, response.SUCCESS); 99 | } else { 100 | console.log("ERROR: No matching Rule Group found..."); 101 | if (event.ResponseURL) await response.send(event, context, response.FAILED); 102 | } 103 | 104 | return; 105 | }; 106 | -------------------------------------------------------------------------------- /NfwSlackIntegration/README.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | The Objective of this document is to deploy an AWS Network Firewall using a distributed deployment model and propagate the alerts generated by the firewall manager to a configurable Slack channel. 4 | 5 | # Key Elements and Challenges 6 | 7 | ## Elements discussed 8 | 9 | - Integrating Network firewall generated alerts to Slack for further action. 10 | - Network firewall Distributed deployment model 11 | 12 | ## Challenges 13 | 14 | VPCs are considered same as a physical network when it comes to compliance regimes such as PCI-DSS. Workloads runs on VPCs that are governed by a compliance regime can be protected using AWS Network Firewall, any unauthorized access from the other VPCs of the same account can be blocked or alerted. AWS Network firewall support limited number of destinations for delivering the generated alerts. However, these destinations act as a log destinations, any further action on these alerts requires a post log offline analysis either using Athena or Kinesis. This solution provides a method where NFW generated alert can be propagated to other systems for further action in a near real time basis. This solution also provides a platform 15 | 16 | ## Solution 17 | 18 | Network firewall can be used monitor and control network traffic between VPCs, ingress and egress flow. Network Firewall manager generated alerts can be delivered to S3 or Cloud watch log group or Kinesis data firehose. There are no out of the box options to deliver the alerts other than the above three destinations. This solution provides an option to deliver the alerts to a configurable Slack channel. 19 | 20 | The solution can also be used as a template or a platform to extend the alert delivery to other platforms such as JIRA, text or email etc., 21 | 22 | ### Prerequisites: 23 | 24 | This solution assumes that user already 25 | 26 | 1. Has a slack channel 27 | 2. Has the required privileges to send a slack message 28 | 3. In possession of the slack endpoint url with the token. 29 | 30 | [Refer this link for creating new slack workspace](https://slack.com/help/articles/206845317-Create-a-Slack-workspace) 31 | 32 | ### What is Included in the solution? 33 | 34 | All the resources that fall under the "Region" box in the above diagram are included in this solution. These resources will be automatically provisioned automatically though the CloudFormation templates and the source code required for the Lambda also included in the bundle. 35 | 36 | At the end of the CloudFormation execution the following AWS resources will be provisioned. 37 | 38 | 1. Vpc 39 | 2. Subnets – 4 ( 2 subnets dedicated for FW and 2 for workloads) 40 | 3. Internet gateway 41 | 4. Route tables with rules – 4 42 | 5. S3 bucket for Network firewall alert destination 43 | 6. S3 – Event configuration to Trigger Lambda 44 | 7. S3 – Bucket policy 45 | 8. Lambda Execution role 46 | 9. Lambda function to send Slack notifications 47 | 10. Secret manager Secret for storing Slack url 48 | 11. Network Firewall with alert configuration 49 | 50 | ### What is not Included in the solution? 51 | 52 | 1. Creating Slack channel 53 | 2. Test EC2 instance in the workload subnets 54 | 3. Test rules in Network Firewall 55 | 4. Actual or simulated traffic to trigger the test rules. 56 | 5. S3 bucket to hold the source files to be deployed. 57 | 58 | ### How does this solution work? 59 | 60 | This solution provides 2 protected subnets (Protected Subnet), 2 Network Firewall endpoint will be provisioned on the dedicated subnets (Firewall Subnet), all traffic going in and out of the protected subnets can be monitored by [creating FW policies](https://docs.aws.amazon.com/waf/latest/developerguide/network-firewall-policies.html) and rules. The Network firewall is configured to place all alerts to a S3 bucket. This S3 bucket is configured to invoke a Lambda function upon any 'put' event in the bucket. This Lambda fetches the configured slack url from Secret Manager and sends the notification message when invoked from S3. 61 | 62 | ### Components of this Solution: 63 | 64 | 1. A set of CloudFormation files in Yaml format. 65 | 2. Python source file for Lambda function. 66 | 3. Compressed Python source file. 67 | 68 | ### How to Deploy: 69 | 70 | 1. Create new or use an existing s3 bucket 71 | 2. Copy the following files to the above bucket (prefix recommended) from [source location](https://gitlab.aws.dev/vramaam/security-aod10-vramaam.git) 72 | 1. base.yml 73 | 2. igw-ingress-route.yml 74 | 3. slack-lambda.py 75 | 4. slackLambda.yml 76 | 5. decentralized-deployment.yml 77 | 6. protected-subnet-route.yml 78 | 7. slack-lambda.py.zip 79 | 3. Using console create a stack by choosing base.yml. 80 | 81 | #### Deployment Parameters: 82 | Refer : docs/NfwAlerts_Slack_Integration.docx 83 | -------------------------------------------------------------------------------- /NfwSlackIntegration/docs/NfwAlerts_Slack_Integration.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-network-firewall-automation-examples/ffb519a62508d00886deb839a07e70f628eccbb8/NfwSlackIntegration/docs/NfwAlerts_Slack_Integration.docx -------------------------------------------------------------------------------- /NfwSlackIntegration/src/base.yml: -------------------------------------------------------------------------------- 1 | # (c) 2021 Amazon Web Services, Inc. or its affiliates. All Rights Reserved. 2 | # This AWS Content is provided subject to the terms of the AWS Customer 3 | # Agreement available at https://aws.amazon.com/agreement/ or other written 4 | # agreement between Customer and Amazon Web Services, Inc. 5 | 6 | AWSTemplateFormatVersion: 2010-09-09 7 | Description: Launches a nested stack that creates the decentralized deployment model-1 8 | 9 | Metadata: 10 | 'AWS::CloudFormation::Interface': 11 | ParameterGroups: 12 | - Label: 13 | default: VPC Configuration 14 | Parameters: 15 | - pVpcName 16 | - pVpcCidr 17 | - pVpcInstanceTenancy 18 | - pAvailabilityZone1 19 | - pAvailabilityZone2 20 | - pNetworkFirewallSubnet1Cidr 21 | - pNetworkFirewallSubnet2Cidr 22 | - pProtectedSubnet1Cidr 23 | - pProtectedSubnet2Cidr 24 | - pS3BucketName 25 | - pS3KeyPrefix 26 | - Label: 27 | default: NFW-Slack Integration Configuration 28 | Parameters: 29 | # - pNFWArn 30 | # - pSlackSecretArn 31 | - pAWSSecretName4Slack 32 | - pSlackChannelName 33 | - pSlackUserName 34 | # - pSecretName 35 | - pSecretKey 36 | - pWebHookUrl 37 | # - plambdaSrcS3 38 | - pAlertS3Bucket 39 | - pSecretTagName 40 | - pSecretTagValue 41 | - pdestCidr 42 | - pdestCondition 43 | - psrcCidr 44 | - psrcCondition 45 | 46 | Parameters: 47 | pVpcName: 48 | Type: String 49 | Default: Inspection 50 | pVpcCidr: 51 | Type: String 52 | Default: 10.10.0.0/16 53 | pVpcInstanceTenancy: 54 | Type: String 55 | AllowedValues: [default, dedicated] 56 | Default: default 57 | pAvailabilityZone1: 58 | Type: String 59 | Default: us-east-2a 60 | pAvailabilityZone2: 61 | Type: String 62 | Default: us-east-2b 63 | pNetworkFirewallSubnet1Cidr: 64 | Type: String 65 | Default: 10.10.1.0/24 66 | pNetworkFirewallSubnet2Cidr: 67 | Type: String 68 | Default: 10.10.2.0/24 69 | pProtectedSubnet1Cidr: 70 | Type: String 71 | Default: 10.10.3.0/24 72 | pProtectedSubnet2Cidr: 73 | Type: String 74 | Default: 10.10.4.0/24 75 | pS3BucketName: 76 | Type: String 77 | Default: bucket-where-source-is - us-w2-yourname-lambda-functions 78 | pS3KeyPrefix: 79 | Type: String 80 | Default: aod-test 81 | # Parameters for Slack integration 82 | # pNFWArn: 83 | # Type: String 84 | # Default: "AWS Network firewall arn" 85 | # pSlackSecretArn: 86 | # Type: String 87 | # Default: "ar1" 88 | pAWSSecretName4Slack: 89 | Type: String 90 | Default: "SlackEnpoint-Cfn" 91 | pSlackChannelName: 92 | Type: String 93 | Default: somename-notifications 94 | pSlackUserName: 95 | Type: String 96 | Default: 'Slack User' 97 | # pSecretName: 98 | # Type: String 99 | # Default: 'SlackEnpoint-Cfn' 100 | pSecretKey: 101 | Type: String 102 | Default: webhookUrl 103 | pWebHookUrl: 104 | Type: String 105 | Default: "https://hooks.slack.com/services/2T21BH0T59T/499BB02N1J1/Tokenvaluegdjdjdjkdk" 106 | pSecretTagName: 107 | Type: String 108 | Default: AppName 109 | pSecretTagValue: 110 | Type: String 111 | Default: LambdaSlackIntegration 112 | # plambdaSrcS3: 113 | # Type: String 114 | # Default: bucket-where-source-is - us-w2-yourname-lambda-functions 115 | pAlertS3Bucket: 116 | Type: String 117 | Default: unique-bucket-name-please - us-w2-yourname-security-aod-alerts 118 | pdestCidr: 119 | Type: String 120 | Default: Destination Cider range filter to alert 121 | pdestCondition: 122 | Type: String 123 | AllowedValues: [include, exclude] 124 | Default: include 125 | psrcCidr: 126 | Type: String 127 | Default: Source Cider range filter to alert 128 | psrcCondition: 129 | Type: String 130 | AllowedValues: [include, exclude] 131 | Default: include 132 | Conditions: 133 | cIsGovCloud: 134 | !Or [ 135 | !Equals [!Ref AWS::Region, us-gov-west-1], 136 | !Equals [!Ref AWS::Region, us-gov-east-1], 137 | ] 138 | 139 | Resources: 140 | rFireWall: # Creates output for Listener ARN 141 | Type: AWS::CloudFormation::Stack 142 | Properties: 143 | TemplateURL: !Sub 144 | - https://${pS3BucketName}.${s3Region}.amazonaws.com/${pS3KeyPrefix}/${templateName} 145 | - s3Region: !If [cIsGovCloud, !Sub "s3-${AWS::Region}", s3] 146 | templateName: decentralized-deployment.yml 147 | Parameters: 148 | pVpcName: !Ref pVpcName 149 | pVpcCidr: !Ref pVpcCidr 150 | pVpcInstanceTenancy: !Ref pVpcInstanceTenancy 151 | pNetworkFirewallSubnetAz1: !Ref pAvailabilityZone1 152 | pNetworkFirewallSubnet1Cidr: !Ref pNetworkFirewallSubnet1Cidr 153 | pNetworkFirewallSubnetAz2: !Ref pAvailabilityZone2 154 | pNetworkFirewallSubnet2Cidr: !Ref pNetworkFirewallSubnet2Cidr 155 | pProtectedSubnetAz1: !Ref pAvailabilityZone1 156 | pProtectedSubnet1Cidr: !Ref pProtectedSubnet1Cidr 157 | pProtectedSubnetAz2: !Ref pAvailabilityZone2 158 | pProtectedSubnet2Cidr: !Ref pProtectedSubnet2Cidr 159 | 160 | rProtectedRoute: # adding an extra layer to pass Listener ARN as parameter 161 | Type: AWS::CloudFormation::Stack 162 | Properties: 163 | TemplateURL: !Sub 164 | - https://${pS3BucketName}.${s3Region}.amazonaws.com/${pS3KeyPrefix}/${templateName} 165 | - s3Region: !If [cIsGovCloud, !Sub "s3-${AWS::Region}", s3] 166 | templateName: protected-subnet-route.yml 167 | Parameters: 168 | pNetworkFirewallSubnetAz1: !Ref pAvailabilityZone1 169 | pNetworkFirewallSubnetAz2: !Ref pAvailabilityZone2 170 | pVpcEndpoints: !GetAtt rFireWall.Outputs.oNetworkFirewallEndpoint 171 | ProtectedSubnetRouteTable1: !GetAtt rFireWall.Outputs.oProtectedSubnetRt1Id 172 | ProtectedSubnetRouteTable2: !GetAtt rFireWall.Outputs.oProtectedSubnetRt2Id 173 | 174 | rInternetGatewayRouteTable: 175 | Type: AWS::CloudFormation::Stack 176 | Properties: 177 | TemplateURL: !Sub 178 | - https://${pS3BucketName}.${s3Region}.amazonaws.com/${pS3KeyPrefix}/${templateName} 179 | - s3Region: !If [cIsGovCloud, !Sub "s3-${AWS::Region}", s3] 180 | templateName: igw-ingress-route.yml 181 | Parameters: 182 | pVpc: !GetAtt rFireWall.Outputs.oVpcId 183 | pVpcName: !Ref pVpcName 184 | pNetworkFirewallSubnetAz1: !Ref pAvailabilityZone1 185 | pNetworkFirewallSubnetAz2: !Ref pAvailabilityZone2 186 | pProtectedSubnet1Cidr: !Ref pProtectedSubnet1Cidr 187 | pProtectedSubnet2Cidr: !Ref pProtectedSubnet2Cidr 188 | pVpcEndpoints: !GetAtt rFireWall.Outputs.oNetworkFirewallEndpoint 189 | pInternetGatewayId: !GetAtt rFireWall.Outputs.oInternetGatewayId 190 | 191 | rSlackNFWIntegration: # Creates alertin infra Alert bucket, s3 event config, reoles and permissions, lambda for slack. 192 | DependsOn: 193 | - rFireWall 194 | Type: AWS::CloudFormation::Stack 195 | Properties: 196 | TemplateURL: !Sub 197 | - https://${pS3BucketName}.${s3Region}.amazonaws.com/${pS3KeyPrefix}/${templateName} 198 | - s3Region: !If [cIsGovCloud, !Sub "s3-${AWS::Region}", s3] 199 | templateName: slackLambda.yml 200 | Parameters: 201 | pNFWArn: !GetAtt rFireWall.Outputs.oNetworkFirewallId 202 | # pSlackSecretArn: !Ref pSlackSecretArn 203 | pAWSSecretName4Slack: !Ref pAWSSecretName4Slack 204 | pSlackChannelName: !Ref pSlackChannelName 205 | pSlackUserName: !Ref pSlackUserName 206 | pSecretKey: !Ref pSecretKey 207 | pWebHookUrl: !Ref pWebHookUrl 208 | plambdaSrcS3: !Ref pS3BucketName 209 | plambdaSrcS3Prefix: !Ref pS3KeyPrefix 210 | pAlertS3Bucket: !Ref pAlertS3Bucket 211 | pSecretTagName: !Ref pSecretTagName 212 | pSecretTagValue: !Ref pSecretTagValue 213 | pdestCidr: !Ref pdestCidr 214 | pdestCondition: !Ref pdestCondition 215 | psrcCidr: !Ref psrcCidr 216 | psrcCondition: !Ref psrcCondition 217 | 218 | 219 | -------------------------------------------------------------------------------- /NfwSlackIntegration/src/decentralized-deployment.yml: -------------------------------------------------------------------------------- 1 | # (c) 2020 Amazon Web Services, Inc. or its affiliates. All Rights Reserved. 2 | # This AWS Content is provided subject to the terms of the AWS Customer 3 | # Agreement available at https://aws.amazon.com/agreement/ or other written 4 | # agreement between Customer and Amazon Web Services, Inc. 5 | # Author : vaidys@amazon.com 6 | AWSTemplateFormatVersion: 2010-09-09 7 | Description: Template creates the VPC with firewall, protected and private subnets along with AWS Network Firewall, NAT Gateways and necessary VPC routing. 8 | 9 | Metadata: 10 | 'AWS::CloudFormation::Interface': 11 | ParameterGroups: 12 | - Label: 13 | default: VPC Configuration 14 | Parameters: 15 | - pVpcName 16 | - pVpcCidr 17 | - pVpcInstanceTenancy 18 | - pNetworkFirewallSubnetAz1 19 | - pNetworkFirewallSubnet1Cidr 20 | - pNetworkFirewallSubnetAz2 21 | - pNetworkFirewallSubnet2Cidr 22 | - pProtectedSubnetAz1 23 | - pProtectedSubnet1Cidr 24 | - pProtectedSubnetAz2 25 | - pProtectedSubnet2Cidr 26 | 27 | Parameters: 28 | pVpcName: 29 | Type: String 30 | Default: Inspection 31 | pVpcCidr: 32 | Type: String 33 | pVpcInstanceTenancy: 34 | Type: String 35 | pNetworkFirewallSubnetAz1: 36 | Type: String 37 | pNetworkFirewallSubnet1Cidr: 38 | Type: String 39 | pNetworkFirewallSubnetAz2: 40 | Type: String 41 | pNetworkFirewallSubnet2Cidr: 42 | Type: String 43 | pProtectedSubnetAz1: 44 | Type: String 45 | pProtectedSubnet1Cidr: 46 | Type: String 47 | pProtectedSubnetAz2: 48 | Type: String 49 | pProtectedSubnet2Cidr: 50 | Type: String 51 | 52 | Resources: 53 | rVpc: 54 | Type: AWS::EC2::VPC 55 | Properties: 56 | CidrBlock: !Ref pVpcCidr 57 | EnableDnsHostnames: True 58 | EnableDnsSupport: True 59 | InstanceTenancy: !Ref pVpcInstanceTenancy 60 | Tags: 61 | - Key: Name 62 | Value: !Sub ${pVpcName}-vpc 63 | 64 | # NetworkFirewallSubnets 65 | rIgw: 66 | Type: AWS::EC2::InternetGateway 67 | DependsOn: rVpc 68 | Properties: 69 | Tags: 70 | - Key: Name 71 | Value: !Sub ${pVpcName}-igw 72 | rAttachIgw: 73 | Type: AWS::EC2::VPCGatewayAttachment 74 | Properties: 75 | VpcId: !Ref rVpc 76 | InternetGatewayId: !Ref rIgw 77 | 78 | rNetworkFirewallSubnetRt: 79 | Type: AWS::EC2::RouteTable 80 | Properties: 81 | VpcId: !Ref rVpc 82 | Tags: 83 | - Key: Name 84 | Value: !Sub ${pVpcName}-network-firewall-subnet-rt 85 | rNetworkFirewallSubnetRtDefaultRoute: 86 | Type: AWS::EC2::Route 87 | DependsOn: 88 | - rIgw 89 | - rAttachIgw 90 | Properties: 91 | RouteTableId: !Ref rNetworkFirewallSubnetRt 92 | DestinationCidrBlock: 0.0.0.0/0 93 | GatewayId: !Ref rIgw 94 | 95 | rNetworkFirewallSubnet1: 96 | Type: AWS::EC2::Subnet 97 | DependsOn: rVpc 98 | Properties: 99 | VpcId: !Ref rVpc 100 | CidrBlock: !Ref pNetworkFirewallSubnet1Cidr 101 | AvailabilityZone: !Ref pNetworkFirewallSubnetAz1 102 | Tags: 103 | - Key: Name 104 | Value: !Sub ${pVpcName}-network-firwall-subnet-1 105 | rNetworkFirewallSubnetRtAssociation1: 106 | Type: AWS::EC2::SubnetRouteTableAssociation 107 | Properties: 108 | SubnetId: !Ref rNetworkFirewallSubnet1 109 | RouteTableId: !Ref rNetworkFirewallSubnetRt 110 | rNetworkFirewallSubnet2: 111 | Type: AWS::EC2::Subnet 112 | DependsOn: rVpc 113 | Properties: 114 | VpcId: !Ref rVpc 115 | CidrBlock: !Ref pNetworkFirewallSubnet2Cidr 116 | AvailabilityZone: !Ref pNetworkFirewallSubnetAz2 117 | Tags: 118 | - Key: Name 119 | Value: !Sub ${pVpcName}-network-firwall-subnet-2 120 | rNetworkFirewallSubnetRtAssociation2: 121 | Type: AWS::EC2::SubnetRouteTableAssociation 122 | Properties: 123 | SubnetId: !Ref rNetworkFirewallSubnet2 124 | RouteTableId: !Ref rNetworkFirewallSubnetRt 125 | 126 | # Protected Subnets 127 | rProtectedSubnetRt1: 128 | Type: AWS::EC2::RouteTable 129 | Properties: 130 | VpcId: !Ref rVpc 131 | Tags: 132 | - Key: Name 133 | Value: !Sub ${pVpcName}-protected-subnet-rt-1 134 | rProtectedSubnetRt2: 135 | Type: AWS::EC2::RouteTable 136 | Properties: 137 | VpcId: !Ref rVpc 138 | Tags: 139 | - Key: Name 140 | Value: !Sub ${pVpcName}-protected-subnet-rt-2 141 | rProtectedSubnet1: 142 | Type: AWS::EC2::Subnet 143 | Properties: 144 | VpcId: !Ref rVpc 145 | CidrBlock: !Ref pProtectedSubnet1Cidr 146 | MapPublicIpOnLaunch: true 147 | AvailabilityZone: !Ref pProtectedSubnetAz1 148 | Tags: 149 | - Key: Name 150 | Value: !Sub ${pVpcName}-protected-subnet-1 151 | rProtectedSubnetRtAssociation1: 152 | Type: AWS::EC2::SubnetRouteTableAssociation 153 | Properties: 154 | SubnetId: !Ref rProtectedSubnet1 155 | RouteTableId: !Ref rProtectedSubnetRt1 156 | rProtectedSubnet2: 157 | Type: AWS::EC2::Subnet 158 | Properties: 159 | VpcId: !Ref rVpc 160 | CidrBlock: !Ref pProtectedSubnet2Cidr 161 | MapPublicIpOnLaunch: true 162 | AvailabilityZone: !Ref pProtectedSubnetAz2 163 | Tags: 164 | - Key: Name 165 | Value: !Sub ${pVpcName}-protected-subnet-2 166 | rProtectedSubnetRtAssociation2: 167 | Type: AWS::EC2::SubnetRouteTableAssociation 168 | Properties: 169 | SubnetId: !Ref rProtectedSubnet2 170 | RouteTableId: !Ref rProtectedSubnetRt2 171 | 172 | ### Network Firewall 173 | rNetworkFirewallPolicy: 174 | Type: 'AWS::NetworkFirewall::FirewallPolicy' 175 | Properties: 176 | FirewallPolicyName: AWS-Network-Firewall-Policy 177 | FirewallPolicy: 178 | StatelessDefaultActions: 179 | - 'aws:pass' 180 | StatelessFragmentDefaultActions: 181 | - 'aws:pass' 182 | rNetworkFirewall: 183 | Type: AWS::NetworkFirewall::Firewall 184 | Properties: 185 | FirewallName: AWS-Network-Firewall 186 | FirewallPolicyArn: !Ref rNetworkFirewallPolicy 187 | VpcId: !Ref rVpc 188 | SubnetMappings: 189 | - SubnetId: !Ref rNetworkFirewallSubnet1 190 | - SubnetId: !Ref rNetworkFirewallSubnet2 191 | Tags: 192 | - Key: Name 193 | Value: AWS-Network-Firewall 194 | Outputs: 195 | oVpcId: 196 | Value: !Ref rVpc 197 | Export: 198 | Name: ProtectedVpcId 199 | oNetworkFirewallId: 200 | Value: !Ref rNetworkFirewall 201 | Export: 202 | Name: NetworkFirewall 203 | oNetworkFirewallEndpoint: 204 | Description: Network firewall vpc endpoints 205 | Value: !Join [ ",",!GetAtt rNetworkFirewall.EndpointIds] 206 | Export: 207 | Name: NetworkFirewallVPCE 208 | oNetworkFirewallSubnet1Id: 209 | Value: !Ref rNetworkFirewallSubnet1 210 | Export: 211 | Name: NetworkFirewallSubnet1 212 | oNetworkFirewallSubnet2Id: 213 | Value: !Ref rNetworkFirewallSubnet2 214 | Export: 215 | Name: NetworkFirewallSubnet2 216 | oProtectedSubnet1Id: 217 | Value: !Ref rProtectedSubnet1 218 | Export: 219 | Name: ProtectedSubnet1 220 | oProtectedSubnet2Id: 221 | Value: !Ref rProtectedSubnet2 222 | Export: 223 | Name: ProtectedSubnet2 224 | oProtectedSubnetRt1Id: 225 | Value: !Ref rProtectedSubnetRt1 226 | Export: 227 | Name: ProtectedSubnetRouteTable1 228 | oProtectedSubnetRt2Id: 229 | Value: !Ref rProtectedSubnetRt2 230 | Export: 231 | Name: ProtectedSubnetRouteTable2 232 | oInternetGatewayId: 233 | Value: !Ref rIgw 234 | Export: 235 | Name: InternetGateway 236 | 237 | 238 | 239 | 240 | 241 | -------------------------------------------------------------------------------- /NfwSlackIntegration/src/igw-ingress-route.yml: -------------------------------------------------------------------------------- 1 | # (c) 2020 Amazon Web Services, Inc. or its affiliates. All Rights Reserved. 2 | # This AWS Content is provided subject to the terms of the AWS Customer 3 | # Agreement available at https://aws.amazon.com/agreement/ or other written 4 | # agreement between Customer and Amazon Web Services, Inc. 5 | # Author : vaidys@amazon.com 6 | 7 | AWSTemplateFormatVersion: 2010-09-09 8 | Description: Creates the route table for IGW with VPCE for ingress inspection 9 | Parameters: 10 | pVpc: 11 | Type: String 12 | pProtectedSubnet1Cidr: 13 | Type: String 14 | pProtectedSubnet2Cidr: 15 | Type: String 16 | pNetworkFirewallSubnetAz1: 17 | Type: String 18 | pNetworkFirewallSubnetAz2: 19 | Type: String 20 | pVpcName: 21 | Type: String 22 | pVpcEndpoints: 23 | Type: String 24 | pInternetGatewayId: 25 | Type: String 26 | 27 | 28 | #Conditions: 29 | # CAz1: 30 | # !Equals [ 31 | # !Select ["0",!Split [":",!Select ["0", !Split [",", !Ref pVpcEndpoints]]]], !Ref pNetworkFirewallSubnetAZ1 32 | # ] 33 | # CAz2: 34 | # !Equals [ 35 | # !Select ["0",!Split [":",!Select ["1", !Split [",", !Ref pVpcEndpoints]]]], !Ref pNetworkFirewallSubnetAZ2 36 | # ] 37 | 38 | Resources: 39 | rIgwRt: 40 | Type: AWS::EC2::RouteTable 41 | Properties: 42 | VpcId: !Ref pVpc 43 | Tags: 44 | - Key: Name 45 | Value: !Sub ${pVpcName}-igw-rt 46 | 47 | rVPCERoute1: 48 | Type: AWS::EC2::Route 49 | Properties: 50 | RouteTableId: !Ref rIgwRt 51 | DestinationCidrBlock: !Ref pProtectedSubnet1Cidr 52 | VpcEndpointId: !Select ["1",!Split [":",!Select ["0", !Split [",", !Ref pVpcEndpoints]]]] 53 | 54 | rVPCERoute2: 55 | Type: AWS::EC2::Route 56 | Properties: 57 | RouteTableId: !Ref rIgwRt 58 | DestinationCidrBlock: !Ref pProtectedSubnet2Cidr 59 | VpcEndpointId: !Select ["1",!Split [":",!Select ["1", !Split [",", !Ref pVpcEndpoints]]]] 60 | 61 | rIgwRtAssociation: 62 | Type: AWS::EC2::GatewayRouteTableAssociation 63 | Properties: 64 | GatewayId: !Ref pInternetGatewayId 65 | RouteTableId: !Ref rIgwRt 66 | 67 | Outputs: 68 | oIgwRt: 69 | Value: !Ref rIgwRt 70 | Export: 71 | Name: IgwRtId 72 | 73 | -------------------------------------------------------------------------------- /NfwSlackIntegration/src/protected-subnet-route.yml: -------------------------------------------------------------------------------- 1 | # (c) 2020 Amazon Web Services, Inc. or its affiliates. All Rights Reserved. 2 | # This AWS Content is provided subject to the terms of the AWS Customer 3 | # Agreement available at https://aws.amazon.com/agreement/ or other written 4 | # agreement between Customer and Amazon Web Services, Inc. 5 | # Author : vaidys@amazon.com 6 | 7 | AWSTemplateFormatVersion: 2010-09-09 8 | Description: Creates the default routes with Firewall VPCE as targets for protected subnets 9 | Parameters: 10 | pVpcEndpoints: 11 | Type: String 12 | pNetworkFirewallSubnetAz1: 13 | Type: String 14 | pNetworkFirewallSubnetAz2: 15 | Type: String 16 | ProtectedSubnetRouteTable1: 17 | Type: String 18 | ProtectedSubnetRouteTable2: 19 | Type: String 20 | 21 | Conditions: 22 | CAz1: 23 | !Equals [ 24 | !Select ["0",!Split [":",!Select ["0", !Split [",", !Ref pVpcEndpoints]]]], !Ref pNetworkFirewallSubnetAz1 25 | ] 26 | CAz2: 27 | !Equals [ 28 | !Select ["0",!Split [":",!Select ["1", !Split [",", !Ref pVpcEndpoints]]]], !Ref pNetworkFirewallSubnetAz2 29 | ] 30 | 31 | Resources: 32 | rProtectedRoute1: 33 | Condition: CAz1 34 | Type: AWS::EC2::Route 35 | Properties: 36 | RouteTableId: !Ref ProtectedSubnetRouteTable1 37 | DestinationCidrBlock: 0.0.0.0/0 38 | VpcEndpointId: !Select ["1",!Split [":",!Select ["0", !Split [",", !Ref pVpcEndpoints]]]] 39 | 40 | rProtectedRoute2: 41 | Condition: CAz2 42 | Type: AWS::EC2::Route 43 | Properties: 44 | RouteTableId: !Ref ProtectedSubnetRouteTable2 45 | DestinationCidrBlock: 0.0.0.0/0 46 | VpcEndpointId: !Select ["1",!Split [":",!Select ["1", !Split [",", !Ref pVpcEndpoints]]]] 47 | 48 | -------------------------------------------------------------------------------- /NfwSlackIntegration/src/slack-lambda.py: -------------------------------------------------------------------------------- 1 | # (c) 2021 Amazon Web Services, Inc. or its affiliates. All Rights Reserved. 2 | # This AWS Content is provided subject to the terms of the AWS Customer 3 | # Agreement available at https://aws.amazon.com/agreement/ or other written 4 | # agreement between Customer and Amazon Web Services, Inc. 5 | # Author : vramaam@amazon.com 6 | 7 | import json 8 | import boto3 9 | import os 10 | import logging 11 | import urllib.parse 12 | from io import BytesIO 13 | import gzip 14 | import base64 15 | import requests 16 | from botocore.exceptions import ClientError 17 | from ipaddress import ip_network, ip_address 18 | 19 | import urllib3 20 | import json 21 | http = urllib3.PoolManager() 22 | 23 | logger = logging.getLogger() 24 | logger.setLevel(logging.INFO) 25 | s3 = boto3.client('s3') 26 | 27 | 28 | def lambda_handler(event, context): 29 | 30 | logger.info('Raw Lambda event:') 31 | logger.info(event) 32 | 33 | slackEndpoint= get_secret () 34 | logger.debug("Retrived slackEndpoint from ASM") 35 | 36 | # Get the object from the event and show its content type 37 | bucket = event['Records'][0]['s3']['bucket']['name'] 38 | key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8') 39 | logger.info("bucket:"+bucket ) 40 | logger.info("key:"+key ) 41 | try: 42 | response = s3.get_object(Bucket=bucket, Key=key) 43 | print("CONTENT TYPE: " + response['ContentType']) 44 | logger.info('S3 object triggered this lambda:') 45 | logger.info(response['ContentType']) 46 | bytestream = BytesIO(response['Body'].read()) 47 | got_text = gzip.GzipFile(mode='rb', fileobj=bytestream).read().decode('utf-8') 48 | logger.info(got_text) 49 | post_slack_Message (event, context, slackEndpoint, convertText2Json(got_text)) 50 | return response['ContentType'] 51 | except Exception as e: 52 | print(e) 53 | print('Error getting object {} from bucket {}. Make sure they exist and your bucket is in the same region as this function.'.format(key, bucket)) 54 | raise e 55 | 56 | 57 | def convertText2Json (rawJasonFormattedText): 58 | nfwalerts = [] 59 | for line in rawJasonFormattedText.splitlines(): 60 | nfwAlert = json.loads(line) 61 | if (isPublishable(nfwAlert)): 62 | nfwalerts.append(nfwAlert) 63 | if not nfwalerts: 64 | return 65 | return json.dumps (nfwalerts) 66 | 67 | 68 | def isPublishable (nfwAlert): 69 | # Liberal filter 70 | srcCidr = os.environ['srcCidr'] 71 | destCidr = os.environ['destCidr'] 72 | 73 | srcCheckReqd = (len(srcCidr) != 0) 74 | destCheckReqd = (len(destCidr) != 0) 75 | 76 | if len(srcCidr) == 0 and len(destCidr)== 0 : 77 | return True 78 | 79 | srcCondition = os.environ['srcCondition'] 80 | destCondition = os.environ['destCondition'] 81 | 82 | srcIP = nfwAlert["event"]["src_ip"] 83 | destIP = nfwAlert["event"]["dest_ip"] 84 | 85 | includeSrc = True 86 | if (srcCheckReqd): 87 | net = ip_network(srcCidr) 88 | logger.info("Check-Src:"+str (ip_address(srcIP) in net) ) 89 | if (not (ip_address(srcIP) in net) and (srcCondition == "include")): 90 | includeSrc = False 91 | if ( (ip_address(srcIP) in net) and (srcCondition != "include")): 92 | logger.info("srcCheckReqd:"+str (srcCheckReqd) ) 93 | includeSrc = False 94 | 95 | includeDest = True 96 | if (destCheckReqd): 97 | net = ip_network(destCidr) 98 | logger.info("Check-Dest:"+str (ip_address(destIP) in net) ) 99 | if (not (ip_address(destIP) in net) and (destCondition == "include")): 100 | includeDest = False 101 | if ( (ip_address(destIP) in net) and (destCondition != "include")): 102 | includeDest = False 103 | 104 | return includeSrc or includeDest 105 | 106 | 107 | def get_secret(): 108 | 109 | secret_name = os.environ['slackSecretName'] 110 | region_name = os.environ['secretRegion'] 111 | 112 | # Create a Secrets Manager client 113 | session = boto3.session.Session() 114 | client = session.client( 115 | service_name='secretsmanager', 116 | region_name=region_name 117 | ) 118 | # In this sample we only handle the specific exceptions for the 'GetSecretValue' API. 119 | # See https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html 120 | # We rethrow the exception by default. 121 | try: 122 | get_secret_value_response = client.get_secret_value( 123 | SecretId=secret_name 124 | ) 125 | except ClientError as e: 126 | print (e) 127 | if e.response['Error']['Code'] == 'DecryptionFailureException': 128 | # Secrets Manager can't decrypt the protected secret text using the provided KMS key. 129 | # Deal with the exception here, and/or rethrow at your discretion. 130 | raise e 131 | elif e.response['Error']['Code'] == 'InternalServiceErrorException': 132 | # An error occurred on the server side. 133 | # Deal with the exception here, and/or rethrow at your discretion. 134 | raise e 135 | elif e.response['Error']['Code'] == 'InvalidParameterException': 136 | # You provided an invalid value for a parameter. 137 | # Deal with the exception here, and/or rethrow at your discretion. 138 | raise e 139 | elif e.response['Error']['Code'] == 'InvalidRequestException': 140 | # You provided a parameter value that is not valid for the current state of the resource. 141 | # Deal with the exception here, and/or rethrow at your discretion. 142 | raise e 143 | elif e.response['Error']['Code'] == 'ResourceNotFoundException': 144 | # We can't find the resource that you asked for. 145 | # Deal with the exception here, and/or rethrow at your discretion. 146 | raise e 147 | else: 148 | # Decrypts secret using the associated KMS CMK. 149 | # Depending on whether the secret is a string or binary, one of these fields will be populated. 150 | if 'SecretString' in get_secret_value_response: 151 | secret = get_secret_value_response['SecretString'] 152 | secret_dict = json.loads(secret) 153 | webhookUrl= secret_dict['webhookUrl'] 154 | return webhookUrl 155 | else: 156 | decoded_binary_secret = base64.b64decode(get_secret_value_response['SecretBinary']) 157 | return decoded_binary_secret 158 | 159 | 160 | 161 | def post_slack_Message (event, context, url, message): 162 | 163 | if not message : 164 | logger.info ("Slack Message body is empty, No message will be published.") 165 | return 166 | SLACK_CHANNEL = os.environ['slackChannel'] 167 | SLACK_USER = os.environ['slackUser'] 168 | msg = { 169 | "channel": SLACK_CHANNEL, 170 | "username": SLACK_USER, 171 | "text": message, 172 | "icon_emoji": "" 173 | } 174 | 175 | encoded_msg = json.dumps(msg).encode('utf-8') 176 | resp = http.request('POST',url, body=encoded_msg) 177 | print({ 178 | "message": message, 179 | "status_code": resp.status, 180 | "response": resp.data 181 | }) 182 | -------------------------------------------------------------------------------- /NfwSlackIntegration/src/slack-lambda.py.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-network-firewall-automation-examples/ffb519a62508d00886deb839a07e70f628eccbb8/NfwSlackIntegration/src/slack-lambda.py.zip -------------------------------------------------------------------------------- /NfwSlackIntegration/src/slackLambda.yml: -------------------------------------------------------------------------------- 1 | # (c) 2021 Amazon Web Services, Inc. or its affiliates. All Rights Reserved. 2 | # This AWS Content is provided subject to the terms of the AWS Customer 3 | # Agreement available at https://aws.amazon.com/agreement/ or other written 4 | # agreement between Customer and Amazon Web Services, Inc. 5 | # Author : vramaam@amazon.com 6 | 7 | AWSTemplateFormatVersion: 2010-09-09 8 | Description: Configures NFW alerts, creates S3 to store alerts, configure lambda events on s3, creates lambda function to push alerts to slack channel . 9 | Metadata: 10 | 'AWS::CloudFormation::Interface': 11 | ParameterGroups: 12 | - Label: 13 | default: Slack Integration Configuration 14 | Parameters: 15 | - pNFWArn 16 | # - pSlackSecretArn 17 | - pAWSSecretName4Slack 18 | - pSlackChannelName 19 | - pSlackUserName 20 | - pSecretKey 21 | - pWebHookUrl 22 | - plambdaSrcS3 23 | - plambdaSrcS3Prefix 24 | - pAlertS3Bucket 25 | - pSecretTagName 26 | - pSecretTagValue 27 | - pdestCidr 28 | - pdestCondition 29 | - psrcCidr 30 | - psrcCondition 31 | Parameters: 32 | pNFWArn: 33 | Type: String 34 | Default: "AWS Network firewall arn" 35 | # pSlackSecretArn: 36 | # Type: String 37 | # Default: "" 38 | pAWSSecretName4Slack: 39 | Type: String 40 | Default: "SlackEndpointUrl" 41 | pSlackChannelName: 42 | Type: String 43 | Default: iosengard-notifications 44 | pSlackUserName: 45 | Type: String 46 | Default: 'Venki Ram' 47 | pSecretKey: 48 | Type: String 49 | Default: webhookUrl 50 | pWebHookUrl: 51 | Type: String 52 | Default: "https://hooks.slack.com/services/abcd/EFGHH/9FkfhkfjhfjhV5NfN9N7WQeD" 53 | pSecretTagName: 54 | Type: String 55 | Default: AppName 56 | pSecretTagValue: 57 | Type: String 58 | Default: LambdaSlackIntegration 59 | plambdaSrcS3: 60 | Type: String 61 | Default: venki-lambda-functions 62 | plambdaSrcS3Prefix: 63 | Type: String 64 | Default: lambda-source 65 | pAlertS3Bucket: 66 | Type: String 67 | pdestCidr: 68 | Type: String 69 | Default: Destination Cider range filter to alert 70 | pdestCondition: 71 | Type: String 72 | AllowedValues: [include, exclude] 73 | Default: include 74 | psrcCidr: 75 | Type: String 76 | Default: Source Cider range filter to alert 77 | psrcCondition: 78 | Type: String 79 | AllowedValues: [include, exclude] 80 | Default: include 81 | Resources: 82 | rSlackSecret: 83 | Type: 'AWS::SecretsManager::Secret' 84 | Properties: 85 | Name: !Ref pAWSSecretName4Slack 86 | Description: To store slack endpoint url with access token. 87 | SecretString: !Join 88 | - '' 89 | - - '{"' 90 | - !Sub ${pSecretKey} 91 | - '":"' 92 | - !Sub ${pWebHookUrl} 93 | - '"}' 94 | Tags: 95 | - 96 | Key: !Sub ${pSecretTagName} 97 | Value: !Sub ${pSecretTagValue} 98 | rLambdaExecutionRole: 99 | Type: "AWS::IAM::Role" 100 | Properties: 101 | AssumeRolePolicyDocument: 102 | Version: "2012-10-17" 103 | Statement: 104 | - Effect: Allow 105 | Principal: 106 | Service: lambda.amazonaws.com 107 | Action: "sts:AssumeRole" 108 | Path: / 109 | RoleName: !Sub "${AWS::Region}-SlackIntegrationLambdaExecutionRole" 110 | Policies: 111 | - PolicyName: cloudwatchlogswrite-policy 112 | PolicyDocument: 113 | Version: '2012-10-17' 114 | Statement: 115 | - Action: 116 | - logs:CreateLogGroup 117 | - logs:CreateLogStream 118 | - logs:PutLogEvents 119 | Resource: "*" 120 | Effect: Allow 121 | - PolicyName: sectretManager-policy 122 | PolicyDocument: 123 | Version: '2012-10-17' 124 | Statement: 125 | - Action: "secretsmanager:*" 126 | Resource: !Ref rSlackSecret 127 | Effect: Allow 128 | - PolicyName: s3-bucket-access-policy 129 | PolicyDocument: 130 | Version: '2012-10-17' 131 | Statement: 132 | - Action: "s3:*" 133 | Resource: 134 | - !Sub "arn:aws:s3:::${pAlertS3Bucket}" 135 | - !Sub "arn:aws:s3:::${pAlertS3Bucket}/*" 136 | Effect: Allow 137 | rSlackIntegrationLambda: 138 | Type: 'AWS::Lambda::Function' 139 | DependsOn: 140 | - rLambdaExecutionRole 141 | Properties: 142 | FunctionName: SlackIntegration 143 | Handler: "slack-lambda.lambda_handler" 144 | Role: 145 | 'Fn::GetAtt': 146 | - rLambdaExecutionRole 147 | - Arn 148 | Code: 149 | S3Bucket: !Ref plambdaSrcS3 150 | S3Key: !Sub ${plambdaSrcS3Prefix}/slack-lambda.py.zip 151 | Runtime: python3.7 152 | MemorySize: 128 153 | Timeout: 300 154 | Environment: 155 | Variables: 156 | slackChannel: !Ref pSlackChannelName 157 | slackUser: !Ref pSlackUserName 158 | secretArn: !Ref rSlackSecret 159 | slackSecretName: !Ref pAWSSecretName4Slack 160 | secretRegion: !Sub "${AWS::Region}" 161 | destCidr: !Ref pdestCidr 162 | destCondition: !Ref pdestCondition 163 | srcCidr: !Ref psrcCidr 164 | srcCondition: !Ref psrcCondition 165 | rLambdaPermission: 166 | Type: AWS::Lambda::Permission 167 | DependsOn: rSlackIntegrationLambda 168 | Properties: 169 | Action: lambda:InvokeFunction 170 | FunctionName: !Ref rSlackIntegrationLambda 171 | Principal: s3.amazonaws.com 172 | SourceArn: !Sub "arn:aws:s3:::${pAlertS3Bucket}" 173 | SourceAccount: !Ref 'AWS::AccountId' 174 | rNfwAlertBucket: 175 | Type: AWS::S3::Bucket 176 | DependsOn: rLambdaPermission 177 | Properties: 178 | BucketName: !Ref pAlertS3Bucket 179 | BucketEncryption: 180 | ServerSideEncryptionConfiguration: 181 | - ServerSideEncryptionByDefault: 182 | SSEAlgorithm: 'AES256' 183 | NotificationConfiguration: 184 | LambdaConfigurations: 185 | - Event: 's3:ObjectCreated:Put' 186 | Filter: 187 | S3Key: 188 | Rules: 189 | - Name: suffix 190 | Value: gz 191 | Function: !GetAtt [ rSlackIntegrationLambda, Arn] 192 | rNfwAlertBucketPolicy: 193 | DependsOn: 194 | - rNfwAlertBucket 195 | Type: 'AWS::S3::BucketPolicy' 196 | Properties: 197 | Bucket: !Ref rNfwAlertBucket 198 | PolicyDocument: 199 | Statement: 200 | - Action: 's3:PutObject' 201 | Condition: 202 | StringEquals: 203 | s3:x-amz-acl: bucket-owner-full-control 204 | Effect: Allow 205 | Principal: 206 | Service: delivery.logs.amazonaws.com 207 | Resource: !Sub 'arn:${AWS::Partition}:s3:::${pAlertS3Bucket}/*' 208 | Sid: AWSLogDeliveryWrite 209 | - Action: 's3:GetBucketAcl' 210 | Effect: Allow 211 | Principal: 212 | Service: delivery.logs.amazonaws.com 213 | Resource: !Sub 'arn:${AWS::Partition}:s3:::${pAlertS3Bucket}' 214 | Sid: AWSLogDeliveryAclCheck 215 | - Action: 's3:*' 216 | Condition: 217 | Bool: 218 | "aws:SecureTransport": "false" 219 | Effect: Deny 220 | Principal: "*" 221 | Resource: 222 | - !Sub 'arn:${AWS::Partition}:s3:::${pAlertS3Bucket}' 223 | - !Sub 'arn:${AWS::Partition}:s3:::${pAlertS3Bucket}/*' 224 | Sid: AWSLogDeliveryEnforceTLS 225 | rNFWLoggingConfiguration: 226 | DependsOn: 227 | - rNfwAlertBucketPolicy 228 | Type: 'AWS::NetworkFirewall::LoggingConfiguration' 229 | Properties: 230 | FirewallArn: !Ref pNFWArn 231 | LoggingConfiguration: 232 | LogDestinationConfigs: 233 | - LogType: ALERT 234 | LogDestinationType: S3 235 | LogDestination: 236 | bucketName: !Ref pAlertS3Bucket 237 | prefix: nfwAlerts 238 | Outputs: 239 | oAlertBucket: 240 | Value: !Ref rNfwAlertBucket 241 | Export: 242 | Name: AlertBucket -------------------------------------------------------------------------------- /NfwSlackIntegration/test/TestSteps.txt: -------------------------------------------------------------------------------- 1 | 1. Create an Ec2 instance in one of the protected subnet 2 | 2. Use the following user data to install a web server on the ec2 instance: 3 | #!/bin/bash 4 | yum install httpd -y 5 | systemctl start httpd 6 | systemctl stop firewalld 7 | cd /var/www/html 8 | echo "Hello!! this is Venki's test installation, 200 OK" > index.html 9 | 10 | 11 | 3. Create the following NFW rules: 12 | stateless rule: 13 | Source: 0.0.0.0/0 14 | Destination 10.0.3.65/32 ( Private IP of the Ec2) 15 | Action: Forward 16 | 17 | StatefUl rule: 18 | Protocol: HTTP 19 | Source ip/port: Any / Any 20 | Destination ip/port: Any /Any 21 | 22 | 23 | Test end point: cd ,, -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # anf-samples 2 | This collection of Amazon Network Firewall templates, demonstrates automated approaches involving an AWS Network Firewall Rule Group, paired with an AWS Lambda function to perform steps like, parsing an external source, and keeping the Rule Group automatically up to date. 3 | 4 | ## File Structure 5 | This project consists of CloudFormation Templates and snippets of source code that demonstrate the functional aspects of the approach. 6 | 7 | ## Examples 8 | 9 | #### Abuse.CH 10 | * Examples of using URLs hosting IP addresses, hostnames, or Suricata rules from https://abuse.ch 11 | 12 | #### AllowListGenerator 13 | * Example of automating the creation of an allow list Suricata stateful rule group for AWS Network Firewall based on HTTP/TLS traffic logs, also provides deep visibility into HTTP/TLS traffic patterns. 14 | 15 | #### Alphasoc 16 | * Examples of blocking using encrypted DNS hosts from https://feeds.alphasoc.net/encrypted_dns.txt 17 | 18 | #### Emerging Threats 19 | * Example of using URLs hosting IP addresses, hostnames, or Suricata rules from https://rules.emergingthreats.net/fwrules/emerging-Block-IPs.txt 20 | 21 | #### Linode Addresses 22 | * Example of using a published list of Linode addresses to drop or alert on the resulting traffic (https://geoip.linode.com/) 23 | 24 | #### NfwSlackIntegration 25 | * Example of propagating the alerts generated by the AWS Firewall Manager to a configurable Slack channel 26 | 27 | #### SFTP-FQDN 28 | * Example where an Domain List is used to resolve IPs for the domain and block the associated IP addresses as well 29 | 30 | #### SpamHaus 31 | * Examples of using URLs hosting IP addresses, hostnames, or Suricata rules from https://www.spamhaus.org 32 | 33 | #### TLS Fingerprint 34 | * An example that uses an Amazon Network Firewall Domain List, partnered with a stateful Suricata rule group to fetch and enforce the TLS Fingerprint of the domain 35 | 36 | #### TOR Project 37 | * Examples of using URLs hosting IP addresses, hostnames, or Suricata rules from https://check.torproject.org/exit-addresses 38 | 39 | ## Architecture Diagram 40 | 41 | 42 | ## Getting Started 43 | 44 | #### 01. Clone the repository 45 | * Clone the repository: 46 | 47 | #### 02. Deploy the AWS Network Firewall Rule Group Automations solution: 48 | * Using AWS CloudFormation, create a Stack from the templates available in the deploment folders from where you cloned the deployment assets. 49 | 50 | *** 51 | 52 | ## License Summary 53 | 54 | This sample code is made available under the MIT-0 license. See the LICENSE file. -------------------------------------------------------------------------------- /SFTP-FQDN/src/SFTP-FQDN.js: -------------------------------------------------------------------------------- 1 | const AWS = require("aws-sdk"); 2 | const dnsPromises = require('dns').promises; 3 | 4 | const networkfirewall = new AWS.NetworkFirewall(); 5 | 6 | const getAddresses = async function(fqdn){ 7 | let res = await dnsPromises.resolve4(fqdn); 8 | return res.map((line)=> {return line + "/32"}); 9 | }; 10 | 11 | const updateRules = async function (ruleGroup, fqdn, addresses) { 12 | let params = ruleGroup; 13 | params.RuleGroupName = params.RuleGroupResponse.RuleGroupName; 14 | params.Description = params.RuleGroupResponse.Description; 15 | params.Type = params.RuleGroupResponse.Type; 16 | delete params.Capacity; 17 | delete params.RuleGroupResponse; 18 | 19 | let rulesString = "# Last autofetched by Lambda: " + new Date().toUTCString() + "\n"; 20 | rulesString += "# Fetched addresses for: " + fqdn + " stored as Variable: $SFTPFQDN\n"; 21 | addresses.forEach (address => { 22 | rulesString += "# " + address + "\n"; 23 | }); 24 | rulesString += 'pass tcp any any -> $SFTPFQDN 22 (msg:"Allow access to ' + fqdn + '"; sid:1001;)'; 25 | 26 | params.RuleGroup.RulesSource.RulesString = rulesString; 27 | params.RuleGroup.RuleVariables.IPSets.SFTPFQDN.Definition = addresses; 28 | 29 | console.log("Updating rules..."); 30 | let res = await networkfirewall.updateRuleGroup(params).promise(); 31 | if (res) { 32 | console.log("Updated '" + params.RuleGroupName + "'."); 33 | } else { 34 | console.log("Error updating the rules for '" + params.RuleGroupName + "'..."); 35 | } 36 | 37 | return; 38 | }; 39 | 40 | exports.handler = async (event, context) => { 41 | var rg = {Type: "STATEFUL", RuleGroupArn: ''}; 42 | const fqdn = ""; 43 | 44 | let addresses = await getAddresses(fqdn); 45 | if (addresses) { 46 | console.log("Searching Rule Groups for " + rg.RuleGroupArn + "..."); 47 | let res = await networkfirewall.describeRuleGroup(rg).promise(); 48 | if (res.RuleGroupResponse) { 49 | console.log("Found matching Rule Group..."); 50 | await updateRules(res, fqdn, addresses); 51 | 52 | } else { 53 | console.log("ERROR: No matching Rule Group found..."); 54 | } 55 | } else { 56 | console.log("Could not resolve addresses for fqdn: " + fqdn); 57 | } 58 | 59 | return; 60 | }; 61 | -------------------------------------------------------------------------------- /SFTP-FQDN/templates/SFTP-FQDN.yaml: -------------------------------------------------------------------------------- 1 | Parameters: 2 | RuleGroupName: 3 | Type: String 4 | Default: 'AutoUpdating-SFTP-FQDN' 5 | RuleGroupFQDN: 6 | Type: String 7 | Default: www.aws.com 8 | Description: "Type the FQDN of the SFTP endpoint" 9 | MinLength: 6 10 | MaxLength: 255 11 | AllowedPattern: ^(?!:\/\/)(?=.{1,255}$)((.{1,63}\.){1,127}(?![0-9]*$)[a-z0-9-]+\.?)$ 12 | ConstraintDescription: must contain a valid Fully Qualified Domain Name (FQDN) 13 | 14 | Resources: 15 | StatefulRulegroup: 16 | Type: 'AWS::NetworkFirewall::RuleGroup' 17 | Properties: 18 | Capacity: 100 19 | RuleGroupName: !Sub "${AWS::StackName}-${RuleGroupName}" 20 | Description: This Rule group is used to restrict SFTP access to an FQDN by IP addresses fetched via Lambda 21 | Type: STATEFUL 22 | RuleGroup: 23 | RuleVariables: 24 | IPSets: 25 | "SFTPFQDN": 26 | Definition: ["127.0.0.1"] 27 | RulesSource: 28 | RulesString: !Sub | 29 | # This will be updated automatically by the Lambda 30 | Tags: 31 | - Key: "ProjectName" 32 | Value: "SFTP-FQDN" 33 | LambdaExecutionRole: 34 | Type: AWS::IAM::Role 35 | Properties: 36 | AssumeRolePolicyDocument: 37 | Version: '2012-10-17' 38 | Statement: 39 | - Effect: Allow 40 | Principal: 41 | Service: 42 | - lambda.amazonaws.com 43 | Action: 44 | - sts:AssumeRole 45 | Path: "/" 46 | Policies: 47 | - PolicyName: LambdaLogs 48 | PolicyDocument: 49 | Version: '2012-10-17' 50 | Statement: 51 | - Effect: Allow 52 | Action: 53 | - 'logs:CreateLogStream' 54 | - 'logs:PutLogEvents' 55 | Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*' 56 | - PolicyName: NetworkFirewall 57 | PolicyDocument: 58 | Version: '2012-10-17' 59 | Statement: 60 | - Effect: Allow 61 | Action: 62 | - 'network-firewall:*' 63 | Resource: 64 | - !GetAtt StatefulRulegroup.RuleGroupArn 65 | Tags: 66 | - Key: "ProjectName" 67 | Value: "SFTP-FQDN" 68 | LambdaLogGroup: 69 | Type: AWS::Logs::LogGroup 70 | Properties: 71 | LogGroupName: !Sub "/aws/lambda/${LambdaFunction}" 72 | DeletionPolicy: Retain 73 | ScheduledRule: 74 | Type: AWS::Events::Rule 75 | Properties: 76 | Description: "SFTP-FQDNTrigger" 77 | ScheduleExpression: "cron(0/5 * * * ? *)" 78 | State: "ENABLED" 79 | Targets: 80 | - 81 | Arn: 82 | Fn::GetAtt: 83 | - "LambdaFunction" 84 | - "Arn" 85 | Id: "TargetFunctionV1" 86 | PermissionForEventsToInvokeLambda: 87 | Type: AWS::Lambda::Permission 88 | Properties: 89 | FunctionName: !Ref "LambdaFunction" 90 | Action: "lambda:InvokeFunction" 91 | Principal: "events.amazonaws.com" 92 | SourceArn: 93 | Fn::GetAtt: 94 | - "ScheduledRule" 95 | - "Arn" 96 | LambdaInvoke: 97 | Type: AWS::CloudFormation::CustomResource 98 | Version: "1.0" 99 | Properties: 100 | ServiceToken: !GetAtt LambdaFunction.Arn 101 | LambdaFunction: 102 | Type: AWS::Lambda::Function 103 | Properties: 104 | Role: !GetAtt LambdaExecutionRole.Arn 105 | Runtime: nodejs16.x 106 | Handler: index.handler 107 | Timeout: 60 108 | Description: Used to fetch IPs for the given FQDN and update the associated RuleGroup 109 | Tags: 110 | - 111 | Key: "ProjectName" 112 | Value: "SFTP-FQDN" 113 | Code: 114 | ZipFile: !Sub | 115 | const AWS = require("aws-sdk"); 116 | var response = require('cfn-response'); 117 | const dnsPromises = require('dns').promises; 118 | 119 | const networkfirewall = new AWS.NetworkFirewall(); 120 | 121 | const getAddresses = async function(fqdn){ 122 | let res = await dnsPromises.resolve4(fqdn); 123 | return res.map((line)=> {return line + "/32"}); 124 | }; 125 | 126 | const updateRules = async function (ruleGroup, fqdn, addresses) { 127 | let params = ruleGroup; 128 | params.RuleGroupName = params.RuleGroupResponse.RuleGroupName; 129 | params.Description = params.RuleGroupResponse.Description; 130 | params.Type = params.RuleGroupResponse.Type; 131 | delete params.Capacity; 132 | delete params.RuleGroupResponse; 133 | 134 | let rulesString = "# Last autofetched by Lambda: " + new Date().toUTCString() + "\n"; 135 | rulesString += "# Fetched addresses for: " + fqdn + " stored as Variable: $SFTPFQDN\n"; 136 | addresses.forEach (address => { 137 | rulesString += "# " + address + "\n"; 138 | }); 139 | rulesString += 'pass tcp any any -> $SFTPFQDN 22 (msg:"Allow access to ' + fqdn + '"; sid:1001;)'; 140 | 141 | params.RuleGroup.RulesSource.RulesString = rulesString; 142 | params.RuleGroup.RuleVariables.IPSets.SFTPFQDN.Definition = addresses; 143 | 144 | console.log("Updating rules..."); 145 | let res = await networkfirewall.updateRuleGroup(params).promise(); 146 | if (res) { 147 | console.log("Updated '" + params.RuleGroupName + "'."); 148 | } else { 149 | console.log("Error updating the rules for '" + params.RuleGroupName + "'..."); 150 | } 151 | 152 | return; 153 | }; 154 | 155 | exports.handler = async (event, context) => { 156 | if (event.RequestType == "Delete") { 157 | await response.send(event, context, "SUCCESS"); 158 | return; 159 | } 160 | 161 | var rg = {Type: "STATEFUL", RuleGroupArn: '${StatefulRulegroup.RuleGroupArn}'}; 162 | const fqdn = "${RuleGroupFQDN}"; 163 | 164 | let addresses = await getAddresses(fqdn); 165 | if (addresses) { 166 | console.log("Searching Rule Groups for " + rg.RuleGroupArn + "..."); 167 | let res = await networkfirewall.describeRuleGroup(rg).promise(); 168 | if (res.RuleGroupResponse) { 169 | console.log("Found matching Rule Group..."); 170 | await updateRules(res, fqdn, addresses); 171 | if (event.ResponseURL) await response.send(event, context, response.SUCCESS); 172 | } else { 173 | console.log("ERROR: No matching Rule Group found..."); 174 | if (event.ResponseURL) await response.send(event, context, response.FAILED); 175 | } 176 | } else { 177 | console.log("Could not resolve addresses for fqdn: " + fqdn); 178 | if (event.ResponseURL) await response.send(event, context, response.FAILED); 179 | } 180 | 181 | return; 182 | }; 183 | -------------------------------------------------------------------------------- /SpamHaus/cfn-templates/SpamHausDropIPFiltering.yaml: -------------------------------------------------------------------------------- 1 | Parameters: 2 | RuleGroupName: 3 | Type: String 4 | Default: 'SpamHausIPList' 5 | RuleGroupAction: 6 | Type: String 7 | Description: "Used to define the action to take on a matching rule if found" 8 | Default : 'drop' 9 | AllowedValues: 10 | - 'alert' 11 | - 'drop' 12 | 13 | Resources: 14 | StatefulRulegroup: 15 | Type: 'AWS::NetworkFirewall::RuleGroup' 16 | Properties: 17 | RuleGroupName: !Sub "${AWS::StackName}-${RuleGroupName}-${RuleGroupAction}" 18 | Type: STATEFUL 19 | RuleGroup: 20 | RulesSource: 21 | RulesString: '#This will be updated via the Lambda function' 22 | Capacity: 3000 23 | Description: >- 24 | Used to track a list of Emerging IP Threats from 25 | https://www.spamhaus.org/drop/drop.txt 26 | Tags: 27 | - Key: "ProjectName" 28 | Value: "SpamHausIPFiltering" 29 | LambdaExecutionRole: 30 | Type: AWS::IAM::Role 31 | Properties: 32 | AssumeRolePolicyDocument: 33 | Version: '2012-10-17' 34 | Statement: 35 | - Effect: Allow 36 | Principal: 37 | Service: 38 | - lambda.amazonaws.com 39 | Action: 40 | - sts:AssumeRole 41 | Path: "/" 42 | Policies: 43 | - PolicyName: LambdaLogs 44 | PolicyDocument: 45 | Version: '2012-10-17' 46 | Statement: 47 | - Effect: Allow 48 | Action: 49 | - 'logs:CreateLogStream' 50 | - 'logs:PutLogEvents' 51 | Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*' 52 | - PolicyName: NetworkFirewall 53 | PolicyDocument: 54 | Version: '2012-10-17' 55 | Statement: 56 | - Effect: Allow 57 | Action: 58 | - 'network-firewall:*' 59 | Resource: 60 | - !GetAtt StatefulRulegroup.RuleGroupArn 61 | Tags: 62 | - Key: "ProjectName" 63 | Value: "SpamHausIPFiltering" 64 | LambdaLogGroup: 65 | Type: AWS::Logs::LogGroup 66 | Properties: 67 | LogGroupName: !Sub "/aws/lambda/${LambdaFunction}" 68 | RetentionInDays: 14 69 | DeletionPolicy: Retain 70 | ScheduledRule: 71 | Type: AWS::Events::Rule 72 | Properties: 73 | Description: "SpamHausDropDailyTrigger" 74 | ScheduleExpression: "cron(0 0 * * ? *)" 75 | State: "ENABLED" 76 | Targets: 77 | - Arn: 78 | Fn::GetAtt: 79 | - "LambdaFunction" 80 | - "Arn" 81 | Id: "TargetFunctionV1" 82 | PermissionForEventsToInvokeLambda: 83 | Type: AWS::Lambda::Permission 84 | Properties: 85 | FunctionName: !Ref "LambdaFunction" 86 | Action: "lambda:InvokeFunction" 87 | Principal: "events.amazonaws.com" 88 | SourceArn: 89 | Fn::GetAtt: 90 | - "ScheduledRule" 91 | - "Arn" 92 | LambdaInvoke: 93 | Type: AWS::CloudFormation::CustomResource 94 | Version: "1.0" 95 | Properties: 96 | ServiceToken: !GetAtt LambdaFunction.Arn 97 | LambdaFunction: 98 | Type: AWS::Lambda::Function 99 | Properties: 100 | Role: !GetAtt LambdaExecutionRole.Arn 101 | Runtime: python3.12 102 | Handler: index.lambda_handler 103 | Timeout: 60 104 | Description: Used to fetch data from the Emerging Threats IP list and update the associated RuleGroup 105 | Tags: 106 | - Key: "ProjectName" 107 | Value: "SpamHausIPFiltering" 108 | Code: 109 | ZipFile: !Sub | 110 | import json 111 | import urllib 112 | import boto3 113 | from datetime import datetime 114 | import cfnresponse 115 | 116 | # Constants 117 | SPAM_HAUS_DROP_URL = "https://www.spamhaus.org/drop/drop.txt" 118 | RULE_GROUP_ARN = '${StatefulRulegroup.RuleGroupArn}' 119 | 120 | # Initialize AWS clients 121 | networkfirewall = boto3.client('network-firewall') 122 | 123 | def fetch_ips(): 124 | print("Fetching the list of IP addresses...") 125 | try: 126 | with urllib.request.urlopen(SPAM_HAUS_DROP_URL) as response: 127 | data = response.read().decode('utf-8') 128 | 129 | list_of_ips = [line.split(" ;")[0] for line in data.splitlines() if line.strip() and line[0].isdigit()] 130 | print(f"Fetched {len(list_of_ips)} IP addresses...") 131 | return list_of_ips 132 | except urllib.error.URLError as e: 133 | print(f"URL Error: {e.reason}") 134 | except urllib.error.HTTPError as e: 135 | print(f"HTTP Error: {e.code} - {e.reason}") 136 | except Exception as e: 137 | print(f"Unexpected error fetching IP addresses: {str(e)}") 138 | return [] 139 | 140 | def update_rules(rule_group): 141 | params = rule_group.copy() 142 | params.pop('Capacity', None) 143 | params['RuleGroupName'] = params['RuleGroupResponse']['RuleGroupName'] 144 | params['Type'] = params['RuleGroupResponse']['Type'] 145 | # Remove keys that can't be updated 146 | params.pop('ResponseMetadata', None) 147 | params.pop('Capacity', None) 148 | params.pop('RuleGroupResponse', None) 149 | 150 | print("Updating rules...") 151 | try: 152 | res = networkfirewall.update_rule_group(**params) 153 | if res: 154 | print(f"Updated '{params['RuleGroupName']}'.") 155 | return True 156 | else: 157 | print(f"Error updating the rules for '{params['RuleGroupName']}'...") 158 | return False 159 | except Exception as e: 160 | print(f"Error updating rules: {str(e)}") 161 | return False 162 | 163 | def create_rules(rule_group, rule_type): 164 | list_of_ips = fetch_ips() 165 | 166 | if not list_of_ips: 167 | print("No IP addresses fetched. Aborting rule creation.") 168 | return False 169 | 170 | rules = [] 171 | rules.append(f"# Last updated: {datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT')}") 172 | rules.append(f"# Using a list of {len(list_of_ips)} IP addresses") 173 | 174 | for index, ip in enumerate(list_of_ips): 175 | rules.append(f"{rule_type} ip {ip} any -> any any (msg:\"{rule_type} emerging threats traffic from {ip}\"; rev:1; sid:55{index};)") 176 | rules.append(f"{rule_type} ip any any -> {ip} any (msg:\"{rule_type} emerging threats traffic to {ip}\"; rev:1; sid:66{index};)") 177 | 178 | rule_group['RuleGroup']['RulesSource']['RulesString'] = '\n'.join(rules) 179 | return update_rules(rule_group) 180 | 181 | def lambda_handler(event, context): 182 | #handle delete event 183 | if event['RequestType'] == 'Delete': 184 | cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, "Successfully processed Delete request") 185 | return 186 | try: 187 | params = {"Type": "STATEFUL", "RuleGroupArn": RULE_GROUP_ARN} 188 | 189 | print("Searching Rule Groups for 'SpamHausIPList'...") 190 | res = networkfirewall.describe_rule_group(**params) 191 | 192 | if 'RuleGroupResponse' in res: 193 | print("Found Rule Group...") 194 | success = create_rules(res, "drop") 195 | if success: 196 | cfnresponse.send(event, context, cfnresponse.SUCCESS, {"Message": "Successfully updated Network Firewall rules with SpamHaus IP list"}, "Rule Group update successful") 197 | else: 198 | cfnresponse.send(event, context, cfnresponse.FAILED, {"Message": "Failed to update Network Firewall rules"}, "Rule Group update failed") 199 | else: 200 | print("ERROR: No matching Rule Group found...") 201 | cfnresponse.send(event, context, cfnresponse.FAILED, {"Message": "No matching Rule Group found for the provided ARN"}, "Rule Group not found") 202 | except Exception as e: 203 | print(f"Error in lambda_handler: {str(e)}") 204 | cfnresponse.send(event, context, cfnresponse.FAILED, {"Message": f"An error occurred: {str(e)}"}, "Lambda execution failed") 205 | 206 | return { 207 | 'statusCode': 200, 208 | 'body': json.dumps('Function executed successfully') 209 | } -------------------------------------------------------------------------------- /SpamHaus/cfn-templates/SpamHausEDropIPFiltering-Deprecated.yaml: -------------------------------------------------------------------------------- 1 | # Deprecated as the edrop.txt file has been merged with the drop.txt file hosted at Spamhaus -------------------------------------------------------------------------------- /SpamHaus/src/SpamHausDropIPFiltering.py: -------------------------------------------------------------------------------- 1 | import json 2 | import urllib 3 | import boto3 4 | from datetime import datetime 5 | 6 | # Constants 7 | SPAM_HAUS_DROP_URL = "https://www.spamhaus.org/drop/drop.txt" 8 | RULE_GROUP_ARN = '' 9 | 10 | # Initialize AWS clients 11 | networkfirewall = boto3.client('network-firewall') 12 | 13 | def fetch_ips(): 14 | print("Fetching the list of IP addresses...") 15 | try: 16 | with urllib.request.urlopen(SPAM_HAUS_DROP_URL) as response: 17 | data = response.read().decode('utf-8') 18 | 19 | list_of_ips = [line.split(" ;")[0] for line in data.splitlines() if line.strip() and line[0].isdigit()] 20 | print(f"Fetched {len(list_of_ips)} IP addresses...") 21 | return list_of_ips 22 | except urllib.error.URLError as e: 23 | print(f"URL Error: {e.reason}") 24 | except urllib.error.HTTPError as e: 25 | print(f"HTTP Error: {e.code} - {e.reason}") 26 | except Exception as e: 27 | print(f"Unexpected error fetching IP addresses: {str(e)}") 28 | return [] 29 | 30 | def update_rules(rule_group): 31 | params = rule_group.copy() 32 | params.pop('Capacity', None) 33 | params['RuleGroupName'] = params['RuleGroupResponse']['RuleGroupName'] 34 | params['Type'] = params['RuleGroupResponse']['Type'] 35 | # Remove keys that can't be updated 36 | params.pop('ResponseMetadata', None) 37 | params.pop('Capacity', None) 38 | params.pop('RuleGroupResponse', None) 39 | 40 | print("Updating rules...") 41 | try: 42 | res = networkfirewall.update_rule_group(**params) 43 | if res: 44 | print(f"Updated '{params['RuleGroupName']}'.") 45 | return True 46 | else: 47 | print(f"Error updating the rules for '{params['RuleGroupName']}'...") 48 | return False 49 | except Exception as e: 50 | print(f"Error updating rules: {str(e)}") 51 | return False 52 | 53 | def create_rules(rule_group, rule_type): 54 | list_of_ips = fetch_ips() 55 | 56 | if not list_of_ips: 57 | print("No IP addresses fetched. Aborting rule creation.") 58 | return False 59 | 60 | rules = [] 61 | rules.append(f"# Last updated: {datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT')}") 62 | rules.append(f"# Using a list of {len(list_of_ips)} IP addresses") 63 | 64 | for index, ip in enumerate(list_of_ips): 65 | rules.append(f"{rule_type} ip {ip} any -> any any (msg:\"{rule_type} emerging threats traffic from {ip}\"; rev:1; sid:55{index};)") 66 | rules.append(f"{rule_type} ip any any -> {ip} any (msg:\"{rule_type} emerging threats traffic to {ip}\"; rev:1; sid:66{index};)") 67 | 68 | rule_group['RuleGroup']['RulesSource']['RulesString'] = '\n'.join(rules) 69 | return update_rules(rule_group) 70 | 71 | def lambda_handler(event, context): 72 | try: 73 | params = {"Type": "STATEFUL", "RuleGroupArn": RULE_GROUP_ARN} 74 | 75 | print("Searching Rule Groups for 'SpamHausIPList'...") 76 | res = networkfirewall.describe_rule_group(**params) 77 | 78 | if 'RuleGroupResponse' in res: 79 | print("Found Rule Group...") 80 | success = create_rules(res, "drop") 81 | if success: 82 | print("Rule Group update successful") 83 | else: 84 | print("Rule Group update failed") 85 | else: 86 | print("ERROR: No matching Rule Group found...") 87 | except Exception as e: 88 | print(f"An error occurred: {str(e)}") 89 | 90 | return { 91 | 'statusCode': 200, 92 | 'body': json.dumps('Function executed successfully') 93 | } -------------------------------------------------------------------------------- /SpamHaus/src/SpamHauseDropIPFiltering-Deprecated.js: -------------------------------------------------------------------------------- 1 | // DEPRECATED - but left for historical context 2 | // This was based on Node 14 and will not work with Node 16 or greater 3 | // Node 16 or great also requires v3 of the AWS SDK 4 | 5 | var AWS = require("aws-sdk"); 6 | var https = require("https"); 7 | var listOfIps = []; 8 | 9 | const SpamHausDropUrl = "https://www.spamhaus.org/drop/drop.txt"; 10 | 11 | const networkfirewall = new AWS.NetworkFirewall(); 12 | 13 | function fetchIPs() { 14 | console.log("Fetching the list of IP addresses..."); 15 | return new Promise((resolve, reject) => { 16 | 17 | let dataString = ''; 18 | let post_req = https.request(SpamHausDropUrl, (res) => { 19 | res.setEncoding("utf8"); 20 | res.on('data', chunk => { 21 | dataString += chunk; 22 | }); 23 | res.on('end', () => { 24 | listOfIps = dataString.split(/\r?\n/); 25 | listOfIps = listOfIps.filter((line) => line.match(/^\d+/)); 26 | listOfIps = listOfIps.map(s => s.split(" ;")[0]); 27 | console.log("Fetched " + listOfIps.length + " IP addresses..."); 28 | resolve(); 29 | }); 30 | res.on('error', (err) => { 31 | reject(err); 32 | }); 33 | }); 34 | post_req.end(); 35 | }); 36 | } 37 | 38 | let updateRules = async function (ruleGroup) { 39 | let params = ruleGroup; 40 | delete params.Capacity; 41 | params.RuleGroupName = params.RuleGroupResponse.RuleGroupName; 42 | params.Type = params.RuleGroupResponse.Type; 43 | delete params.RuleGroupResponse; 44 | 45 | console.log("Updating rules..."); 46 | let res = await networkfirewall.updateRuleGroup(params).promise(); 47 | if (res) { 48 | console.log("Updated '" + params.RuleGroupName + "'."); 49 | } else { 50 | console.log("Error updating the rules for '" + params.RuleGroupName + "'..."); 51 | } 52 | return; 53 | }; 54 | 55 | let createRules = async function (ruleGroup, type) { 56 | if (listOfIps.length == 0) { 57 | await fetchIPs(); 58 | } else { 59 | console.log("Using recently fetched list of " + listOfIps.length + " IP addresses..."); 60 | } 61 | 62 | let rulesString = "# Last updated: " + new Date().toUTCString() + "\n"; 63 | rulesString += "# Using a list of " + listOfIps.length + " IP addresses\n"; 64 | 65 | listOfIps.forEach((ip, index) => { 66 | rulesString += type + ' ip ' + ip + ' any -> any any (msg:"' + type + ' emerging threats traffic from ' + ip + '"; rev:1; sid:55' + index + ';)\n'; 67 | rulesString += type + ' ip any any -> ' + ip + ' any (msg:"' + type + ' emerging threats traffic to ' + ip + '"; rev:1; sid:66' + index + ';)\n'; 68 | }); 69 | 70 | ruleGroup.RuleGroup.RulesSource.RulesString = rulesString; 71 | await updateRules(ruleGroup); 72 | 73 | return; 74 | }; 75 | 76 | exports.handler = async (event, context) => { 77 | 78 | var params = {Type: "STATEFUL", RuleGroupArn: ''}; 79 | 80 | console.log("Searching Rule Groups for 'SpamHausIPList'..."); 81 | let res = await networkfirewall.describeRuleGroup(params).promise(); 82 | 83 | if (res.RuleGroupResponse) { 84 | console.log("Found Rule Group..."); 85 | await createRules(res,"drop"); 86 | } else { 87 | console.log("ERROR: No matching Rule Group found..."); 88 | } 89 | 90 | return; 91 | }; 92 | -------------------------------------------------------------------------------- /SpamHaus/src/SpamHauseEDropIPFiltering-Deprecated.js: -------------------------------------------------------------------------------- 1 | // DEPRECATED - but left for historical context 2 | // This was based on Node 14 and will not work with Node 16 or greater 3 | // Node 16 or great also requires v3 of the AWS SDK 4 | 5 | //Additionally the drop.txt and edrop.txt files have been merged into the drop.txt link 6 | var AWS = require("aws-sdk"); 7 | var https = require("https"); 8 | var listOfIps = []; 9 | 10 | const SpamHausDropUrl = "https://www.spamhaus.org/drop/edrop.txt"; 11 | 12 | const networkfirewall = new AWS.NetworkFirewall(); 13 | 14 | function fetchIPs() { 15 | console.log("Fetching the list of IP addresses..."); 16 | return new Promise((resolve, reject) => { 17 | 18 | let dataString = ''; 19 | let post_req = https.request(SpamHausDropUrl, (res) => { 20 | res.setEncoding("utf8"); 21 | res.on('data', chunk => { 22 | dataString += chunk; 23 | }); 24 | res.on('end', () => { 25 | listOfIps = dataString.split(/\r?\n/); 26 | listOfIps = listOfIps.filter((line) => line.match(/^\d+/)); 27 | listOfIps = listOfIps.map(s => s.split(" ;")[0]); 28 | console.log("Fetched " + listOfIps.length + " IP addresses..."); 29 | resolve(); 30 | }); 31 | res.on('error', (err) => { 32 | reject(err); 33 | }); 34 | }); 35 | post_req.end(); 36 | }); 37 | } 38 | 39 | let updateRules = async function (ruleGroup) { 40 | let params = ruleGroup; 41 | delete params.Capacity; 42 | params.RuleGroupName = params.RuleGroupResponse.RuleGroupName; 43 | params.Type = params.RuleGroupResponse.Type; 44 | delete params.RuleGroupResponse; 45 | 46 | console.log("Updating rules..."); 47 | let res = await networkfirewall.updateRuleGroup(params).promise(); 48 | if (res) { 49 | console.log("Updated '" + params.RuleGroupName + "'."); 50 | } else { 51 | console.log("Error updating the rules for '" + params.RuleGroupName + "'..."); 52 | } 53 | return; 54 | }; 55 | 56 | let createRules = async function (ruleGroup, type) { 57 | if (listOfIps.length == 0) { 58 | await fetchIPs(); 59 | } else { 60 | console.log("Using recently fetched list of " + listOfIps.length + " IP addresses..."); 61 | } 62 | 63 | let rulesString = "# Last updated: " + new Date().toUTCString() + "\n"; 64 | rulesString += "# Using a list of " + listOfIps.length + " IP addresses\n"; 65 | 66 | listOfIps.forEach((ip, index) => { 67 | rulesString += type + ' ip ' + ip + ' any -> any any (msg:"' + type + ' emerging threats traffic from ' + ip + '"; rev:1; sid:55' + index + ';)\n'; 68 | rulesString += type + ' ip any any -> ' + ip + ' any (msg:"' + type + ' emerging threats traffic to ' + ip + '"; rev:1; sid:66' + index + ';)\n'; 69 | }); 70 | 71 | ruleGroup.RuleGroup.RulesSource.RulesString = rulesString; 72 | await updateRules(ruleGroup); 73 | 74 | return; 75 | }; 76 | 77 | exports.handler = async (event, context) => { 78 | var params = {Type: "STATEFUL", RuleGroupArn: ''}; 79 | 80 | console.log("Searching Rule Groups for 'SpamHausEIPList'..."); 81 | let res = await networkfirewall.describeRuleGroup(params).promise(); 82 | 83 | if (res.RuleGroupResponse) { 84 | console.log("Found Rule Group..."); 85 | await createRules(res,"drop"); 86 | } else { 87 | console.log("ERROR: No matching Rule Group found..."); 88 | } 89 | 90 | return; 91 | }; 92 | -------------------------------------------------------------------------------- /TLSFingerprint/cnf-templates/TLSFingerprint.yaml: -------------------------------------------------------------------------------- 1 | Parameters: 2 | SNIRuleGroupName: 3 | Type: String 4 | Default: 'AllowListedSNIDomains' 5 | FingerprintRuleGroupName: 6 | Type: String 7 | Default: 'AutoUpdating-AllowListedTLSFingerprints' 8 | 9 | Resources: 10 | SNIRuleGroup: 11 | Type: 'AWS::NetworkFirewall::RuleGroup' 12 | Properties: 13 | RuleGroupName: !Sub "${AWS::StackName}-${SNIRuleGroupName}" 14 | Type: STATEFUL 15 | RuleGroup: 16 | RulesSource: 17 | RulesSourceList: 18 | GeneratedRulesType: ALLOWLIST 19 | Targets: 20 | - www.aws.com 21 | TargetTypes: 22 | - TLS_SNI 23 | Capacity: 1000 24 | Description: !Sub 'This rule is used to manage which domains are limiting access. The CloudWatch Events Rule: TLFingerprintStatefulRulegroupHourlyTrigger, triggers a daily parse of the list of domains and adds their TLS fingerprint to the Rule Group: AllowListedTLSFingerprints' 25 | Tags: 26 | - Key: "ProjectName" 27 | Value: "AllowListedTLSFingerprints" 28 | FingerprintRuleGroup: 29 | Type: 'AWS::NetworkFirewall::RuleGroup' 30 | Properties: 31 | RuleGroupName: !Sub "${AWS::StackName}-${FingerprintRuleGroupName}" 32 | Type: STATEFUL 33 | RuleGroup: 34 | RulesSource: 35 | RulesString: !Sub '#This rule is automatically managed by the CloudWatch Events Rule: TLFingerprintStatefulRulegroupHourlyTrigger; it fetches the domain list from the SNI Domain Rule Group and updates the TLS fingerprints daily.' 36 | Capacity: 1000 37 | Description: !Sub 'This rule is automatically managed by the CloudWatch Events Rule: TLFingerprintStatefulRulegroupHourlyTrigger; it fetches the domain list fromthe SNI Domain Rule Group and updates the TLS fingerprints daily.' 38 | Tags: 39 | - Key: "ProjectName" 40 | Value: "AllowListedTLSFingerprints" 41 | LambdaExecutionRole: 42 | Type: AWS::IAM::Role 43 | Properties: 44 | AssumeRolePolicyDocument: 45 | Version: '2012-10-17' 46 | Statement: 47 | - Effect: Allow 48 | Principal: 49 | Service: 50 | - lambda.amazonaws.com 51 | Action: 52 | - sts:AssumeRole 53 | Path: "/" 54 | Policies: 55 | - PolicyName: LambdaLogs 56 | PolicyDocument: 57 | Version: '2012-10-17' 58 | Statement: 59 | - Effect: Allow 60 | Action: 61 | - 'logs:CreateLogStream' 62 | - 'logs:PutLogEvents' 63 | Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*' 64 | - PolicyName: NetworkFirewall 65 | PolicyDocument: 66 | Version: '2012-10-17' 67 | Statement: 68 | - Effect: Allow 69 | Action: 70 | - 'network-firewall:*' 71 | Resource: 72 | - !GetAtt FingerprintRuleGroup.RuleGroupArn 73 | - Effect: Allow 74 | Action: 75 | - 'network-firewall:DescribeRuleGroup' 76 | Resource: 77 | - !GetAtt SNIRuleGroup.RuleGroupArn 78 | Tags: 79 | - Key: "ProjectName" 80 | Value: "AllowListedTLSFingerprints" 81 | ScheduledRule: 82 | Type: AWS::Events::Rule 83 | Properties: 84 | Description: "TLFingerprintStatefulRulegroupHourlyTrigger" 85 | ScheduleExpression: "cron(0 0 * * ? *)" 86 | State: "ENABLED" 87 | Targets: 88 | - Arn: 89 | Fn::GetAtt: 90 | - "LambdaFunction" 91 | - "Arn" 92 | Id: "TargetFunctionV1" 93 | LambdaLogGroup: 94 | Type: AWS::Logs::LogGroup 95 | Properties: 96 | LogGroupName: !Sub "/aws/lambda/${LambdaFunction}" 97 | RetentionInDays: 14 98 | DeletionPolicy: Retain 99 | PermissionForEventsToInvokeLambda: 100 | Type: AWS::Lambda::Permission 101 | Properties: 102 | FunctionName: !Ref "LambdaFunction" 103 | Action: "lambda:InvokeFunction" 104 | Principal: "events.amazonaws.com" 105 | SourceArn: 106 | Fn::GetAtt: 107 | - "ScheduledRule" 108 | - "Arn" 109 | LambdaInvoke: 110 | Type: AWS::CloudFormation::CustomResource 111 | Version: "1.0" 112 | Properties: 113 | ServiceToken: !GetAtt LambdaFunction.Arn 114 | LambdaFunction: 115 | Type: AWS::Lambda::Function 116 | Properties: 117 | Role: !GetAtt LambdaExecutionRole.Arn 118 | Runtime: python3.12 119 | Handler: index.lambda_handler 120 | Timeout: 600 121 | Description: Used to check TLS server fingerprint and update the associated RuleGroup 122 | Tags: 123 | - Key: "ProjectName" 124 | Value: "AllowListedTLSFingerprints" 125 | Code: 126 | ZipFile: !Sub | 127 | import boto3 128 | import json 129 | import random 130 | import urllib 131 | import ssl 132 | import socket 133 | import hashlib 134 | import base64 135 | from datetime import datetime 136 | import cfnresponse 137 | 138 | SOURCE_ARN = '${SNIRuleGroup.RuleGroupArn}' 139 | DESTINATION_ARN = '${FingerprintRuleGroup.RuleGroupArn}' 140 | 141 | nf = boto3.client('network-firewall') 142 | 143 | def gen_sid(): 144 | return random.randint(0, 999999999) 145 | 146 | def get_domains(arn): 147 | params = {"Type": "STATEFUL", "RuleGroupArn": arn} 148 | try: 149 | res = nf.describe_rule_group(**params) 150 | if 'RuleGroupResponse' in res: 151 | print("Found source rulegroup") 152 | domains = res['RuleGroup']['RulesSource']['RulesSourceList']['Targets'] 153 | return domains 154 | else: 155 | print("ERROR: No matching Rule Group found") 156 | except Exception as e: 157 | print(f"Error: {str(e)}") 158 | return None 159 | 160 | def fetch_cert(host, port=443): 161 | try: 162 | context = ssl.create_default_context() 163 | 164 | with socket.create_connection((host, port)) as sock: 165 | with context.wrap_socket(sock, server_hostname=host) as secure_sock: 166 | der_cert = secure_sock.getpeercert(binary_form=True) 167 | cert = secure_sock.getpeercert() 168 | 169 | print(f"Hostname {host} verified successfully.") 170 | 171 | # Extract common name 172 | common_name = '' 173 | if cert and 'subject' in cert: 174 | subject = dict(x[0] for x in cert['subject']) 175 | common_name = subject.get('commonName', '') 176 | 177 | # Genrate SHA-1 fingerprint 178 | sha1_fingerprint = hashlib.sha1(der_cert).hexdigest() 179 | formatted_fingerprint = ':'.join(sha1_fingerprint[i:i+2] for i in range(0, len(sha1_fingerprint), 2)) 180 | 181 | print(f' Fetching from: {host}') 182 | print(f' Subject Common Name: {common_name}') 183 | print(f' Certificate Fingerprint (SHA-1): {formatted_fingerprint}') 184 | 185 | return { 186 | 'subject': {'CN': common_name}, 187 | 'fingerprint': formatted_fingerprint 188 | } 189 | except ssl.CertificateError as e: 190 | print(f"Hostname verification failed for {host}: {e}") 191 | return None 192 | except Exception as e: 193 | print(f"Error fetching certificate for {host}: {str(e)}") 194 | return None 195 | 196 | def update_rule_group(new_rule): 197 | params = {"Type": "STATEFUL", "RuleGroupArn": DESTINATION_ARN} 198 | try: 199 | res = nf.describe_rule_group(**params) 200 | if 'RuleGroupResponse' in res: 201 | print("Found destination rulegroup") 202 | res['RuleGroup']['RulesSource']['RulesString'] = new_rule 203 | res.pop('Capacity', None) 204 | res['RuleGroupName'] = res['RuleGroupResponse']['RuleGroupName'] 205 | res['Description'] = res['RuleGroupResponse']['Description'] 206 | res['Type'] = res['RuleGroupResponse']['Type'] 207 | # Remove keys that can't be updated 208 | res.pop('ResponseMetadata', None) 209 | res.pop('Capacity', None) 210 | res.pop('RuleGroupResponse', None) 211 | 212 | print("Updating rules") 213 | result = nf.update_rule_group(**res) 214 | if result: 215 | print(f"Updated '{res['RuleGroupName']}'") 216 | return True 217 | else: 218 | print(f"Error updating '{res['RuleGroupName']}'...") 219 | return False 220 | else: 221 | print("No matching Rule Group found") 222 | return False 223 | except Exception as e: 224 | print(f"Error: {str(e)}") 225 | return False 226 | 227 | def lambda_handler(event, context): 228 | if event.get('RequestType') == "Delete": 229 | cfnresponse.send(event, context, cfnresponse.SUCCESS, {'message': 'Function executed successfully'}) 230 | return {'statusCode': 200, 'body': json.dumps('SUCCESS')} 231 | try: 232 | print(f"Fetch a list of domains from: {SOURCE_ARN}") 233 | domains = get_domains(SOURCE_ARN) 234 | 235 | if domains: 236 | print(f"Using a list of: {len(domains)} domains") 237 | new_rule = [] 238 | new_rule.append(f'# This rule is automatically managed by a Lambda') 239 | new_rule.append(f'# Last updated: {datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S GMT")}') 240 | 241 | for domain in domains: 242 | f_cert = fetch_cert(domain, port=443) 243 | if f_cert: 244 | new_rule.append(f'pass tls $EXTERNAL_NET any -> $HOME_NET any (msg:"Allow https://{domain}/"; tls.cert_fingerprint; content:"{f_cert["fingerprint"]}"; sid:{gen_sid()}; rev:1;)') 245 | else: 246 | new_rule.append(f'# ERROR: Unable to retrieve a fingerprint for: {domain}') 247 | 248 | new_rule.append(f'drop tls $EXTERNAL_NET any -> $HOME_NET any (msg:"Drop all other TLS fingerprints"; tls.cert_fingerprint; content:!"{f_cert["fingerprint"]}"; sid:1; rev:1;)') 249 | 250 | update_success = update_rule_group('\n'.join(new_rule)) 251 | 252 | if update_success: 253 | cfnresponse.send(event, context, cfnresponse.SUCCESS, {"message": "Successfully updated the rule group"}, None) 254 | return {'statusCode': 200, 'body': json.dumps('SUCCESS')} 255 | else: 256 | cfnresponse.send(event, context, cfnresponse.FAILED, {"message": "Failed to update the rule group"}, None) 257 | print("Failed to update the rule group") 258 | else: 259 | cfnresponse.send(event, context, cfnresponse.FAILED, {"message": f"Error fetching a list of domains from: {SOURCE_ARN}"}, None) 260 | print(f"Error fetching a list of domains from: {SOURCE_ARN}") 261 | except Exception as e: 262 | cfnresponse.send(event, context, cfnresponse.FAILED, {"message": f"Unexpected error: {str(e)}"}, None) 263 | print(f"Unexpected error: {str(e)}") -------------------------------------------------------------------------------- /TLSFingerprint/src/TLSFingerprint-Deprecated.js: -------------------------------------------------------------------------------- 1 | // DEPRECATED - but left for historical context 2 | // This was based on Node 14 and will not work with Node 16 or greater 3 | // Node 16 or great also requires v3 of the AWS SDK 4 | 5 | var AWS = require("aws-sdk"); 6 | const https = require("https"); 7 | const tls = require('tls'); 8 | 9 | const nf = new AWS.NetworkFirewall(); 10 | 11 | function genSid() { 12 | return Math.floor(Math.random() * 1000000000); 13 | } 14 | 15 | async function getDomains(arn){ 16 | let params = {Type: "STATEFUL", RuleGroupArn: arn}; 17 | let res = await nf.describeRuleGroup(params).promise(); 18 | if (res.RuleGroupResponse) { 19 | console.log("Found source rulegroup"); 20 | let domains = (res.RuleGroup.RulesSource.RulesSourceList.Targets); 21 | return domains; 22 | } else { 23 | console.log("ERROR: No matching Rule Group found"); 24 | } 25 | return; 26 | } 27 | 28 | function fetchCert(host) { 29 | let fCert = {subject: {CN: ""}, fingerprint: ""}; 30 | const options = { 31 | hostname: host, 32 | port: 443, 33 | path: "/", 34 | method: 'GET', 35 | checkServerIdentity: function(host, cert) { 36 | const err = tls.checkServerIdentity(host, cert); 37 | if (err) { 38 | return err; 39 | } 40 | fCert.subject.CN = cert.subject.CN; 41 | fCert.fingerprint = cert.fingerprint.toLowerCase(); 42 | } 43 | }; 44 | 45 | options.agent = new https.Agent(options); 46 | 47 | return new Promise((resolve, reject) => { 48 | let req = https.request(options, (res) => { 49 | res.on('data', d => {}); 50 | 51 | res.on('end', () => { 52 | console.log(' Fetching from:', host); 53 | console.log(' Subject Common Name:', fCert.subject.CN); 54 | console.log(' Certificate SHA-1 fingerprint:', fCert.fingerprint); 55 | resolve(fCert); 56 | }); 57 | 58 | res.on('error', (err) => { 59 | reject(err); 60 | }); 61 | 62 | }); 63 | 64 | req.end(); 65 | }); 66 | } 67 | 68 | async function updateRuleGroup(newRule){ 69 | let params = {Type: "STATEFUL", RuleGroupArn: ''}; 70 | let res = await nf.describeRuleGroup(params).promise(); 71 | if (res.RuleGroupResponse) { 72 | console.log("Found destination rulegroup"); 73 | res.RuleGroup.RulesSource.RulesString = newRule; 74 | delete res.Capacity; 75 | res.RuleGroupName = res.RuleGroupResponse.RuleGroupName; 76 | res.Description = res.RuleGroupResponse.Description; 77 | res.Type = res.RuleGroupResponse.Type; 78 | delete res.RuleGroupResponse; 79 | 80 | console.log("Updating rules"); 81 | let result = await nf.updateRuleGroup(res).promise(); 82 | if (result) { 83 | console.log("Updated '" + res.RuleGroupName); 84 | } else { 85 | console.log("Error updating '" + res.RuleGroupName + "'..."); 86 | } 87 | } else { 88 | console.log("No matching Rule Group found"); 89 | } 90 | return; 91 | } 92 | 93 | exports.handler = async (event, context) => { 94 | 95 | let sourceArn = ''; 96 | console.log("Fetch a list of domains from: ", sourceArn); 97 | let domains = await getDomains(sourceArn); 98 | 99 | if (domains) { 100 | console.log("Using a list of: " + domains.length + " domains"); 101 | let newRule = '# This rule is automatically managed by a Lambda\n# Last updated: ' + new Date().toUTCString() + "\n"; 102 | 103 | for (let index = 0; index < domains.length; index++) { 104 | let fCert = await fetchCert(domains[index]); 105 | if (fCert) { 106 | newRule += 'pass tls $EXTERNAL_NET any -> $HOME_NET any (msg:"Allow https://' + domains[index] + '/"; tls.fingerprint:"' + fCert.fingerprint + '"; sid:' + genSid() + '; rev:1;)\n'; 107 | } else { 108 | newRule += '# ERROR: Unable to retrieve a fingerprint for: ' + domains[index] + '\n'; 109 | } 110 | } 111 | 112 | newRule += 'drop tls $EXTERNAL_NET any -> $HOME_NET any (msg:"Drop all other TLS fingerprints"; tls.fingerprint:":"; sid:1; rev:1;)'; 113 | 114 | await updateRuleGroup(newRule); 115 | } else { 116 | console.log("Error fetching a list of domains from: ", sourceArn); 117 | } 118 | 119 | return; 120 | }; 121 | -------------------------------------------------------------------------------- /TLSFingerprint/src/TLSFingerprint.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import json 3 | import random 4 | import urllib 5 | import ssl 6 | import socket 7 | import hashlib 8 | import base64 9 | from datetime import datetime 10 | 11 | SOURCE_ARN = '' 12 | DESTINATION_ARN = '' 13 | 14 | nf = boto3.client('network-firewall') 15 | 16 | def gen_sid(): 17 | return random.randint(0, 999999999) 18 | 19 | def get_domains(arn): 20 | params = {"Type": "STATEFUL", "RuleGroupArn": arn} 21 | try: 22 | res = nf.describe_rule_group(**params) 23 | if 'RuleGroupResponse' in res: 24 | print("Found source rulegroup") 25 | domains = res['RuleGroup']['RulesSource']['RulesSourceList']['Targets'] 26 | return domains 27 | else: 28 | print("ERROR: No matching Rule Group found") 29 | except Exception as e: 30 | print(f"Error: {str(e)}") 31 | return None 32 | 33 | def fetch_cert(host, port=443): 34 | try: 35 | context = ssl.create_default_context() 36 | 37 | with socket.create_connection((host, port)) as sock: 38 | with context.wrap_socket(sock, server_hostname=host) as secure_sock: 39 | der_cert = secure_sock.getpeercert(binary_form=True) 40 | cert = secure_sock.getpeercert() 41 | 42 | print(f"Hostname {host} verified successfully.") 43 | 44 | # Extract common name 45 | common_name = '' 46 | if cert and 'subject' in cert: 47 | subject = dict(x[0] for x in cert['subject']) 48 | common_name = subject.get('commonName', '') 49 | 50 | # Genrate SHA-1 fingerprint 51 | sha1_fingerprint = hashlib.sha1(der_cert).hexdigest() 52 | formatted_fingerprint = ':'.join(sha1_fingerprint[i:i+2] for i in range(0, len(sha1_fingerprint), 2)) 53 | 54 | print(f' Fetching from: {host}') 55 | print(f' Subject Common Name: {common_name}') 56 | print(f' Certificate Fingerprint (SHA-1): {formatted_fingerprint}') 57 | 58 | return { 59 | 'subject': {'CN': common_name}, 60 | 'fingerprint': formatted_fingerprint 61 | } 62 | except ssl.CertificateError as e: 63 | print(f"Hostname verification failed for {host}: {e}") 64 | return None 65 | except Exception as e: 66 | print(f"Error fetching certificate for {host}: {str(e)}") 67 | return None 68 | 69 | def update_rule_group(new_rule): 70 | params = {"Type": "STATEFUL", "RuleGroupArn": DESTINATION_ARN} 71 | try: 72 | res = nf.describe_rule_group(**params) 73 | if 'RuleGroupResponse' in res: 74 | print("Found destination rulegroup") 75 | res['RuleGroup']['RulesSource']['RulesString'] = new_rule 76 | res.pop('Capacity', None) 77 | res['RuleGroupName'] = res['RuleGroupResponse']['RuleGroupName'] 78 | res['Description'] = res['RuleGroupResponse']['Description'] 79 | res['Type'] = res['RuleGroupResponse']['Type'] 80 | # Remove keys that can't be updated 81 | res.pop('ResponseMetadata', None) 82 | res.pop('Capacity', None) 83 | res.pop('RuleGroupResponse', None) 84 | 85 | print("Updating rules") 86 | result = nf.update_rule_group(**res) 87 | if result: 88 | print(f"Updated '{res['RuleGroupName']}'") 89 | return True 90 | else: 91 | print(f"Error updating '{res['RuleGroupName']}'...") 92 | return False 93 | else: 94 | print("No matching Rule Group found") 95 | return False 96 | except Exception as e: 97 | print(f"Error: {str(e)}") 98 | return False 99 | 100 | def lambda_handler(event, context): 101 | try: 102 | print(f"Fetch a list of domains from: {SOURCE_ARN}") 103 | domains = get_domains(SOURCE_ARN) 104 | 105 | if domains: 106 | print(f"Using a list of: {len(domains)} domains") 107 | new_rule = [] 108 | new_rule.append(f'# This rule is automatically managed by a Lambda') 109 | new_rule.append(f'# Last updated: {datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S GMT")}') 110 | 111 | for domain in domains: 112 | f_cert = fetch_cert(domain, port=443) 113 | if f_cert: 114 | new_rule.append(f'pass tls $EXTERNAL_NET any -> $HOME_NET any (msg:"Allow https://{domain}/"; tls.cert_fingerprint; content:"{f_cert["fingerprint"]}"; sid:{gen_sid()}; rev:1;)') 115 | else: 116 | new_rule.append(f'# ERROR: Unable to retrieve a fingerprint for: {domain}') 117 | 118 | new_rule.append(f'drop tls $EXTERNAL_NET any -> $HOME_NET any (msg:"Drop all other TLS fingerprints"; tls.cert_fingerprint; content:!"{f_cert["fingerprint"]}"; sid:1; rev:1;)') 119 | 120 | update_success = update_rule_group('\n'.join(new_rule)) 121 | 122 | if update_success: 123 | return {'statusCode': 200, 'body': json.dumps('SUCCESS')} 124 | else: 125 | print("Failed to update the rule group") 126 | else: 127 | print(f"Error fetching a list of domains from: {SOURCE_ARN}") 128 | except Exception as e: 129 | print(f"Unexpected error: {str(e)}") -------------------------------------------------------------------------------- /TorProject/cfn-templates/TorProjectIPFiltering.yaml: -------------------------------------------------------------------------------- 1 | Parameters: 2 | RuleGroupName: 3 | Type: String 4 | Default: 'AutoUpdating-TorProjectIPList' 5 | RuleGroupAction: 6 | Type: String 7 | Description: "Used to define the action to take on a matching rule if found" 8 | Default : 'drop' 9 | AllowedValues: 10 | - 'alert' 11 | - 'drop' 12 | 13 | Resources: 14 | StatefulRulegroup: 15 | Type: 'AWS::NetworkFirewall::RuleGroup' 16 | Properties: 17 | RuleGroupName: !Sub "${AWS::StackName}-${RuleGroupName}-${RuleGroupAction}" 18 | Type: STATEFUL 19 | RuleGroup: 20 | RulesSource: 21 | RulesString: '#This will be updated via the Lambda function' 22 | Capacity: 8000 23 | Description: >- 24 | Used to track a list of Emerging IP Threats from 25 | https://check.torproject.org/exit-addresses 26 | Tags: 27 | - Key: "ProjectName" 28 | Value: "TorProjectIPFiltering" 29 | LambdaExecutionRole: 30 | Type: AWS::IAM::Role 31 | Properties: 32 | AssumeRolePolicyDocument: 33 | Version: '2012-10-17' 34 | Statement: 35 | - Effect: Allow 36 | Principal: 37 | Service: 38 | - lambda.amazonaws.com 39 | Action: 40 | - sts:AssumeRole 41 | Path: "/" 42 | Policies: 43 | - PolicyName: LambdaLogs 44 | PolicyDocument: 45 | Version: '2012-10-17' 46 | Statement: 47 | - Effect: Allow 48 | Action: 49 | - 'logs:CreateLogStream' 50 | - 'logs:PutLogEvents' 51 | Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*' 52 | - PolicyName: NetworkFirewall 53 | PolicyDocument: 54 | Version: '2012-10-17' 55 | Statement: 56 | - Effect: Allow 57 | Action: 58 | - 'network-firewall:*' 59 | Resource: 60 | - !GetAtt StatefulRulegroup.RuleGroupArn 61 | Tags: 62 | - Key: "ProjectName" 63 | Value: "TorProjectIPFiltering" 64 | LambdaLogGroup: 65 | Type: AWS::Logs::LogGroup 66 | Properties: 67 | LogGroupName: !Sub "/aws/lambda/${LambdaFunction}" 68 | RetentionInDays: 14 69 | DeletionPolicy: Retain 70 | ScheduledRule: 71 | Type: AWS::Events::Rule 72 | Properties: 73 | Description: "TorProjectDailyTrigger" 74 | ScheduleExpression: "cron(0 0 * * ? *)" 75 | State: "ENABLED" 76 | Targets: 77 | - Arn: 78 | Fn::GetAtt: 79 | - "LambdaFunction" 80 | - "Arn" 81 | Id: "TargetFunctionV1" 82 | PermissionForEventsToInvokeLambda: 83 | Type: AWS::Lambda::Permission 84 | Properties: 85 | FunctionName: !Ref "LambdaFunction" 86 | Action: "lambda:InvokeFunction" 87 | Principal: "events.amazonaws.com" 88 | SourceArn: 89 | Fn::GetAtt: 90 | - "ScheduledRule" 91 | - "Arn" 92 | LambdaInvoke: 93 | Type: AWS::CloudFormation::CustomResource 94 | Version: "1.0" 95 | Properties: 96 | ServiceToken: !GetAtt LambdaFunction.Arn 97 | LambdaFunction: 98 | Type: AWS::Lambda::Function 99 | Properties: 100 | Role: !GetAtt LambdaExecutionRole.Arn 101 | Runtime: python3.12 102 | Handler: index.lambda_handler 103 | Timeout: 60 104 | Description: Used to fetch data from the Emerging Threats IP list and update the associated RuleGroup 105 | Tags: 106 | - 107 | Key: "ProjectName" 108 | Value: "TorProjectIPFiltering" 109 | Code: 110 | ZipFile: !Sub | 111 | import boto3 112 | import json 113 | import urllib.request 114 | from datetime import datetime 115 | import cfnresponse 116 | 117 | TOR_PROJECT_URL = "https://check.torproject.org/exit-addresses" 118 | RULE_GROUP_ARN = "${StatefulRulegroup.RuleGroupArn}" 119 | 120 | networkfirewall = boto3.client('network-firewall') 121 | 122 | def fetch_ips(): 123 | print("Fetching the list of IP addresses...") 124 | try: 125 | with urllib.request.urlopen(TOR_PROJECT_URL) as response: 126 | data = response.read().decode('utf-8') 127 | 128 | ip_list = [line.split()[1] for line in data.splitlines() if line.startswith("ExitAddress ")] 129 | print(f"Fetched {len(ip_list)} IP addresses...") 130 | return ip_list 131 | except urllib.error.URLError as e: 132 | print(f"Error fetching IP addresses: {e}") 133 | return [] 134 | 135 | def update_rules(rule_group): 136 | params = rule_group.copy() 137 | print(params) # Debug line 138 | params['RuleGroupName'] = params['RuleGroupResponse']['RuleGroupName'] 139 | params['Type'] = params['RuleGroupResponse']['Type'] 140 | # Remove keys that can't be updated 141 | params.pop('ResponseMetadata', None) 142 | params.pop('Capacity', None) 143 | params.pop('RuleGroupResponse', None) 144 | 145 | print("Updating rules...") 146 | try: 147 | networkfirewall.update_rule_group(**params) 148 | print(f"Updated '{params['RuleGroupName']}'.") 149 | except Exception as e: 150 | print(f"Error updating the rules for '{params['RuleGroupName']}': {str(e)}") 151 | 152 | def create_rules(rule_group, type_): 153 | ip_list = fetch_ips() 154 | 155 | rules_string = f"# Last updated: {datetime.utcnow().isoformat()}\n" 156 | rules_string += f"# Using a list of {len(ip_list)} IP addresses\n" 157 | 158 | for index, ip in enumerate(ip_list): 159 | rules_string += f"{type_} ip {ip} any -> any any (msg:\"{type_} emerging threats traffic from {ip}\"; rev:1; sid:55{index};)\n" 160 | rules_string += f"{type_} ip any any -> {ip} any (msg:\"{type_} emerging threats traffic to {ip}\"; rev:1; sid:66{index};)\n" 161 | 162 | rule_group['RuleGroup']['RulesSource']['RulesString'] = rules_string 163 | update_rules(rule_group) 164 | 165 | def lambda_handler(event, context): 166 | if event.get('RequestType') == "Delete": 167 | cfnresponse.send(event, context, cfnresponse.SUCCESS, {'message': 'Function executed successfully'}) 168 | return {'statusCode': 200, 'body': json.dumps('SUCCESS')} 169 | 170 | try: 171 | params = { 172 | "Type": "STATEFUL", 173 | "RuleGroupArn": RULE_GROUP_ARN 174 | } 175 | 176 | print("Searching for Rule Groups...") 177 | res = networkfirewall.describe_rule_group(**params) 178 | if 'RuleGroupResponse' in res: 179 | print("Found Rule Group...") 180 | create_rules(res, "drop") 181 | cfnresponse.send(event, context, cfnresponse.SUCCESS, {'message': 'Function executed successfully'}) 182 | else: 183 | print("ERROR: No matching Rule Group found...") 184 | cfnresponse.send(event, context, cfnresponse.FAILED, {'message': 'No matching Rule Group found'}) 185 | except Exception as e: 186 | print(f"Error: {str(e)}") 187 | cfnresponse.send(event, context, cfnresponse.FAILED, {'message': f'Error: {str(e)}'}) 188 | 189 | return { 190 | 'statusCode': 200, 191 | 'body': 'Function executed successfully' 192 | } 193 | -------------------------------------------------------------------------------- /TorProject/src/TorProjectIPFiltering-Deprecated.js: -------------------------------------------------------------------------------- 1 | // DEPRECATED - but left for historical context 2 | // This was based on Node 14 and will not work with Node 16 or greater 3 | // Node 16 or great also requires v3 of the AWS SDK 4 | 5 | var AWS = require("aws-sdk"); 6 | var https = require("https"); 7 | var listOfIps = []; 8 | 9 | const TorProjectUrl = "https://check.torproject.org/exit-addresses"; 10 | 11 | const networkfirewall = new AWS.NetworkFirewall(); 12 | 13 | function fetchIPs() { 14 | console.log("Fetching the list of IP addresses..."); 15 | return new Promise((resolve, reject) => { 16 | 17 | let dataString = ''; 18 | let post_req = https.request(TorProjectUrl, (res) => { 19 | res.setEncoding("utf8"); 20 | res.on('data', chunk => { 21 | dataString += chunk; 22 | }); 23 | res.on('end', () => { 24 | listOfIps = dataString 25 | .split(/\r?\n/) 26 | .filter((line) => line.match(/ExitAddress /)) 27 | .map(s => s.split(" ")[1]); 28 | console.log("Fetched " + listOfIps.length + " IP addresses..."); 29 | resolve(); 30 | }); 31 | res.on('error', (err) => { 32 | reject(err); 33 | }); 34 | }); 35 | post_req.end(); 36 | }); 37 | } 38 | 39 | let updateRules = async function (ruleGroup) { 40 | let params = ruleGroup; 41 | delete params.Capacity; 42 | params.RuleGroupName = params.RuleGroupResponse.RuleGroupName; 43 | params.Type = params.RuleGroupResponse.Type; 44 | delete params.RuleGroupResponse; 45 | 46 | console.log("Updating rules..."); 47 | let res = await networkfirewall.updateRuleGroup(params).promise(); 48 | if (res) { 49 | console.log("Updated '" + params.RuleGroupName + "'."); 50 | } else { 51 | console.log("Error updating the rules for '" + params.RuleGroupName + "'..."); 52 | } 53 | return; 54 | }; 55 | 56 | let createRules = async function (ruleGroup, type) { 57 | if (listOfIps.length == 0) { 58 | await fetchIPs(); 59 | } else { 60 | console.log("Using recently fetched list of " + listOfIps.length + " IP addresses..."); 61 | } 62 | 63 | let rulesString = "# Last updated: " + new Date().toUTCString() + "\n"; 64 | rulesString += "# Using a list of " + listOfIps.length + " IP addresses\n"; 65 | 66 | listOfIps.forEach((ip, index) => { 67 | rulesString += type + ' ip ' + ip + ' any -> any any (msg:"' + type + ' emerging threats traffic from ' + ip + '"; rev:1; sid:55' + index + ';)\n'; 68 | rulesString += type + ' ip any any -> ' + ip + ' any (msg:"' + type + ' emerging threats traffic to ' + ip + '"; rev:1; sid:66' + index + ';)\n'; 69 | }); 70 | 71 | ruleGroup.RuleGroup.RulesSource.RulesString = rulesString; 72 | await updateRules(ruleGroup); 73 | 74 | return; 75 | }; 76 | 77 | exports.handler = async (event, context) => { 78 | 79 | var params = {Type: "STATEFUL", RuleGroupArn: 'arn:aws:network-firewall:us-east-1:442338576812:stateful-rulegroup/tor-t1-AutoUpdating-TorProjectIPList-drop'}; 80 | 81 | console.log("Searching for Rule Groups..."); 82 | let res = await networkfirewall.describeRuleGroup(params).promise(); 83 | if (res.RuleGroupResponse) { 84 | console.log("Found Rule Group..."); 85 | await createRules(res,"drop"); 86 | } else { 87 | console.log("ERROR: No matching Rule Group found..."); 88 | } 89 | 90 | return; 91 | }; 92 | -------------------------------------------------------------------------------- /TorProject/src/TorProjectIPFiltering.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import json 3 | import urllib.request 4 | from datetime import datetime 5 | 6 | TOR_PROJECT_URL = "https://check.torproject.org/exit-addresses" 7 | RULE_GROUP_ARN = "" 8 | 9 | networkfirewall = boto3.client('network-firewall') 10 | 11 | def fetch_ips(): 12 | print("Fetching the list of IP addresses...") 13 | try: 14 | with urllib.request.urlopen(TOR_PROJECT_URL) as response: 15 | data = response.read().decode('utf-8') 16 | 17 | ip_list = [line.split()[1] for line in data.splitlines() if line.startswith("ExitAddress ")] 18 | print(f"Fetched {len(ip_list)} IP addresses...") 19 | return ip_list 20 | except urllib.error.URLError as e: 21 | print(f"Error fetching IP addresses: {e}") 22 | return [] 23 | 24 | def update_rules(rule_group): 25 | params = rule_group.copy() 26 | print(params) # Debug line 27 | params['RuleGroupName'] = params['RuleGroupResponse']['RuleGroupName'] 28 | params['Type'] = params['RuleGroupResponse']['Type'] 29 | # Remove keys that can't be updated 30 | params.pop('ResponseMetadata', None) 31 | params.pop('Capacity', None) 32 | params.pop('RuleGroupResponse', None) 33 | 34 | print("Updating rules...") 35 | try: 36 | networkfirewall.update_rule_group(**params) 37 | print(f"Updated '{params['RuleGroupName']}'.") 38 | except Exception as e: 39 | print(f"Error updating the rules for '{params['RuleGroupName']}': {str(e)}") 40 | 41 | def create_rules(rule_group, type_): 42 | ip_list = fetch_ips() 43 | 44 | rules_string = f"# Last updated: {datetime.utcnow().isoformat()}\n" 45 | rules_string += f"# Using a list of {len(ip_list)} IP addresses\n" 46 | 47 | for index, ip in enumerate(ip_list): 48 | rules_string += f"{type_} ip {ip} any -> any any (msg:\"{type_} emerging threats traffic from {ip}\"; rev:1; sid:55{index};)\n" 49 | rules_string += f"{type_} ip any any -> {ip} any (msg:\"{type_} emerging threats traffic to {ip}\"; rev:1; sid:66{index};)\n" 50 | 51 | rule_group['RuleGroup']['RulesSource']['RulesString'] = rules_string 52 | update_rules(rule_group) 53 | 54 | def lambda_handler(event, context): 55 | if event.get('RequestType') == "Delete": 56 | return {'statusCode': 200, 'body': json.dumps('SUCCESS')} 57 | 58 | try: 59 | params = { 60 | "Type": "STATEFUL", 61 | "RuleGroupArn": RULE_GROUP_ARN 62 | } 63 | 64 | print("Searching for Rule Groups...") 65 | res = networkfirewall.describe_rule_group(**params) 66 | if 'RuleGroupResponse' in res: 67 | print("Found Rule Group...") 68 | create_rules(res, "drop") 69 | else: 70 | print("ERROR: No matching Rule Group found...") 71 | except Exception as e: 72 | print(f"Error: {str(e)}") 73 | 74 | return { 75 | 'statusCode': 200, 76 | 'body': 'Function executed successfully' 77 | } 78 | --------------------------------------------------------------------------------