Parameters: RuleGroupName: Type: String Default: 'LinodeAddresses' RuleGroupAction: Type: String Description: "Used to define the action to take on a matching rule if found" Default : 'drop' AllowedValues: - 'alert' - 'drop' Resources: StatefulRulegroup: Type: 'AWS::NetworkFirewall::RuleGroup' Properties: RuleGroupName: !Sub "${AWS::StackName}-${RuleGroupName}-${RuleGroupAction}" Type: STATEFUL RuleGroup: RulesSource: RulesString: '#This will be updated via the Lambda function' Capacity: 3000 Description: >- Used to dynamically track a list of addresses associated with Linode endpoints Tags: - Key: "ProjectName" Value: "LinodeAddressesFiltering" LambdaExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: "/" Policies: - PolicyName: LambdaLogs PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*' - PolicyName: NetworkFirewall PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - 'network-firewall:*' Resource: - !GetAtt StatefulRulegroup.RuleGroupArn Tags: - Key: "ProjectName" Value: "LinodeAddressesFiltering" LambdaLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub "/aws/lambda/${LambdaFunction}" DeletionPolicy: Retain ScheduledRule: Type: AWS::Events::Rule Properties: Description: "AlphasocEncyptedDNSDailyTrigger" ScheduleExpression: "cron(0 0 * * ? *)" State: "ENABLED" Targets: - Arn: Fn::GetAtt: - "LambdaFunction" - "Arn" Id: "TargetFunctionV1" PermissionForEventsToInvokeLambda: Type: AWS::Lambda::Permission Properties: FunctionName: !Ref "LambdaFunction" Action: "lambda:InvokeFunction" Principal: "events.amazonaws.com" SourceArn: Fn::GetAtt: - "ScheduledRule" - "Arn" LambdaInvoke: Type: AWS::CloudFormation::CustomResource Version: "1.0" Properties: ServiceToken: !GetAtt LambdaFunction.Arn LambdaFunction: Type: AWS::Lambda::Function Properties: Role: !GetAtt LambdaExecutionRole.Arn Runtime: nodejs16.x Handler: index.handler Timeout: 60 Description: Used to fetch data from the Linode Addresses list and update the associated RuleGroup Tags: - Key: "ProjectName" Value: "LinodeAddressesFiltering" Code: ZipFile: !Sub | const AWS = require("aws-sdk"); const response = require('cfn-response'); const https = require("https"); let listOfIps = [], listHeader = []; const networkfirewall = new AWS.NetworkFirewall(); const rulesUrl = "https://geoip.linode.com/"; const rgARN = "${StatefulRulegroup.RuleGroupArn}" function fetchIPs() { console.log("Fetching the list of IP addresses..."); return new Promise((resolve, reject) => { let dataString = ''; let post_req = https.request(rulesUrl, (res) => { res.setEncoding("utf8"); res.on('data', chunk => { dataString += chunk; }); res.on('end', () => { //initial array creation listOfIps = dataString.split(/\r?\n/); //strip out the header listHeader = listOfIps.filter((line) => line.match(/^#+/)); //extract IPv4 lines listOfIps = listOfIps.filter((line) => line.match(/^(\d+(\.|$)){3}(\d+)/)); //create objects from the IPv4 Data listOfIps = listOfIps.map(s => { let items = s.split(","); return JSON.parse('{"ip":"' + items[0] + '","country":"' + items[1] + '","subdivision":"' + items[2] + '","city":"' + items[3] + '"}'); }); console.log("Fetched " + listOfIps.length + " IP addresses..."); resolve(); }); res.on('error', (err) => { reject(err); }); }); post_req.end(); }); } let updateRules = async function (ruleGroup) { let params = ruleGroup; delete params.Capacity; params.RuleGroupName = params.RuleGroupResponse.RuleGroupName; params.Description = "Dynamic list using data fetched from" + rulesUrl; params.Type = params.RuleGroupResponse.Type; delete params.RuleGroupResponse; console.log("Updating rules..."); let res = await networkfirewall.updateRuleGroup(params).promise(); if (res) { console.log("Updated '" + params.RuleGroupName + "'."); } else { console.log("Error updating the rules for '" + params.RuleGroupName + "'..."); } return; }; let createRules = async function (ruleGroup, type) { if (listOfIps.length == 0) { await fetchIPs(); } else { console.log("Using recently fetched list of " + listOfIps.length + " IP addresses..."); } let rulesString = "# RG Last updated: " + new Date().toUTCString() + "\n"; rulesString += "# Using a list of " + listOfIps.length + " IP addresses\n"; rulesString += "# ------------------ Following section fetched from " + rulesUrl + " -----------------------\n"; listHeader.forEach((line) => {rulesString += line + "\n"}); rulesString += "# ------------------ End section fetched from " + rulesUrl + " -----------------------\n"; listOfIps.forEach((obj, index) => { //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;) 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'; }); ruleGroup.RuleGroup.RulesSource.RulesString = rulesString; await updateRules(ruleGroup); return; }; exports.handler = async (event, context) => { if (event.RequestType == "Delete") { await response.send(event, context, "SUCCESS"); return; } let params = {Type: "STATEFUL", RuleGroupArn: rgARN}; console.log("Searching for matching Rule Group..."); let res = await networkfirewall.describeRuleGroup(params).promise(); if (res.RuleGroupResponse) { console.log("Found Rule Group..."); await createRules(res,"${RuleGroupAction}"); if (event.ResponseURL) await response.send(event, context, response.SUCCESS); } else { console.log("ERROR: No matching Rule Group found..."); if (event.ResponseURL) await response.send(event, context, response.FAILED); } return; };