diff --git a/README.md b/README.md index 3239b5b..fffc546 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,25 @@ The table below lists all the supported jobs with their links. | 21. | 688d093c-3b8d-11eb-adc1-0242ac120002 | S3 bucket should allow only HTTPS requests | [aws-s3-bucket-policy-allow-https](remediation_worker/jobs/aws_s3_bucket_policy_allow_https) | | 22. | 09639b9d-98e8-493b-b8a4-916775a7dea9 | SQS queue policy should restricted access to required users | [aws-sqs-queue-publicly-accessible](remediation_worker/jobs/aws_sqs_queue_publicly_accessible) | | 23. | 1ec4a1f2-3e08-11eb-b378-0242ac130002 | Network ACL should restrict administration ports (3389 and 22) from public access | [aws-ec2-administration-ports-ingress-allowed](remediation_worker/jobs/aws_ec2_administration_ports_ingress_allowed) | +| 24. | ce603728-d631-4bae-8657-c22da6e5944e | Kinesis data stream should be encrypted | [kinesis-encrypt-stream](remediation_worker/jobs/kinesis_encrypt_stream) | +| 25. | 5c8c263d7a550e1fb6560c39 | EC2 instance should restrict public access to FTP data port (20) | [ec2-close-port-20](remediation_worker/jobs/ec2_close_port_20) | +| 26. | 4823ede0-7bed-4af0-a182-81c2ada80203 | EC2 instance should restrict public access to Kibana (5601) | [ec2-close-port-5601](remediation_worker/jobs/ec2_close_port_5601) | +| 27. | 5c8c26427a550e1fb6560c41 | EC2 instance should restrict public access to MySQL server port (3306) | [ec2-close-port-3306](remediation_worker/jobs/ec2_close_port_3306) | +| 28. | 5c8c26417a550e1fb6560c3e | EC2 instance should restrict public access to Oracle SQL port (1521) | [ec2-close-port-1521](remediation_worker/jobs/ec2_close_port_1521) | +| 29. | 5c8c26417a550e1fb6560c3d | EC2 instance should restrict public access to SQL Server port (1433) | [ec2-close-port-1433](remediation_worker/jobs/ec2_close_port_1433) | +| 30. | 5c8c263e7a550e1fb6560c3b | EC2 instance should restrict public access to Telnet port (23) | [ec2-close-port-23](remediation_worker/jobs/ec2_close_port_23) | +| 31. | 5c8c263d7a550e1fb6560c3a | EC2 instance should restrict public access to FTP port (21) | [ec2-close-port-21](remediation_worker/jobs/ec2_close_port_21) | +| 32. | 04700175-adbe-49e1-bc7a-bc9605597ce2 | EC2 instance should restrict public access to Elasticsearch ports (9200,9300) | [ec2-close-port-9200_9300](remediation_worker/jobs/ec2_close_port_9200_9300) | +| 33. | 5c8c26427a550e1fb6560c40 | EC2 instance should restrict public access to MongoDB port (27017) | [ec2-close-port-27017](remediation_worker/jobs/ec2_close_port_27017) | +| 34. | 5c8c26407a550e1fb6560c3c | EC2 instance should restrict public access to TCP port (8080) | [ec2-close-port-8080](remediation_worker/jobs/ec2_close_port_8080) | +| 35. | 5c8c26447a550e1fb6560c44 | EC2 instance should restrict public access to Redshift port (5439) | [ec2-close-port-5439](remediation_worker/jobs/ec2_close_port_5439) | +| 36. | 2cdb8877-7ac3-4483-9ed0-1e792171d125 | EBS volume snapshot should be private | [ebs-private-snapshot](remediation_worker/jobs/ebs_private_snapshot) | +| 37. | 5c8c26467a550e1fb6560c48 | RDS instance should restrict public access | [rds-remove-public-endpoint](remediation_worker/jobs/rds_remove_public_endpoint) | +| 38. | 5c8c264a7a550e1fb6560c4c | RDS should have automatic minor version upgrades enabled | [rds-enable-version-update](remediation_worker/jobs/rds_enable_version_update) | +| 39. | 5c8c25f37a550e1fb6560bca | EC2 VPC default security group should restrict all access | [aws-ec2-default-security-group-traffic](remediation_worker/jobs/aws_ec2_default_security_group_traffic) | +| 40. | 5c8c260b7a550e1fb6560bf4 | IAM password policy should set a minimum length | [aws-iam-password-policy-min-length](remediation_worker/jobs/aws_iam_password_policy_min_length) | +| 41. | 5c8c26107a550e1fb6560bfc | IAM password policy should prevent password reuse | [aws-iam-password-reuse-prevention](remediation_worker/jobs/aws_iam_password_reuse_prevention) | +| 42. | 7fe4eb28-3b82-11eb-adc1-0242ac120002 | IAM server certificates that are expired should be removed | [aws-iam-server-certificate-expired](remediation_worker/jobs/aws_iam_server_certificate_expired) | ## Contributing The Secure State team welcomes welcomes contributions from the community. If you wish to contribute code and you have not signed our contributor license agreement (CLA), our bot will update the issue when you open a Pull Request. For any questions about the CLA process, please refer to our [FAQ](https://cla.vmware.com/faq). diff --git a/remediation_worker/jobs/aws_ec2_administration_ports_ingress_allowed/README.md b/remediation_worker/jobs/aws_ec2_administration_ports_ingress_allowed/README.md index e9f33e1..c2b55e8 100644 --- a/remediation_worker/jobs/aws_ec2_administration_ports_ingress_allowed/README.md +++ b/remediation_worker/jobs/aws_ec2_administration_ports_ingress_allowed/README.md @@ -14,7 +14,7 @@ Network ACL should restrict administration ports (3389 and 22) from public acces ### Prerequisites -The provided AWS credential must have access to `ec2:DeleteNetworkAcl`, `ec2:DescribeNetworkAcls` and `ec2:ReplaceNetworkAclEntry`. +The provided AWS credential must have access to `ec2:DeleteNetworkAclEntry`, `ec2:DescribeNetworkAcls`. You may find the latest example policy file [here](minimum_policy.json) diff --git a/remediation_worker/jobs/aws_ec2_administration_ports_ingress_allowed/aws_ec2_administration_ports_ingress_allowed.py b/remediation_worker/jobs/aws_ec2_administration_ports_ingress_allowed/aws_ec2_administration_ports_ingress_allowed.py index d8c32f4..1659c68 100644 --- a/remediation_worker/jobs/aws_ec2_administration_ports_ingress_allowed/aws_ec2_administration_ports_ingress_allowed.py +++ b/remediation_worker/jobs/aws_ec2_administration_ports_ingress_allowed/aws_ec2_administration_ports_ingress_allowed.py @@ -61,201 +61,6 @@ def parse(self, payload): logging.info(return_dict) return return_dict - def create_list_of_rule_nos(self, network_acl_entries): - """Creates List of Rule Numbers in the Network Acl - :param network_acl_entries: List of Network Acl entries. - :type network_acl_id: str. - :returns: List of Rule Numbers. - :rtype: list. - """ - rule_nos = [] - for entry in network_acl_entries["Entries"]: - rule_nos.append(entry["RuleNumber"]) - return rule_nos - - def create_list_of_port_range(self, network_acl_entries): - """Creates List of Port Ranges in the Network Acl - :param network_acl_entries: List of Network Acl entries. - :type network_acl_id: str. - :returns: List of Port Ranges. - :rtype: list. - """ - port_ranges = [] - for entry in network_acl_entries["Entries"]: - if "PortRange" not in entry: - continue - else: - port = (entry["PortRange"]["From"], entry["PortRange"]["To"]) - port_ranges.append(port) - return port_ranges - - def check_if_nacl_exists( - self, network_acl_entries, port_from, port_to, port_ranges - ): - """Checks if the Network ACL Entry already exists - :param network_acl_entries: List of Network Acl entries. - :param port_from: Port Range from. - :param port_to: Port Range To. - :param port_ranges: List of port ranges. - :type network_acl_entries: list - :type port_from: int - :type port_to: int - :type port_ranges: list - :returns: Boolean value indicating if the entry with given port range already exists - :rtype: bool - """ - for nacl_entry in network_acl_entries["Entries"]: - if "PortRange" not in nacl_entry: - continue - elif ( - nacl_entry["PortRange"]["From"] == port_from - and nacl_entry["PortRange"]["To"] == port_to - ): - return True - else: - continue - for port in port_ranges: - if port[0] == port_from and port[1] == port_to: - return True - return False - - def find_and_remove_port( - self, - network_acl_id, - client, - network_acl_entries, - port_no, - rule_nos, - port_ranges, - ): - """Find and remove port 22 and 3389 from Network Acl Entries - :param network_acl_id: Network Acl Id. - :param client: Instance of the AWS boto3 client. - :param network_acl_entries: List of Network Acl Entries. - :param port_no: Port No. to remove. - :param rule_nos: List of Rule Numbers. - :type rule_nos: list. - :type port_no: int. - :type network_acl_entries: list. - :type network_acl_id: str. - :type client: object. - :returns: None. - :rtype: None. - """ - for entry in network_acl_entries["Entries"]: - if ( - entry["Egress"] is False - and entry["RuleAction"] == "allow" - and entry["Protocol"] in ["6", "-1"] - and entry["CidrBlock"] == "0.0.0.0/0" - ): - if "PortRange" not in entry or entry["PortRange"] == { - "From": port_no, - "To": port_no, - }: - client.delete_network_acl_entry( - Egress=False, - NetworkAclId=network_acl_id, - RuleNumber=entry["RuleNumber"], - ) - elif ( - entry["PortRange"]["From"] < port_no - and entry["PortRange"]["To"] == port_no - ): - portrange_to = port_no - 1 - - if self.check_if_nacl_exists( - network_acl_entries, - entry["PortRange"]["From"], - portrange_to, - port_ranges, - ): - client.delete_network_acl_entry( - Egress=False, - NetworkAclId=network_acl_id, - RuleNumber=entry["RuleNumber"], - ) - else: - client.replace_network_acl_entry( - CidrBlock=entry["CidrBlock"], - Egress=entry["Egress"], - NetworkAclId=network_acl_id, - PortRange={ - "From": entry["PortRange"]["From"], - "To": portrange_to, - }, - Protocol=entry["Protocol"], - RuleAction=entry["RuleAction"], - RuleNumber=entry["RuleNumber"], - ) - - port = (entry["PortRange"]["From"], portrange_to) - port_ranges.append(port) - elif ( - entry["PortRange"]["From"] < port_no - and entry["PortRange"]["To"] > port_no - ): - rule_no = entry["RuleNumber"] + 10 - - while rule_no in rule_nos: - rule_no = rule_no + 10 - - portrange_to = port_no - 1 - - if self.check_if_nacl_exists( - network_acl_entries, - entry["PortRange"]["From"], - portrange_to, - port_ranges, - ): - client.delete_network_acl_entry( - Egress=False, - NetworkAclId=network_acl_id, - RuleNumber=entry["RuleNumber"], - ) - else: - client.replace_network_acl_entry( - CidrBlock=entry["CidrBlock"], - Egress=entry["Egress"], - NetworkAclId=network_acl_id, - PortRange={ - "From": entry["PortRange"]["From"], - "To": portrange_to, - }, - Protocol=entry["Protocol"], - RuleAction=entry["RuleAction"], - RuleNumber=entry["RuleNumber"], - ) - - port = (entry["PortRange"]["From"], portrange_to) - port_ranges.append(port) - - portrange_from = port_no + 1 - - if self.check_if_nacl_exists( - network_acl_entries, - portrange_from, - entry["PortRange"]["To"], - port_ranges, - ): - continue - else: - client.create_network_acl_entry( - CidrBlock=entry["CidrBlock"], - Egress=entry["Egress"], - NetworkAclId=network_acl_id, - PortRange={ - "From": portrange_from, - "To": entry["PortRange"]["To"], - }, - Protocol=entry["Protocol"], - RuleAction=entry["RuleAction"], - RuleNumber=rule_no, - ) - rule_nos.append(rule_no) - port = (portrange_from, entry["PortRange"]["To"]) - port_ranges.append(port) - def remediate(self, region, client, network_acl_id, cloud_account_id): """Remove Network ACL Rules that allows public access to administration ports (3389 and 22) :param region: The buckets region @@ -276,23 +81,39 @@ def remediate(self, region, client, network_acl_id, cloud_account_id): logging.info( "executing client.describe_network_acls to get network acl" ) + logging.info(" executing client.describe_network_acls") + logging.info(f" NetworkAclId: {network_acl_id}") + # List network acl details network_acl = client.describe_network_acls( NetworkAclIds=[network_acl_id] ) network_acl_entries = network_acl["NetworkAcls"][0] - #Create List of Rule Numbers - rule_nos = self.create_list_of_rule_nos(network_acl_entries) - #Create List of Port Ranges - port_ranges = self.create_list_of_port_range(network_acl_entries) - #Remove the port from the Network ACL entries - self.find_and_remove_port( - network_acl_id, - client, - network_acl_entries, - port_no, - rule_nos, - port_ranges, - ) + for entry in network_acl_entries["Entries"]: + #Searching for the ingress nacl entries with RuleAction = allow, + #protocol as tcp or all traffic, CidrBlock="0.0.0.0/0" and the + #port range inclusive of port 22 and 3389 + if ( + entry["Egress"] is False + and entry["RuleAction"] == "allow" + and entry["Protocol"] in ["6", "-1"] + and entry["CidrBlock"] == "0.0.0.0/0" + and ( + "PortRange" not in entry + or ( + entry["PortRange"]["From"] <= port_no + and entry["PortRange"]["To"] >= port_no + ) + ) + ): + # Delete nacl entry which provides public access to administration ports (3389 and 22) + logging.info(" executing client.delete_network_acl_entry") + logging.info(f" NetworkAclId: {network_acl_id}") + logging.info(f" RuleNumber: {entry['RuleNumber']}") + client.delete_network_acl_entry( + Egress=False, + NetworkAclId=network_acl_id, + RuleNumber=entry["RuleNumber"], + ) logging.info("successfully completed remediation job") except Exception as e: logging.error(f"{str(e)}") diff --git a/remediation_worker/jobs/aws_ec2_administration_ports_ingress_allowed/constraints.txt b/remediation_worker/jobs/aws_ec2_administration_ports_ingress_allowed/constraints.txt index 68a9723..98a054e 100644 --- a/remediation_worker/jobs/aws_ec2_administration_ports_ingress_allowed/constraints.txt +++ b/remediation_worker/jobs/aws_ec2_administration_ports_ingress_allowed/constraints.txt @@ -29,9 +29,9 @@ py==1.9.0 \ pluggy==0.13.1 \ --hash=sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0 \ --hash=sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d -s3transfer==0.3.4 \ - --hash=sha256:1e28620e5b444652ed752cf87c7e0cb15b0e578972568c6609f0f18212f259ed \ - --hash=sha256:7fdddb4f22275cf1d32129e21f056337fd2a80b6ccef1664528145b72c49e6d2 +s3transfer==0.5.0 \ + --hash=sha256:50ed823e1dc5868ad40c8dc92072f757aa0e653a192845c94a3b676f4a62da4c \ + --hash=sha256:9c1dc369814391a6bda20ebbf4b70a0f34630592c9aa520856bf384916af2803 six==1.15.0 \ --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \ --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced diff --git a/remediation_worker/jobs/aws_ec2_administration_ports_ingress_allowed/minimum_policy.json b/remediation_worker/jobs/aws_ec2_administration_ports_ingress_allowed/minimum_policy.json index ba9b8ba..142b9b9 100644 --- a/remediation_worker/jobs/aws_ec2_administration_ports_ingress_allowed/minimum_policy.json +++ b/remediation_worker/jobs/aws_ec2_administration_ports_ingress_allowed/minimum_policy.json @@ -6,7 +6,7 @@ "Effect": "Allow", "Action": [ "ec2:DescribeNetworkAcls", - "ec2:DeleteNetworkAcl" + "ec2:DeleteNetworkAclEntry" ], "Resource": "*" } diff --git a/remediation_worker/jobs/aws_ec2_administration_ports_ingress_allowed/requirements.txt b/remediation_worker/jobs/aws_ec2_administration_ports_ingress_allowed/requirements.txt index ae343fd..93f7845 100644 --- a/remediation_worker/jobs/aws_ec2_administration_ports_ingress_allowed/requirements.txt +++ b/remediation_worker/jobs/aws_ec2_administration_ports_ingress_allowed/requirements.txt @@ -1,6 +1,6 @@ -boto3==1.16.60 \ - --hash=sha256:10e8d9b18a8ae15677e850c7240140b9539635a03098f01dfdd75b2042d15862 \ - --hash=sha256:aee742f2a2315244fb31a507f65d8809fcd0029508c0b12be8611ddd2075b666 -botocore==1.19.60 \ - --hash=sha256:423a1a9502bd7bc5db8c6e64f9374f64d8ac18e6b870278a9ff65f59d268cd58 \ - --hash=sha256:80dd615a34c7e2c73606070a9358f7b5c1cb0c9989348306c1c9ddff45bb6ebe +boto3==1.18.4 \ + --hash=sha256:649ed1ca205f5ee0b0328d54580780aebc1a7a05681a24f6ee05253007ca48d8 \ + --hash=sha256:7079b40bd6621c54a0385a8fc11240cff4318a4d487292653e393e18254f5d94 +botocore==1.21.5 \ + --hash=sha256:0070c5e02b581db40ff5fd1b5e02db90ed88e7e861901894bd78fd998656da68 \ + --hash=sha256:bed34fe7a007180f4208b65515bab1755cdd9fcf2c6720f74ae7ecd2e707f4b7 diff --git a/remediation_worker/jobs/aws_ec2_default_security_group_traffic/README.md b/remediation_worker/jobs/aws_ec2_default_security_group_traffic/README.md new file mode 100644 index 0000000..8c2870e --- /dev/null +++ b/remediation_worker/jobs/aws_ec2_default_security_group_traffic/README.md @@ -0,0 +1,68 @@ +# Configure default Security Group to restrict all access + +This job removes all the Ingress and Egress Rules of a default Security Group to restrict all access. + +### Applicable Rule + +##### Rule ID: +5c8c25f37a550e1fb6560bca + +##### Rule Name: +EC2 VPC default security group should restrict all access + +## Getting Started + +### Prerequisites + +The provided AWS credential must have access to `ec2:DescribeSecurityGroupRules`, `ec2:RevokeSecurityGroupIngress` and `ec2:RevokeSecurityGroupEgress`. + +You may find the latest example policy file [here](minimum_policy.json) + +### Running the script + +You may run this script using following commands: +```shell script + pip install -r ../../requirements.txt + python3 aws_ec2_default_security_group_traffic.py +``` + +## Running the tests +You may run test using following command under vss-remediation-worker-job-code-python directory: +```shell script + python3 -m pytest test +``` + +## Deployment +1. Provision a Virtual Machine +Create an EC2 instance to use for the worker. The minimum required specifications are 128 MB memory and 1/2 Core CPU. +2. Setup Docker +Install Docker on the newly provisioned EC2 instance. You can refer to the [docs here](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/docker-basics.html) for more information. +3. Deploy the worker image +SSH into the EC2 instance and run the command below to deploy the worker image: +```shell script + docker run --rm -it --name worker \ + -e VSS_CLIENT_ID={ENTER CLIENT ID} + -e VSS_CLIENT_SECRET={ENTER CLIENT SECRET} \ + vmware/vss-remediation-worker:latest-python +``` + + +## Contributing +The Secure State team welcomes welcomes contributions from the community. If you wish to contribute code and you have not signed our contributor license agreement (CLA), our bot will update the issue when you open a Pull Request. For any questions about the CLA process, please refer to our [FAQ](https://cla.vmware.com/faq). +All contributions to this repository must be signed as described on that page. Your signature certifies that you wrote the patch or have the right to pass it on as an open-source patch. + +For more detailed information, refer to [CONTRIBUTING.md](../../../CONTRIBUTING.md). + +## Versioning + +We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/vmware-samples/secure-state-remediation-jobs/tags). + +## Authors + +* **VMware Secure State** - *Initial work* + +See also the list of [contributors](https://github.com/vmware-samples/secure-state-remediation-jobs/contributors) who participated in this project. + +## License + +This project is licensed under the Apache License - see the [LICENSE](https://github.com/vmware-samples/secure-state-remediation-jobs/blob/master/LICENSE.txt) file for details diff --git a/remediation_worker/jobs/aws_ec2_default_security_group_traffic/__init__.py b/remediation_worker/jobs/aws_ec2_default_security_group_traffic/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/remediation_worker/jobs/aws_ec2_default_security_group_traffic/aws_ec2_default_security_group_traffic.py b/remediation_worker/jobs/aws_ec2_default_security_group_traffic/aws_ec2_default_security_group_traffic.py new file mode 100644 index 0000000..c40a97d --- /dev/null +++ b/remediation_worker/jobs/aws_ec2_default_security_group_traffic/aws_ec2_default_security_group_traffic.py @@ -0,0 +1,123 @@ +# Copyright (c) 2020 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import json +import logging +import sys + +import boto3 + +logging.basicConfig(level=logging.INFO) + + +class DefaultSecurityGroupRemoveRules(object): + def parse(self, payload): + """Parse payload received from Remediation Service. + + :param payload: JSON string containing parameters received from the remediation service. + :type payload: str. + :returns: Dictionary of parsed parameters + :rtype: dict + :raises: Exception, JSONDecodeError + """ + remediation_entry = json.loads(payload) + notification_info = remediation_entry.get("notificationInfo", None) + finding_info = notification_info.get("FindingInfo", None) + security_group_id = finding_info.get("ObjectId", None) + + object_chain = remediation_entry["notificationInfo"]["FindingInfo"][ + "ObjectChain" + ] + object_chain_dict = json.loads(object_chain) + cloud_account_id = object_chain_dict["cloudAccountId"] + region = finding_info.get("Region") + + logging.info(f"security_group_id: {security_group_id}") + logging.info(f"region: {region}") + logging.info(f"cloud_account_id: {cloud_account_id}") + + if security_group_id is None: + raise Exception( + "Missing parameters for 'payload.notificationInfo.ObjectId'." + ) + + return { + "security_group_id": security_group_id, + "region": region, + "cloud_account_id": cloud_account_id, + } + + def remediate(self, client, security_group_id, region, cloud_account_id): + """Restrict all access for EC2 VPC default security group. + :param client: Instance of the AWS boto3 client. + :param security_group_id: The ID of the security group. + :param region: Region in which the security group exists. + :param cloud_account_id: AWS Account no. + :type security_group_id: str. + :type region: str. + :type cloud_account_id: str. + :returns: Integer signaling success or failure + :rtype: int + :raises: botocore.exceptions.ClientError + """ + try: + logging.info(" executing client.describe_security_group_rules") + logging.info(f" group-id: {security_group_id}") + security_group_rules = client.describe_security_group_rules( + Filters=[{"Name": "group-id", "Values": [security_group_id]},], + MaxResults=1000, + ) + for rule in security_group_rules["SecurityGroupRules"]: + if rule["IsEgress"]: + logging.info(" executing client.revoke_security_group_egress") + logging.info(f" GroupId: {security_group_id}") + logging.info(f" SecurityGroupRuleIds: {rule['SecurityGroupRuleId']}") + client.revoke_security_group_egress( + GroupId=security_group_id, + SecurityGroupRuleIds=[rule["SecurityGroupRuleId"]] + ) + else: + logging.info(" executing client.revoke_security_group_ingress") + logging.info(f" GroupId: {security_group_id}") + logging.info(f" SecurityGroupRuleIds: {rule['SecurityGroupRuleId']}") + client.revoke_security_group_ingress( + GroupId=security_group_id, + SecurityGroupRuleIds=[rule["SecurityGroupRuleId"]] + ) + logging.info("successfully executed remediation") + except Exception as e: + logging.error(f"{str(e)}") + + return 0 + + def run(self, args): + """Run the remediation job. + + :param args: List of arguments provided to the job. + :type args: list. + :returns: int + """ + params = self.parse(args[1]) + client = boto3.client("ec2", params["region"]) + logging.info("acquired ec2 client and parsed params - starting remediation") + rc = self.remediate(client=client, **params) + return rc + + +if __name__ == "__main__": + logging.info("aws_ec2_default_security_group_traffic.py called - running now") + obj = DefaultSecurityGroupRemoveRules() + obj.run(sys.argv) diff --git a/remediation_worker/jobs/aws_ec2_default_security_group_traffic/constraints.txt b/remediation_worker/jobs/aws_ec2_default_security_group_traffic/constraints.txt new file mode 100644 index 0000000..c5f48f9 --- /dev/null +++ b/remediation_worker/jobs/aws_ec2_default_security_group_traffic/constraints.txt @@ -0,0 +1,43 @@ +attrs==20.1.0 \ + --hash=sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a \ + --hash=sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff +docutils==0.15.2 \ + --hash=sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0 \ + --hash=sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827 \ + --hash=sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99 +iniconfig==1.1.1 \ + --hash=sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3 \ + --hash=sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32 +jmespath==0.10.0 \ + --hash=sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9 \ + --hash=sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f +more-itertools==8.4.0 \ + --hash=sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5 \ + --hash=sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2 +packaging==20.4 \ + --hash=sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8 \ + --hash=sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181 +pyparsing==2.4.7 \ + --hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1 \ + --hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b +python-dateutil==2.8.1 \ + --hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \ + --hash=sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a +py==1.9.0 \ + --hash=sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2 \ + --hash=sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342 +pluggy==0.13.1 \ + --hash=sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0 \ + --hash=sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d +s3transfer==0.3.4 \ + --hash=sha256:1e28620e5b444652ed752cf87c7e0cb15b0e578972568c6609f0f18212f259ed \ + --hash=sha256:7fdddb4f22275cf1d32129e21f056337fd2a80b6ccef1664528145b72c49e6d2 +six==1.15.0 \ + --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \ + --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced +toml==0.10.1 \ + --hash=sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f \ + --hash=sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88 +urllib3==1.26.3 \ + --hash=sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80 \ + --hash=sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73 \ No newline at end of file diff --git a/remediation_worker/jobs/aws_ec2_default_security_group_traffic/minimum_policy.json b/remediation_worker/jobs/aws_ec2_default_security_group_traffic/minimum_policy.json new file mode 100644 index 0000000..ca52fb0 --- /dev/null +++ b/remediation_worker/jobs/aws_ec2_default_security_group_traffic/minimum_policy.json @@ -0,0 +1,15 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "RemoveDefaultSecurityGroupRules", + "Effect": "Allow", + "Action": [ + "ec2:DescribeSecurityGroupRules", + "ec2:RevokeSecurityGroupIngress", + "ec2:RevokeSecurityGroupEgress" + ], + "Resource": "*" + } + ] +} \ No newline at end of file diff --git a/remediation_worker/jobs/aws_ec2_default_security_group_traffic/requirements-dev.txt b/remediation_worker/jobs/aws_ec2_default_security_group_traffic/requirements-dev.txt new file mode 100644 index 0000000..0babf10 --- /dev/null +++ b/remediation_worker/jobs/aws_ec2_default_security_group_traffic/requirements-dev.txt @@ -0,0 +1,9 @@ +-r requirements.txt +-c constraints.txt + +mock==4.0.2 \ + --hash=sha256:3f9b2c0196c60d21838f307f5825a7b86b678cedc58ab9e50a8988187b4d81e0 \ + --hash=sha256:dd33eb70232b6118298d516bbcecd26704689c386594f0f3c4f13867b2c56f72 +pytest==6.0.1 \ + --hash=sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4 \ + --hash=sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad \ No newline at end of file diff --git a/remediation_worker/jobs/aws_ec2_default_security_group_traffic/requirements.txt b/remediation_worker/jobs/aws_ec2_default_security_group_traffic/requirements.txt new file mode 100644 index 0000000..f01353d --- /dev/null +++ b/remediation_worker/jobs/aws_ec2_default_security_group_traffic/requirements.txt @@ -0,0 +1,6 @@ +boto3==1.18.4 \ + --hash=sha256:649ed1ca205f5ee0b0328d54580780aebc1a7a05681a24f6ee05253007ca48d8 \ + --hash=sha256:7079b40bd6621c54a0385a8fc11240cff4318a4d487292653e393e18254f5d94 +botocore==1.21.5 \ + --hash=sha256:0070c5e02b581db40ff5fd1b5e02db90ed88e7e861901894bd78fd998656da68 \ + --hash=sha256:bed34fe7a007180f4208b65515bab1755cdd9fcf2c6720f74ae7ecd2e707f4b7 \ No newline at end of file diff --git a/remediation_worker/jobs/aws_iam_password_policy_min_length/README.md b/remediation_worker/jobs/aws_iam_password_policy_min_length/README.md new file mode 100644 index 0000000..c487cac --- /dev/null +++ b/remediation_worker/jobs/aws_iam_password_policy_min_length/README.md @@ -0,0 +1,68 @@ +# Set minimum password length for an AWS account. + +This job sets a minimum password length to 14 for an AWS Account. + +### Applicable Rule + +##### Rule ID: +5c8c260b7a550e1fb6560bf4 + +##### Rule Name: +IAM password policy should set a minimum length + +## Getting Started + +### Prerequisites + +The provided AWS credential must have access to `iam:UpdateAccountPasswordPolicy`. + +You may find the latest example policy file [here](minimum_policy.json) + +### Running the script + +You may run this script using following commands: +```shell script + pip install -r ../../requirements.txt + python3 aws_iam_password_policy_min_length.py +``` + +## Running the tests +You may run test using following command under vss-remediation-worker-job-code-python directory: +```shell script + python3 -m pytest test +``` + +## Deployment +1. Provision a Virtual Machine +Create an EC2 instance to use for the worker. The minimum required specifications are 128 MB memory and 1/2 Core CPU. +2. Setup Docker +Install Docker on the newly provisioned EC2 instance. You can refer to the [docs here](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/docker-basics.html) for more information. +3. Deploy the worker image +SSH into the EC2 instance and run the command below to deploy the worker image: +```shell script + docker run --rm -it --name worker \ + -e VSS_CLIENT_ID={ENTER CLIENT ID} + -e VSS_CLIENT_SECRET={ENTER CLIENT SECRET} \ + vmware/vss-remediation-worker:latest-python +``` + + +## Contributing +The Secure State team welcomes welcomes contributions from the community. If you wish to contribute code and you have not signed our contributor license agreement (CLA), our bot will update the issue when you open a Pull Request. For any questions about the CLA process, please refer to our [FAQ](https://cla.vmware.com/faq). +All contributions to this repository must be signed as described on that page. Your signature certifies that you wrote the patch or have the right to pass it on as an open-source patch. + +For more detailed information, refer to [CONTRIBUTING.md](../../../CONTRIBUTING.md). + +## Versioning + +We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/vmware-samples/secure-state-remediation-jobs/tags). + +## Authors + +* **VMware Secure State** - *Initial work* + +See also the list of [contributors](https://github.com/vmware-samples/secure-state-remediation-jobs/contributors) who participated in this project. + +## License + +This project is licensed under the Apache License - see the [LICENSE](https://github.com/vmware-samples/secure-state-remediation-jobs/blob/master/LICENSE.txt) file for details diff --git a/remediation_worker/jobs/aws_iam_password_policy_min_length/__init__.py b/remediation_worker/jobs/aws_iam_password_policy_min_length/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/remediation_worker/jobs/aws_iam_password_policy_min_length/aws_iam_password_policy_min_length.py b/remediation_worker/jobs/aws_iam_password_policy_min_length/aws_iam_password_policy_min_length.py new file mode 100644 index 0000000..0c07e4b --- /dev/null +++ b/remediation_worker/jobs/aws_iam_password_policy_min_length/aws_iam_password_policy_min_length.py @@ -0,0 +1,93 @@ +# Copyright (c) 2020 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import json +import logging +import sys + +import boto3 + +logging.basicConfig(level=logging.INFO) + + +class SetPasswordMinimumLength(object): + def parse(self, payload): + """Parse payload received from Remediation Service. + + :param payload: JSON string containing parameters sent to the remediation job. + :type payload: str. + :returns: Dictionary of parsed parameters + :rtype: dict + :raises: Exception, JSONDecodeError + """ + remediation_entry = json.loads(payload) + notification_info = remediation_entry.get("notificationInfo", None) + finding_info = notification_info.get("FindingInfo", None) + account_id = finding_info.get("ObjectId", None) + + if account_id is None: + raise Exception( + "Missing parameters for 'payload.notificationInfo.ObjectId'." + ) + + logging.info("parsed params") + logging.info(f" account_id: {account_id}") + + return_dict = { + "account_id": account_id, + } + logging.info(return_dict) + return return_dict + + def remediate(self, client, account_id): + """Set Minimum Password length for an account + :param client: Instance of the AWS boto3 client. + :param account_id: AWS Account Id. + :type client: object. + :type account_id: str. + :returns: Integer signaling success or failure + :rtype: int + :raises: botocore.exceptions.ClientError + """ + try: + logging.info( + f"Setting Minimum Password length for the account: {account_id}" + ) + logging.info("executing client.update_account_password_policy") + client.update_account_password_policy(MinimumPasswordLength=14) + logging.info("successfully completed remediation job") + except Exception as e: + logging.error(f"{str(e)}") + return 0 + + def run(self, args): + """Run the remediation job. + + :param args: List of arguments provided to the job. + :type args: list. + :returns: int + """ + params = self.parse(args[1]) + client = boto3.client("iam") + logging.info("acquired iam client and parsed params - starting remediation") + rc = self.remediate(client=client, **params) + return rc + + +if __name__ == "__main__": + logging.info("aws_iam_password_policy_min_length.py called - running now") + obj = SetPasswordMinimumLength() + obj.run(sys.argv) diff --git a/remediation_worker/jobs/aws_iam_password_policy_min_length/constraints.txt b/remediation_worker/jobs/aws_iam_password_policy_min_length/constraints.txt new file mode 100644 index 0000000..68a9723 --- /dev/null +++ b/remediation_worker/jobs/aws_iam_password_policy_min_length/constraints.txt @@ -0,0 +1,43 @@ +attrs==20.1.0 \ + --hash=sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a \ + --hash=sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff +docutils==0.15.2 \ + --hash=sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0 \ + --hash=sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827 \ + --hash=sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99 +iniconfig==1.1.1 \ + --hash=sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3 \ + --hash=sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32 +jmespath==0.10.0 \ + --hash=sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9 \ + --hash=sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f +more-itertools==8.4.0 \ + --hash=sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5 \ + --hash=sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2 +packaging==20.4 \ + --hash=sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8 \ + --hash=sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181 +pyparsing==2.4.7 \ + --hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1 \ + --hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b +python-dateutil==2.8.1 \ + --hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \ + --hash=sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a +py==1.9.0 \ + --hash=sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2 \ + --hash=sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342 +pluggy==0.13.1 \ + --hash=sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0 \ + --hash=sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d +s3transfer==0.3.4 \ + --hash=sha256:1e28620e5b444652ed752cf87c7e0cb15b0e578972568c6609f0f18212f259ed \ + --hash=sha256:7fdddb4f22275cf1d32129e21f056337fd2a80b6ccef1664528145b72c49e6d2 +six==1.15.0 \ + --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \ + --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced +toml==0.10.1 \ + --hash=sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f \ + --hash=sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88 +urllib3==1.26.3 \ + --hash=sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80 \ + --hash=sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73 diff --git a/remediation_worker/jobs/aws_iam_password_policy_min_length/minimum_policy.json b/remediation_worker/jobs/aws_iam_password_policy_min_length/minimum_policy.json new file mode 100644 index 0000000..77586ab --- /dev/null +++ b/remediation_worker/jobs/aws_iam_password_policy_min_length/minimum_policy.json @@ -0,0 +1,13 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "SetMinimumPasswordLength", + "Effect": "Allow", + "Action": [ + "iam:UpdateAccountPasswordPolicy" + ], + "Resource": "*" + } + ] +} diff --git a/remediation_worker/jobs/aws_iam_password_policy_min_length/requirements-dev.txt b/remediation_worker/jobs/aws_iam_password_policy_min_length/requirements-dev.txt new file mode 100644 index 0000000..cf03a93 --- /dev/null +++ b/remediation_worker/jobs/aws_iam_password_policy_min_length/requirements-dev.txt @@ -0,0 +1,9 @@ +-r requirements.txt +-c constraints.txt + +mock==4.0.2 \ + --hash=sha256:3f9b2c0196c60d21838f307f5825a7b86b678cedc58ab9e50a8988187b4d81e0 \ + --hash=sha256:dd33eb70232b6118298d516bbcecd26704689c386594f0f3c4f13867b2c56f72 +pytest==6.0.1 \ + --hash=sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4 \ + --hash=sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad diff --git a/remediation_worker/jobs/aws_iam_password_policy_min_length/requirements.txt b/remediation_worker/jobs/aws_iam_password_policy_min_length/requirements.txt new file mode 100644 index 0000000..ae343fd --- /dev/null +++ b/remediation_worker/jobs/aws_iam_password_policy_min_length/requirements.txt @@ -0,0 +1,6 @@ +boto3==1.16.60 \ + --hash=sha256:10e8d9b18a8ae15677e850c7240140b9539635a03098f01dfdd75b2042d15862 \ + --hash=sha256:aee742f2a2315244fb31a507f65d8809fcd0029508c0b12be8611ddd2075b666 +botocore==1.19.60 \ + --hash=sha256:423a1a9502bd7bc5db8c6e64f9374f64d8ac18e6b870278a9ff65f59d268cd58 \ + --hash=sha256:80dd615a34c7e2c73606070a9358f7b5c1cb0c9989348306c1c9ddff45bb6ebe diff --git a/remediation_worker/jobs/aws_iam_password_reuse_prevention/README.md b/remediation_worker/jobs/aws_iam_password_reuse_prevention/README.md new file mode 100644 index 0000000..71a2b88 --- /dev/null +++ b/remediation_worker/jobs/aws_iam_password_reuse_prevention/README.md @@ -0,0 +1,68 @@ +# Set Password Reuse Prevention Policy for an AWS Account + +This job sets password reuse prevention policy for an AWS Account. The number of passwords to remember per user will be set to 24. + +### Applicable Rule + +##### Rule ID: +5c8c26107a550e1fb6560bfc + +##### Rule Name: +IAM password policy should prevent password reuse + +## Getting Started + +### Prerequisites + +The provided AWS credential must have access to `iam:UpdateAccountPasswordPolicy`. + +You may find the latest example policy file [here](minimum_policy.json) + +### Running the script + +You may run this script using following commands: +```shell script + pip install -r ../../requirements.txt + python3 aws_iam_password_reuse_prevention.py +``` + +## Running the tests +You may run test using following command under vss-remediation-worker-job-code-python directory: +```shell script + python3 -m pytest test +``` + +## Deployment +1. Provision a Virtual Machine +Create an EC2 instance to use for the worker. The minimum required specifications are 128 MB memory and 1/2 Core CPU. +2. Setup Docker +Install Docker on the newly provisioned EC2 instance. You can refer to the [docs here](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/docker-basics.html) for more information. +3. Deploy the worker image +SSH into the EC2 instance and run the command below to deploy the worker image: +```shell script + docker run --rm -it --name worker \ + -e VSS_CLIENT_ID={ENTER CLIENT ID} + -e VSS_CLIENT_SECRET={ENTER CLIENT SECRET} \ + vmware/vss-remediation-worker:latest-python +``` + + +## Contributing +The Secure State team welcomes welcomes contributions from the community. If you wish to contribute code and you have not signed our contributor license agreement (CLA), our bot will update the issue when you open a Pull Request. For any questions about the CLA process, please refer to our [FAQ](https://cla.vmware.com/faq). +All contributions to this repository must be signed as described on that page. Your signature certifies that you wrote the patch or have the right to pass it on as an open-source patch. + +For more detailed information, refer to [CONTRIBUTING.md](../../../CONTRIBUTING.md). + +## Versioning + +We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/vmware-samples/secure-state-remediation-jobs/tags). + +## Authors + +* **VMware Secure State** - *Initial work* + +See also the list of [contributors](https://github.com/vmware-samples/secure-state-remediation-jobs/contributors) who participated in this project. + +## License + +This project is licensed under the Apache License - see the [LICENSE](https://github.com/vmware-samples/secure-state-remediation-jobs/blob/master/LICENSE.txt) file for details diff --git a/remediation_worker/jobs/aws_iam_password_reuse_prevention/__init__.py b/remediation_worker/jobs/aws_iam_password_reuse_prevention/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/remediation_worker/jobs/aws_iam_password_reuse_prevention/aws_iam_password_reuse_prevention.py b/remediation_worker/jobs/aws_iam_password_reuse_prevention/aws_iam_password_reuse_prevention.py new file mode 100644 index 0000000..c7a0f3c --- /dev/null +++ b/remediation_worker/jobs/aws_iam_password_reuse_prevention/aws_iam_password_reuse_prevention.py @@ -0,0 +1,93 @@ +# Copyright (c) 2020 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import json +import logging +import sys + +import boto3 + +logging.basicConfig(level=logging.INFO) + + +class SetPasswordReusePrevention(object): + def parse(self, payload): + """Parse payload received from Remediation Service. + + :param payload: JSON string containing parameters sent to the remediation job. + :type payload: str. + :returns: Dictionary of parsed parameters + :rtype: dict + :raises: Exception, JSONDecodeError + """ + remediation_entry = json.loads(payload) + notification_info = remediation_entry.get("notificationInfo", None) + finding_info = notification_info.get("FindingInfo", None) + account_id = finding_info.get("ObjectId", None) + + if account_id is None: + raise Exception( + "Missing parameters for 'payload.notificationInfo.ObjectId'." + ) + + logging.info("parsed params") + logging.info(f" account_id: {account_id}") + + return_dict = { + "account_id": account_id, + } + logging.info(return_dict) + return return_dict + + def remediate(self, client, account_id): + """Set Password Reuse Prevention Policy for an AWS account + :param client: Instance of the AWS boto3 client. + :param account_id: AWS Account Id. + :type client: object. + :type account_id: str. + :returns: Integer signaling success or failure + :rtype: int + :raises: botocore.exceptions.ClientError + """ + try: + logging.info( + f"Setting Password Reuse Prevention Policy for the account: {account_id}" + ) + logging.info("executing client.update_account_password_policy") + client.update_account_password_policy(PasswordReusePrevention=24) + logging.info("successfully completed remediation job") + except Exception as e: + logging.error(f"{str(e)}") + return 0 + + def run(self, args): + """Run the remediation job. + + :param args: List of arguments provided to the job. + :type args: list. + :returns: int + """ + params = self.parse(args[1]) + client = boto3.client("iam") + logging.info("acquired iam client and parsed params - starting remediation") + rc = self.remediate(client=client, **params) + return rc + + +if __name__ == "__main__": + logging.info("aws_iam_password_reuse_prevention.py called - running now") + obj = SetPasswordReusePrevention() + obj.run(sys.argv) diff --git a/remediation_worker/jobs/aws_iam_password_reuse_prevention/constraints.txt b/remediation_worker/jobs/aws_iam_password_reuse_prevention/constraints.txt new file mode 100644 index 0000000..68a9723 --- /dev/null +++ b/remediation_worker/jobs/aws_iam_password_reuse_prevention/constraints.txt @@ -0,0 +1,43 @@ +attrs==20.1.0 \ + --hash=sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a \ + --hash=sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff +docutils==0.15.2 \ + --hash=sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0 \ + --hash=sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827 \ + --hash=sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99 +iniconfig==1.1.1 \ + --hash=sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3 \ + --hash=sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32 +jmespath==0.10.0 \ + --hash=sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9 \ + --hash=sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f +more-itertools==8.4.0 \ + --hash=sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5 \ + --hash=sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2 +packaging==20.4 \ + --hash=sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8 \ + --hash=sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181 +pyparsing==2.4.7 \ + --hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1 \ + --hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b +python-dateutil==2.8.1 \ + --hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \ + --hash=sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a +py==1.9.0 \ + --hash=sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2 \ + --hash=sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342 +pluggy==0.13.1 \ + --hash=sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0 \ + --hash=sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d +s3transfer==0.3.4 \ + --hash=sha256:1e28620e5b444652ed752cf87c7e0cb15b0e578972568c6609f0f18212f259ed \ + --hash=sha256:7fdddb4f22275cf1d32129e21f056337fd2a80b6ccef1664528145b72c49e6d2 +six==1.15.0 \ + --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \ + --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced +toml==0.10.1 \ + --hash=sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f \ + --hash=sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88 +urllib3==1.26.3 \ + --hash=sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80 \ + --hash=sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73 diff --git a/remediation_worker/jobs/aws_iam_password_reuse_prevention/minimum_policy.json b/remediation_worker/jobs/aws_iam_password_reuse_prevention/minimum_policy.json new file mode 100644 index 0000000..d95ddce --- /dev/null +++ b/remediation_worker/jobs/aws_iam_password_reuse_prevention/minimum_policy.json @@ -0,0 +1,13 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "SetPasswordReusePolicy", + "Effect": "Allow", + "Action": [ + "iam:UpdateAccountPasswordPolicy" + ], + "Resource": "*" + } + ] +} diff --git a/remediation_worker/jobs/aws_iam_password_reuse_prevention/requirements-dev.txt b/remediation_worker/jobs/aws_iam_password_reuse_prevention/requirements-dev.txt new file mode 100644 index 0000000..cf03a93 --- /dev/null +++ b/remediation_worker/jobs/aws_iam_password_reuse_prevention/requirements-dev.txt @@ -0,0 +1,9 @@ +-r requirements.txt +-c constraints.txt + +mock==4.0.2 \ + --hash=sha256:3f9b2c0196c60d21838f307f5825a7b86b678cedc58ab9e50a8988187b4d81e0 \ + --hash=sha256:dd33eb70232b6118298d516bbcecd26704689c386594f0f3c4f13867b2c56f72 +pytest==6.0.1 \ + --hash=sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4 \ + --hash=sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad diff --git a/remediation_worker/jobs/aws_iam_password_reuse_prevention/requirements.txt b/remediation_worker/jobs/aws_iam_password_reuse_prevention/requirements.txt new file mode 100644 index 0000000..ae343fd --- /dev/null +++ b/remediation_worker/jobs/aws_iam_password_reuse_prevention/requirements.txt @@ -0,0 +1,6 @@ +boto3==1.16.60 \ + --hash=sha256:10e8d9b18a8ae15677e850c7240140b9539635a03098f01dfdd75b2042d15862 \ + --hash=sha256:aee742f2a2315244fb31a507f65d8809fcd0029508c0b12be8611ddd2075b666 +botocore==1.19.60 \ + --hash=sha256:423a1a9502bd7bc5db8c6e64f9374f64d8ac18e6b870278a9ff65f59d268cd58 \ + --hash=sha256:80dd615a34c7e2c73606070a9358f7b5c1cb0c9989348306c1c9ddff45bb6ebe diff --git a/remediation_worker/jobs/aws_iam_server_certificate_expired/README.md b/remediation_worker/jobs/aws_iam_server_certificate_expired/README.md new file mode 100644 index 0000000..3d1622d --- /dev/null +++ b/remediation_worker/jobs/aws_iam_server_certificate_expired/README.md @@ -0,0 +1,70 @@ +# Delete Expired IAM Server Certificate + +This job deletes expired IAM Server Certificate. + +**NOTE -** Deleting the certificate could have implications for your application if you are using an expired server certificate with Elastic Load Balancing, Cloudfront etc. One has to make configurations at respective services to ensure there is no interruption in application. + +### Applicable Rule + +##### Rule ID: +7fe4eb28-3b82-11eb-adc1-0242ac120002 + +##### Rule Name: +IAM server certificates that are expired should be removed + +## Getting Started + +### Prerequisites + +The provided AWS credential must have access to `iam:DeleteServerCertificate`. + +You may find the latest example policy file [here](minimum_policy.json) + +### Running the script + +You may run this script using following commands: +```shell script + pip install -r ../../requirements.txt + python3 aws_iam_server_certificate_expired.py +``` + +## Running the tests +You may run test using following command under vss-remediation-worker-job-code-python directory: +```shell script + python3 -m pytest test +``` + +## Deployment +1. Provision a Virtual Machine +Create an EC2 instance to use for the worker. The minimum required specifications are 128 MB memory and 1/2 Core CPU. +2. Setup Docker +Install Docker on the newly provisioned EC2 instance. You can refer to the [docs here](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/docker-basics.html) for more information. +3. Deploy the worker image +SSH into the EC2 instance and run the command below to deploy the worker image: +```shell script + docker run --rm -it --name worker \ + -e VSS_CLIENT_ID={ENTER CLIENT ID} + -e VSS_CLIENT_SECRET={ENTER CLIENT SECRET} \ + vmware/vss-remediation-worker:latest-python +``` + + +## Contributing +The Secure State team welcomes welcomes contributions from the community. If you wish to contribute code and you have not signed our contributor license agreement (CLA), our bot will update the issue when you open a Pull Request. For any questions about the CLA process, please refer to our [FAQ](https://cla.vmware.com/faq). +All contributions to this repository must be signed as described on that page. Your signature certifies that you wrote the patch or have the right to pass it on as an open-source patch. + +For more detailed information, refer to [CONTRIBUTING.md](../../../CONTRIBUTING.md). + +## Versioning + +We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/vmware-samples/secure-state-remediation-jobs/tags). + +## Authors + +* **VMware Secure State** - *Initial work* + +See also the list of [contributors](https://github.com/vmware-samples/secure-state-remediation-jobs/contributors) who participated in this project. + +## License + +This project is licensed under the Apache License - see the [LICENSE](https://github.com/vmware-samples/secure-state-remediation-jobs/blob/master/LICENSE.txt) file for details diff --git a/remediation_worker/jobs/aws_iam_server_certificate_expired/__init__.py b/remediation_worker/jobs/aws_iam_server_certificate_expired/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/remediation_worker/jobs/aws_iam_server_certificate_expired/aws_iam_server_certificate_expired.py b/remediation_worker/jobs/aws_iam_server_certificate_expired/aws_iam_server_certificate_expired.py new file mode 100644 index 0000000..e302d6d --- /dev/null +++ b/remediation_worker/jobs/aws_iam_server_certificate_expired/aws_iam_server_certificate_expired.py @@ -0,0 +1,92 @@ +# Copyright (c) 2020 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import json +import logging +import sys + +import boto3 + +logging.basicConfig(level=logging.INFO) + + +class DeleteExpiredServerCertificate(object): + def parse(self, payload): + """Parse payload received from Remediation Service. + + :param payload: JSON string containing parameters sent to the remediation job. + :type payload: str. + :returns: Dictionary of parsed parameters + :rtype: dict + :raises: Exception, JSONDecodeError + """ + remediation_entry = json.loads(payload) + notification_info = remediation_entry.get("notificationInfo", None) + finding_info = notification_info.get("FindingInfo", None) + certificate_name = finding_info.get("ObjectId", None) + + if certificate_name is None: + raise Exception( + "Missing parameters for 'payload.notificationInfo.ObjectId'." + ) + + logging.info("parsed params") + logging.info(f" certificate_name: {certificate_name}") + + return_dict = { + "certificate_name": certificate_name, + } + logging.info(return_dict) + return return_dict + + def remediate(self, client, certificate_name): + """Deleting Expired Server Certificate + :param client: Instance of the AWS boto3 client. + :param certificate_name: Certificate name. + :type client: object. + :type certificate_name: str. + :returns: Integer signaling success or failure + :rtype: int + :raises: botocore.exceptions.ClientError + """ + try: + logging.info(f"Deleting Expired Server Certificate: {certificate_name}") + logging.info("executing client.delete_server_certificate") + client.delete_server_certificate(ServerCertificateName=certificate_name) + logging.info("successfully completed remediation job") + except Exception as e: + logging.error(f"{str(e)}") + raise + return 0 + + def run(self, args): + """Run the remediation job. + + :param args: List of arguments provided to the job. + :type args: list. + :returns: int + """ + params = self.parse(args[1]) + client = boto3.client("iam") + logging.info("acquired iam client and parsed params - starting remediation") + rc = self.remediate(client=client, **params) + return rc + + +if __name__ == "__main__": + logging.info("aws_iam_server_certificate_expired.py called - running now") + obj = DeleteExpiredServerCertificate() + obj.run(sys.argv) diff --git a/remediation_worker/jobs/aws_iam_server_certificate_expired/constraints.txt b/remediation_worker/jobs/aws_iam_server_certificate_expired/constraints.txt new file mode 100644 index 0000000..68a9723 --- /dev/null +++ b/remediation_worker/jobs/aws_iam_server_certificate_expired/constraints.txt @@ -0,0 +1,43 @@ +attrs==20.1.0 \ + --hash=sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a \ + --hash=sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff +docutils==0.15.2 \ + --hash=sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0 \ + --hash=sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827 \ + --hash=sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99 +iniconfig==1.1.1 \ + --hash=sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3 \ + --hash=sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32 +jmespath==0.10.0 \ + --hash=sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9 \ + --hash=sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f +more-itertools==8.4.0 \ + --hash=sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5 \ + --hash=sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2 +packaging==20.4 \ + --hash=sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8 \ + --hash=sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181 +pyparsing==2.4.7 \ + --hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1 \ + --hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b +python-dateutil==2.8.1 \ + --hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \ + --hash=sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a +py==1.9.0 \ + --hash=sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2 \ + --hash=sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342 +pluggy==0.13.1 \ + --hash=sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0 \ + --hash=sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d +s3transfer==0.3.4 \ + --hash=sha256:1e28620e5b444652ed752cf87c7e0cb15b0e578972568c6609f0f18212f259ed \ + --hash=sha256:7fdddb4f22275cf1d32129e21f056337fd2a80b6ccef1664528145b72c49e6d2 +six==1.15.0 \ + --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \ + --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced +toml==0.10.1 \ + --hash=sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f \ + --hash=sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88 +urllib3==1.26.3 \ + --hash=sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80 \ + --hash=sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73 diff --git a/remediation_worker/jobs/aws_iam_server_certificate_expired/minimum_policy.json b/remediation_worker/jobs/aws_iam_server_certificate_expired/minimum_policy.json new file mode 100644 index 0000000..683f089 --- /dev/null +++ b/remediation_worker/jobs/aws_iam_server_certificate_expired/minimum_policy.json @@ -0,0 +1,13 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "DeleteExpiredServerCertificate", + "Effect": "Allow", + "Action": [ + "iam:DeleteServerCertificate" + ], + "Resource": "*" + } + ] +} diff --git a/remediation_worker/jobs/aws_iam_server_certificate_expired/requirements-dev.txt b/remediation_worker/jobs/aws_iam_server_certificate_expired/requirements-dev.txt new file mode 100644 index 0000000..cf03a93 --- /dev/null +++ b/remediation_worker/jobs/aws_iam_server_certificate_expired/requirements-dev.txt @@ -0,0 +1,9 @@ +-r requirements.txt +-c constraints.txt + +mock==4.0.2 \ + --hash=sha256:3f9b2c0196c60d21838f307f5825a7b86b678cedc58ab9e50a8988187b4d81e0 \ + --hash=sha256:dd33eb70232b6118298d516bbcecd26704689c386594f0f3c4f13867b2c56f72 +pytest==6.0.1 \ + --hash=sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4 \ + --hash=sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad diff --git a/remediation_worker/jobs/aws_iam_server_certificate_expired/requirements.txt b/remediation_worker/jobs/aws_iam_server_certificate_expired/requirements.txt new file mode 100644 index 0000000..ae343fd --- /dev/null +++ b/remediation_worker/jobs/aws_iam_server_certificate_expired/requirements.txt @@ -0,0 +1,6 @@ +boto3==1.16.60 \ + --hash=sha256:10e8d9b18a8ae15677e850c7240140b9539635a03098f01dfdd75b2042d15862 \ + --hash=sha256:aee742f2a2315244fb31a507f65d8809fcd0029508c0b12be8611ddd2075b666 +botocore==1.19.60 \ + --hash=sha256:423a1a9502bd7bc5db8c6e64f9374f64d8ac18e6b870278a9ff65f59d268cd58 \ + --hash=sha256:80dd615a34c7e2c73606070a9358f7b5c1cb0c9989348306c1c9ddff45bb6ebe diff --git a/remediation_worker/jobs/azure_mysql_enforce_ssl_connection_enable/azure_mysql_enforce_ssl_connection_enable.py b/remediation_worker/jobs/azure_mysql_enforce_ssl_connection_enable/azure_mysql_enforce_ssl_connection_enable.py index d0d1512..c6366c1 100644 --- a/remediation_worker/jobs/azure_mysql_enforce_ssl_connection_enable/azure_mysql_enforce_ssl_connection_enable.py +++ b/remediation_worker/jobs/azure_mysql_enforce_ssl_connection_enable/azure_mysql_enforce_ssl_connection_enable.py @@ -77,11 +77,17 @@ def remediate(self, client, resource_group_name, mysql_server_name): logging.info(f" resource_group_name={resource_group_name}") logging.info(f" server_name={mysql_server_name}") - client.servers.begin_update( + poller = client.servers.begin_update( resource_group_name=resource_group_name, server_name=mysql_server_name, parameters=ServerUpdateParameters(ssl_enforcement="Enabled"), ) + while not poller.done(): + time.sleep(5) + status = poller.status() + logging.info(f"The remediation job status: {status}") + poller.result() + except Exception as e: logging.error(f"{str(e)}") raise diff --git a/remediation_worker/jobs/azure_postgresql_allow_access_to_azure_service_disabled/azure_postgresql_allow_access_to_azure_service_disabled.py b/remediation_worker/jobs/azure_postgresql_allow_access_to_azure_service_disabled/azure_postgresql_allow_access_to_azure_service_disabled.py index ca2725c..084ff74 100644 --- a/remediation_worker/jobs/azure_postgresql_allow_access_to_azure_service_disabled/azure_postgresql_allow_access_to_azure_service_disabled.py +++ b/remediation_worker/jobs/azure_postgresql_allow_access_to_azure_service_disabled/azure_postgresql_allow_access_to_azure_service_disabled.py @@ -70,11 +70,17 @@ def remediate(self, client, resource_group_name, postgre_server_name): logging.info(f" server_name={postgre_server_name}") logging.info(f" firewall_rule_name=AllowAllWindowsAzureIps") - client.firewall_rules.begin_delete( + poller = client.firewall_rules.begin_delete( resource_group_name=resource_group_name, server_name=postgre_server_name, firewall_rule_name="AllowAllWindowsAzureIps", ) + while not poller.done(): + time.sleep(5) + status = poller.status() + logging.info(f"The remediation job status: {status}") + poller.result() + except Exception as e: logging.error(f"{str(e)}") raise diff --git a/remediation_worker/jobs/azure_postgresql_enforce_ssl_connection_enable/azure_postgresql_enforce_ssl_connection_enable.py b/remediation_worker/jobs/azure_postgresql_enforce_ssl_connection_enable/azure_postgresql_enforce_ssl_connection_enable.py index 3b530f2..e0f0fb0 100644 --- a/remediation_worker/jobs/azure_postgresql_enforce_ssl_connection_enable/azure_postgresql_enforce_ssl_connection_enable.py +++ b/remediation_worker/jobs/azure_postgresql_enforce_ssl_connection_enable/azure_postgresql_enforce_ssl_connection_enable.py @@ -70,11 +70,17 @@ def remediate(self, client, resource_group_name, postgre_server_name): logging.info(f" resource_group_name={resource_group_name}") logging.info(f" server_name={postgre_server_name}") - client.servers.begin_update( + poller = client.servers.begin_update( resource_group_name=resource_group_name, server_name=postgre_server_name, parameters=ServerUpdateParameters(ssl_enforcement="Enabled"), ) + while not poller.done(): + time.sleep(5) + status = poller.status() + logging.info(f"The remediation job status: {status}") + poller.result() + except Exception as e: logging.error(f"{str(e)}") raise diff --git a/remediation_worker/jobs/azure_sql_auditing_on_server/azure_sql_auditing_on_server.py b/remediation_worker/jobs/azure_sql_auditing_on_server/azure_sql_auditing_on_server.py index df05e04..a90691f 100644 --- a/remediation_worker/jobs/azure_sql_auditing_on_server/azure_sql_auditing_on_server.py +++ b/remediation_worker/jobs/azure_sql_auditing_on_server/azure_sql_auditing_on_server.py @@ -12,6 +12,7 @@ from azure.mgmt.monitor import MonitorClient from azure.identity import ClientSecretCredential from azure.graphrbac import GraphRbacManagementClient +from azure.core.exceptions import HttpResponseError from azure.mgmt.keyvault import KeyVaultManagementClient from azure.common.credentials import ServicePrincipalCredentials from azure.mgmt.authorization import AuthorizationManagementClient @@ -70,7 +71,6 @@ MAX_COUNT_REGION = 6 MAX_COUNT_COMPONENT = 4 - def generate_name(region, subscription_id, resource_group_name): """Generates a name for the resource :param region: location in which the resource exists @@ -571,7 +571,7 @@ def create_server_blob_auditing_policy( logging.info(f" resource_group_name={resource_group_name}") logging.info(f" server_name={sql_server_name}") - client.server_blob_auditing_policies.create_or_update( + poller = client.server_blob_auditing_policies.create_or_update( resource_group_name=resource_group_name, server_name=sql_server_name, parameters=ServerBlobAuditingPolicy( @@ -579,6 +579,11 @@ def create_server_blob_auditing_policy( storage_endpoint=f"https://{stg_account_name}.blob.core.windows.net/", ), ) + while not poller.done(): + time.sleep(5) + status = poller.status() + logging.info(f"The remediation job status: {status}") + poller.result() def ensure_identity_assigned( self, client, resource_group_name, sql_server_name, region @@ -741,16 +746,28 @@ def remediate( ) scope = f"/subscriptions/{subscription_id}/resourceGroups/{resource_group_name}/providers/Microsoft.Storage/storageAccounts/{stg_account.name}" - # Assign Storage Blob Contributer role to the SQL Server - self.create_role_assignment( - stg_name, - subscription_id, - client_authorization, - guid, - scope, - principalId, - sql_server_name, + role_definition_id = f"/subscriptions/{subscription_id}/providers/Microsoft.Authorization/roleDefinitions/ba92f5b4-2d11-453d-a403-e96b0029c9fe" + status = self.check_role_assignment( + client_authorization, scope, principalId, role_definition_id ) + try: + if status is False: + # Assign Storage Blob Contributer role to the SQL Server + self.create_role_assignment( + stg_name, + subscription_id, + client_authorization, + guid, + scope, + principalId, + sql_server_name, + ) + except HttpResponseError as e: + if e.status_code == 400: + logging.error(f"{str(e)}") + logging.info("In case of PrincipalNotFound error, retry remediating this finding after few minutes") + return -1 + else: # If the Storage Account Created by CHSS exists stg_id = stg_account.id @@ -763,17 +780,23 @@ def remediate( status = self.check_role_assignment( client_authorization, scope, principalId, role_definition_id ) - if status is False: - # If role assignment does not exists, assign the Storage Blob Contributer Role to the SQL Server - self.create_role_assignment( - stg_account.name, - subscription_id, - client_authorization, - guid, - scope, - principalId, - sql_server_name, - ) + try: + if status is False: + # If role assignment does not exists, assign the Storage Blob Contributer Role to the SQL Server + self.create_role_assignment( + stg_account.name, + subscription_id, + client_authorization, + guid, + scope, + principalId, + sql_server_name, + ) + except HttpResponseError as e: + if e.status_code == 400: + logging.error(f"{str(e)}") + logging.info("In case of PrincipalNotFound error, retry remediating this finding after few minutes") + return -1 else: logging.error("Resource group name not found") return -1 diff --git a/remediation_worker/jobs/azure_sql_tde_protector_encrypted_cmk/README.md b/remediation_worker/jobs/azure_sql_tde_protector_encrypted_cmk/README.md index ca1841e..28667b0 100644 --- a/remediation_worker/jobs/azure_sql_tde_protector_encrypted_cmk/README.md +++ b/remediation_worker/jobs/azure_sql_tde_protector_encrypted_cmk/README.md @@ -27,6 +27,7 @@ The provided Azure service principal must have the following permissions: `Microsoft.KeyVault/vaults/accessPolicies/write`, `Microsoft.Sql/servers/read`, `Microsoft.Sql/servers/write`, +`Microsoft.Sql/servers/encryptionProtector/read`, `Microsoft.Sql/servers/encryptionProtector/write`, `Microsoft.Sql/servers/keys/write` `Microsoft.Sql/servers/keys/read`. diff --git a/remediation_worker/jobs/azure_sql_tde_protector_encrypted_cmk/azure_sql_tde_protector_encrypted_cmk.py b/remediation_worker/jobs/azure_sql_tde_protector_encrypted_cmk/azure_sql_tde_protector_encrypted_cmk.py index 2138dce..f7c437f 100644 --- a/remediation_worker/jobs/azure_sql_tde_protector_encrypted_cmk/azure_sql_tde_protector_encrypted_cmk.py +++ b/remediation_worker/jobs/azure_sql_tde_protector_encrypted_cmk/azure_sql_tde_protector_encrypted_cmk.py @@ -688,7 +688,7 @@ def remediate( ).result() # Update the SQL TDE protector to encrypt using cmk - client.encryption_protectors.begin_create_or_update( + poller = client.encryption_protectors.begin_create_or_update( resource_group_name=resource_group_name, server_name=sql_server_name, encryption_protector_name="current", @@ -696,6 +696,12 @@ def remediate( server_key_name=server_key_name, server_key_type="AzureKeyVault" ), ) + while not poller.done(): + time.sleep(5) + status = poller.status() + logging.info(f"The remediation job status: {status}") + poller.result() + except Exception as e: logging.error(f"{str(e)}") raise diff --git a/remediation_worker/jobs/azure_sql_tde_protector_encrypted_cmk/minimum_permissions.json b/remediation_worker/jobs/azure_sql_tde_protector_encrypted_cmk/minimum_permissions.json index 38866a6..aaf427f 100644 --- a/remediation_worker/jobs/azure_sql_tde_protector_encrypted_cmk/minimum_permissions.json +++ b/remediation_worker/jobs/azure_sql_tde_protector_encrypted_cmk/minimum_permissions.json @@ -19,6 +19,7 @@ "Microsoft.KeyVault/vaults/accessPolicies/write", "Microsoft.Sql/servers/read", "Microsoft.Sql/servers/write", + "Microsoft.Sql/servers/encryptionProtector/read", "Microsoft.Sql/servers/encryptionProtector/write", "Microsoft.Sql/servers/keys/write", "Microsoft.Sql/servers/keys/read" diff --git a/remediation_worker/jobs/azure_sql_threat_detection_on_server/azure_sql_threat_detection_on_server.py b/remediation_worker/jobs/azure_sql_threat_detection_on_server/azure_sql_threat_detection_on_server.py index 58742e4..f1f9f31 100644 --- a/remediation_worker/jobs/azure_sql_threat_detection_on_server/azure_sql_threat_detection_on_server.py +++ b/remediation_worker/jobs/azure_sql_threat_detection_on_server/azure_sql_threat_detection_on_server.py @@ -70,13 +70,19 @@ def remediate(self, client, resource_group_name, sql_server_name): logging.info(f" resource_group_name={resource_group_name}") logging.info(f" server_name={sql_server_name}") - client.server_security_alert_policies.create_or_update( + poller = client.server_security_alert_policies.create_or_update( resource_group_name=resource_group_name, server_name=sql_server_name, parameters=ServerSecurityAlertPolicy( state=SecurityAlertPolicyState.enabled ), ) + while not poller.done(): + time.sleep(5) + status = poller.status() + logging.info(f"The remediation job status: {status}") + poller.result() + except Exception as e: logging.error(f"{str(e)}") raise diff --git a/remediation_worker/jobs/azure_sql_threat_detection_types_all_server/azure_sql_threat_detection_types_all_server.py b/remediation_worker/jobs/azure_sql_threat_detection_types_all_server/azure_sql_threat_detection_types_all_server.py index a383c68..292155d 100644 --- a/remediation_worker/jobs/azure_sql_threat_detection_types_all_server/azure_sql_threat_detection_types_all_server.py +++ b/remediation_worker/jobs/azure_sql_threat_detection_types_all_server/azure_sql_threat_detection_types_all_server.py @@ -73,13 +73,19 @@ def remediate(self, client, resource_group_name, sql_server_name): logging.info(f" resource_group_name={resource_group_name}") logging.info(f" server_name={sql_server_name}") - client.server_security_alert_policies.create_or_update( + poller = client.server_security_alert_policies.create_or_update( resource_group_name=resource_group_name, server_name=sql_server_name, parameters=ServerSecurityAlertPolicy( state=SecurityAlertPolicyState.enabled, disabled_alerts=[] ), ) + while not poller.done(): + time.sleep(5) + status = poller.status() + logging.info(f"The remediation job status: {status}") + poller.result() + except Exception as e: logging.error(f"{str(e)}") raise diff --git a/remediation_worker/jobs/ebs_private_snapshot/README.md b/remediation_worker/jobs/ebs_private_snapshot/README.md new file mode 100644 index 0000000..a2a39f0 --- /dev/null +++ b/remediation_worker/jobs/ebs_private_snapshot/README.md @@ -0,0 +1,75 @@ +# Configure the EBS volume snapshot as private. + +This job makes an EBS snapshot private by removing the keyword 'all' from GroupNames. + +## Getting Started + +##### Rule ID: +2cdb8877-7ac3-4483-9ed0-1e792171d125 + +##### Rule Name: +EBS volume snapshot should be private + +### Prerequisites + +The provided AWS credential must have permissions that listed in the policy file [here](minimum_policy.json) + +### Running the script + +You may run this script using following commands: +```shell script + pip install -r ../../requirements.txt + python3 ebs_private_snapshot.py "`cat finding.json`" +``` + where finding.json has volume id and region info: + ```json + { + "notificationInfo": { + "FindingInfo": { + "ObjectId": "snap-047ed496ef688a585", + "Region": "us-west-2" + } + } +} +``` + +## Running the tests +You may run test using following command under vss-remediation-worker-job-code-python directory: +```shell script + python3 -m pytest test +``` + +## Deployment +1. Provision a Virtual Machine +Create an EC2 instance to use for the worker. The minimum required specifications are 128 MB memory and 1/2 Core CPU. +2. Setup Docker +Install Docker on the newly provisioned EC2 instance. You can refer to the [docs here](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/docker-basics.html) for more information. +3. Deploy the worker image +SSH into the EC2 instance and run the command below to deploy the worker image: +```shell script + docker run --rm -it --name worker \ + -e VSS_CLIENT_ID={ENTER CLIENT ID} + -e VSS_CLIENT_SECRET={ENTER CLIENT SECRET} \ + vmware/vss-remediation-worker:latest-python +``` + + +## Contributing +The Secure State team welcomes welcomes contributions from the community. If you wish to contribute code and you have not signed our contributor license agreement (CLA), our bot will update the issue when you open a Pull Request. For any questions about the CLA process, please refer to our [FAQ](https://cla.vmware.com/faq). +All contributions to this repository must be signed as described on that page. Your signature certifies that you wrote the patch or have the right to pass it on as an open-source patch. + +For more detailed information, refer to [CONTRIBUTING.md](../../../CONTRIBUTING.md). + +## Versioning + +We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/vmware-samples/secure-state-remediation-jobs/tags). + +## Authors + +* **VMware Secure State** - *Initial work* + +See also the list of [contributors](https://github.com/vmware-samples/secure-state-remediation-jobs/contributors) who participated in this project. + +## License + +This project is licensed under the Apache License - see the [LICENSE](https://github.com/vmware-samples/secure-state-remediation-jobs/blob/master/LICENSE.txt) file for details diff --git a/remediation_worker/jobs/ebs_private_snapshot/__init__.py b/remediation_worker/jobs/ebs_private_snapshot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/remediation_worker/jobs/ebs_private_snapshot/constraints.txt b/remediation_worker/jobs/ebs_private_snapshot/constraints.txt new file mode 100644 index 0000000..6b211d2 --- /dev/null +++ b/remediation_worker/jobs/ebs_private_snapshot/constraints.txt @@ -0,0 +1,43 @@ +docutils==0.15.2 \ + --hash=sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0 \ + --hash=sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827 \ + --hash=sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99 +jmespath==0.10.0 \ + --hash=sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9 \ + --hash=sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f +python-dateutil==2.8.1 \ + --hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \ + --hash=sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a +s3transfer==0.3.3 \ + --hash=sha256:2482b4259524933a022d59da830f51bd746db62f047d6eb213f2f8855dcb8a13 \ + --hash=sha256:921a37e2aefc64145e7b73d50c71bb4f26f46e4c9f414dc648c6245ff92cf7db +urllib3==1.25.9 \ + --hash=sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527 \ + --hash=sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115 +six==1.15.0 \ + --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \ + --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced +packaging==20.4 \ + --hash=sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8 \ + --hash=sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181 +attrs==19.3.0 \ + --hash=sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c \ + --hash=sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72 +more-itertools==8.4.0 \ + --hash=sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5 \ + --hash=sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2 +pluggy==0.13.1 \ + --hash=sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0 \ + --hash=sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d +py==1.9.0 \ + --hash=sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2 \ + --hash=sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342 +toml==0.10.1 \ + --hash=sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f \ + --hash=sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88 +iniconfig==1.0.1 \ + --hash=sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437 \ + --hash=sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69 +pyparsing==2.4.7 \ + --hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1 \ + --hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b diff --git a/remediation_worker/jobs/ebs_private_snapshot/ebs_private_snapshot.py b/remediation_worker/jobs/ebs_private_snapshot/ebs_private_snapshot.py new file mode 100644 index 0000000..9ab4d21 --- /dev/null +++ b/remediation_worker/jobs/ebs_private_snapshot/ebs_private_snapshot.py @@ -0,0 +1,132 @@ +# Copyright (c) 2021 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import logging +import sys + +import boto3 +from botocore.exceptions import ClientError + +logging.basicConfig(level=logging.INFO) + + +class EBSPrivateSnapshot: + def parse(self, payload): + """Parse payload received from Remediation Service. + :param payload: JSON string containing parameters sent to the remediation job. + :type payload: str. + :returns: Dictionary of parsed parameters + :rtype: dict + :raises: Exception, JSONDecodeError + """ + logging.debug(payload) + remediation_entry = json.loads(payload) + notification_info = remediation_entry.get("notificationInfo", None) + finding_info = notification_info.get("FindingInfo", None) + + snapshot_id = finding_info.get("ObjectId", None) + region = finding_info.get("Region", None) + + if snapshot_id is None: + logging.error("Missing parameters for 'SNAPSHOT_ID'.") + raise Exception("Missing parameters for 'SNAPSHOT_ID'.") + + if region is None: + logging.error("Missing parameters for 'REGION'.") + raise Exception("Missing parameters for 'REGION'.") + + logging.debug("parsed params") + logging.debug(f" snapshot_id: {snapshot_id}") + logging.info(f" region: {region}") + + return { + "snapshot_id": snapshot_id, + "region": region + } + + def remediate(self, client, snapshot_id, region): + + """Set snapshots to private. + :param client: Instance of the AWS boto3 client. + :param snapshot_id: The id of the EBS snapshot. + :param region: The region of the EBS snapshot. + :type snapshot_id: str. + :returns: Bool signaling success or failure + :rtype: bool + """ + logging.info("Removing Public access by executing client.describe_snapshot_attribute") + logging.info("Attribute = createVolumePermission") + logging.info(f"SnapshotId={snapshot_id}") + + try: + # Get the permissions of the snapshot, no exeption expected + snapshot_permissions = client.describe_snapshot_attribute( + Attribute='createVolumePermission', + SnapshotId=snapshot_id + ).get('CreateVolumePermissions') + + logging.info(f"permission={snapshot_permissions}") + + # if createVolumePermission has "Group":"all", remove it + if snapshot_permissions: + for permission in snapshot_permissions: + if 'all' in permission['Group']: + logging.info(f"Found Public Snapshot: {snapshot_id}") + + # remove all from the groupname, no exception expected + client.modify_snapshot_attribute( + Attribute='createVolumePermission', + GroupNames=[ + 'all', + ], + OperationType='remove', + SnapshotId=snapshot_id, + ) + logging.info(f"Public access removed from {snapshot_id}") + return 0 + else: + logging.info(f"Snapshot {snapshot_id} is not public, exiting") + return 1 + + except ClientError as state_err: + error = state_err.response["Error"]["Code"] + logging.error(f"Got Exception={error}") + return 1 + except Exception as e: + error = "Receiving other exceptions {0}".format(str(e)) + logging.error(error) + return 1 + + + def run(self, args): + """Run the remediation job. + :param args: List of arguments provided to the job. + :type args: list. + :returns: int + """ + params = self.parse(args[1]) + client = boto3.client("ec2", region_name=params['region']) + + logging.debug( + "acquired ec2 client and parsed params - starting remediation." + ) + + return self.remediate(client=client, **params) + + +if __name__ == "__main__": + logging.info("ebs_private_snapshot.py called - running now") + obj = EBSPrivateSnapshot() + obj.run(sys.argv) diff --git a/remediation_worker/jobs/ebs_private_snapshot/minimum_policy.json b/remediation_worker/jobs/ebs_private_snapshot/minimum_policy.json new file mode 100644 index 0000000..a033b19 --- /dev/null +++ b/remediation_worker/jobs/ebs_private_snapshot/minimum_policy.json @@ -0,0 +1,14 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "ModifyPrivateSnapShot", + "Effect": "Allow", + "Action": [ + "ec2:DescribeSnapshotAttribute", + "ec2:ModifySnapshotAttribute" + ], + "Resource": "*" + } + ] +} diff --git a/remediation_worker/jobs/ebs_private_snapshot/requirements-dev.txt b/remediation_worker/jobs/ebs_private_snapshot/requirements-dev.txt new file mode 100644 index 0000000..9412e93 --- /dev/null +++ b/remediation_worker/jobs/ebs_private_snapshot/requirements-dev.txt @@ -0,0 +1,6 @@ +-r requirements.txt +-c constraints.txt + +pytest==6.0.1 \ + --hash=sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4 \ + --hash=sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad diff --git a/remediation_worker/jobs/ebs_private_snapshot/requirements.txt b/remediation_worker/jobs/ebs_private_snapshot/requirements.txt new file mode 100644 index 0000000..b239388 --- /dev/null +++ b/remediation_worker/jobs/ebs_private_snapshot/requirements.txt @@ -0,0 +1,6 @@ +boto3==1.14.9 \ + --hash=sha256:185f7b36c16f76e501d8dfc5cd209113426e078e4968dd13cc355c916bc99597 \ + --hash=sha256:51243ba0e976343ca0b98bb4a15fc3d588526220f6ba45bfed7ea45472b1e033 +botocore==1.17.9 \ + --hash=sha256:7dd59bc766d567ca83bc6113aa139d92ba447738ccdfcd40788848553d329a52 \ + --hash=sha256:cd4bb2d96ff2ec6bf4fbcdb2f241d0fb6ba1e7955b4721cf1d81f13db02768b6 diff --git a/remediation_worker/jobs/ec2_close_port_1433/README.md b/remediation_worker/jobs/ec2_close_port_1433/README.md new file mode 100644 index 0000000..5154e09 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_1433/README.md @@ -0,0 +1,53 @@ +# Close Port 1433 for all Security Groups associated with an EC2 Instance + +This job blocks public access to port 1433 for both IPv4 and IPv6 for all security groups associated with an EC2 instance by removing all the ingress security group rules containing port 1433 in the port range and source as "0.0.0.0/0" or "::/0". + +### Applicable Rule + +##### Rule ID: +5c8c26417a550e1fb6560c3d + +##### Rule Name: +EC2 instance should restrict public access to SQL Server port (1433) + +## Getting Started + +### Prerequisites + +The provided AWS credential must have access to `ec2:DescribeInstances`, `ec2:RevokeSecurityGroupIngress`, `ec2:DescribeSecurityGroupRules`. + +You may find the latest example policy file [here](minimum_policy.json) + +### Running the script + +You may run this script using following commands: +```shell script + pip install -r ../../requirements.txt + python3 ec2_close_port_1433.py +``` + +## Running the tests +You may run test using following command under vss-remediation-worker-job-code-python directory: +```shell script + python3 -m pytest +``` + +## Contributing +The Secure State team welcomes welcomes contributions from the community. If you wish to contribute code and you have not signed our contributor license agreement (CLA), our bot will update the issue when you open a Pull Request. For any questions about the CLA process, please refer to our [FAQ](https://cla.vmware.com/faq). +All contributions to this repository must be signed as described on that page. Your signature certifies that you wrote the patch or have the right to pass it on as an open-source patch. + +For more detailed information, refer to [CONTRIBUTING.md](../../../CONTRIBUTING.md). + +## Versioning + +We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/vmware-samples/secure-state-remediation-jobs/tags). + +## Authors + +* **VMware Secure State** - *Initial work* + +See also the list of [contributors](https://github.com/vmware-samples/secure-state-remediation-jobs/contributors) who participated in this project. + +## License + +This project is licensed under the Apache License - see the [LICENSE](https://github.com/vmware-samples/secure-state-remediation-jobs/blob/master/LICENSE.txt) file for details diff --git a/remediation_worker/jobs/ec2_close_port_1433/__init__.py b/remediation_worker/jobs/ec2_close_port_1433/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/remediation_worker/jobs/ec2_close_port_1433/constraints.txt b/remediation_worker/jobs/ec2_close_port_1433/constraints.txt new file mode 100644 index 0000000..0d9b3c2 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_1433/constraints.txt @@ -0,0 +1,43 @@ +docutils==0.15.2 \ + --hash=sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0 \ + --hash=sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827 \ + --hash=sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99 +jmespath==0.10.0 \ + --hash=sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9 \ + --hash=sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f +python-dateutil==2.8.1 \ + --hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \ + --hash=sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a +s3transfer==0.5.0 \ + --hash=sha256:50ed823e1dc5868ad40c8dc92072f757aa0e653a192845c94a3b676f4a62da4c \ + --hash=sha256:9c1dc369814391a6bda20ebbf4b70a0f34630592c9aa520856bf384916af2803 +urllib3==1.25.9 \ + --hash=sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527 \ + --hash=sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115 +six==1.15.0 \ + --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \ + --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced +packaging==20.4 \ + --hash=sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8 \ + --hash=sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181 +attrs==19.3.0 \ + --hash=sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c \ + --hash=sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72 +more-itertools==8.4.0 \ + --hash=sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5 \ + --hash=sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2 +pluggy==0.13.1 \ + --hash=sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0 \ + --hash=sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d +py==1.9.0 \ + --hash=sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2 \ + --hash=sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342 +toml==0.10.1 \ + --hash=sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f \ + --hash=sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88 +iniconfig==1.0.1 \ + --hash=sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437 \ + --hash=sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69 +pyparsing==2.4.7 \ + --hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1 \ + --hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b diff --git a/remediation_worker/jobs/ec2_close_port_1433/ec2_close_port_1433.py b/remediation_worker/jobs/ec2_close_port_1433/ec2_close_port_1433.py new file mode 100644 index 0000000..189efe1 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_1433/ec2_close_port_1433.py @@ -0,0 +1,131 @@ +# Copyright (c) 2020 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations +import json +import logging +import sys + +import boto3 + +logging.basicConfig(level=logging.INFO) + + +class EC2ClosePort1433(object): + def parse(self, payload): + """Parse payload received from Remediation Service. + + :param payload: JSON string containing parameters received from the remediation service. + :type payload: str. + :returns: Dictionary of parsed parameters + :rtype: dict + :raises: Exception, JSONDecodeError + """ + remediation_entry = json.loads(payload) + notification_info = remediation_entry.get("notificationInfo", None) + + finding_info = notification_info.get("FindingInfo", None) + instance_id = finding_info.get("ObjectId", None) + + if instance_id is None: + logging.error("Missing parameters for 'payload.notificationInfo.ObjectId'.") + raise Exception( + "Missing parameters for 'payload.notificationInfo.ObjectId'." + ) + + region = finding_info.get("Region", None) + if region is None: + logging.warning("no region specified - defaulting to us-east-1") + region = "us-east-1" + + logging.info("parsed params") + logging.info(f" instance_id: {instance_id}") + logging.info(f" region: {region}") + + return {"instance_id": instance_id}, region + + def remediate(self, client, instance_id): + """Block public access to port 1433 of all security groups attached to an EC2 instance + by removing all the rules with port 1433 in the port range + :param client: Instance of the AWS boto3 client. + :param instance_id: The ID of the EC2 instance. + :type instance_id: str + :returns: Integer signaling success or failure + :rtype: int + :raises: botocore.exceptions.ClientError + """ + try: + port = 1433 + logging.info(" executing client.describe_instances") + logging.info(f" InstanceId: {instance_id}") + # Extract security group Id + security_groups = client.describe_instances(InstanceIds=[instance_id])[ + "Reservations" + ][0]["Instances"][0]["SecurityGroups"] + for sg_info in security_groups: + security_group_id = sg_info["GroupId"] + logging.info(" executing client.describe_security_group_rules") + logging.info(f" group-id: {security_group_id}") + # List all the security group rules + security_group_rules = client.describe_security_group_rules( + Filters=[{"Name": "group-id", "Values": [security_group_id]},], + MaxResults=1000, + ) + for rule in security_group_rules["SecurityGroupRules"]: + if ( + rule["IpProtocol"] == "tcp" + and rule["IsEgress"] is False + and rule["FromPort"] <= port + and rule["ToPort"] >= port + and ( + ("CidrIpv4" in rule and rule["CidrIpv4"] == "0.0.0.0/0") + or ("CidrIpv6" in rule and rule["CidrIpv6"] == "::/0") + ) + ): + # Removes Ingress security group rule containing port 1433 in the range with + # protocol 'tcp', source '0.0.0.0/0' or '::/0' + logging.info( + " executing client.revoke_security_group_ingress" + ) + logging.info(f" GroupId: {security_group_id}") + logging.info( + f" SecurityGroupRuleIds: {rule['SecurityGroupRuleId']}" + ) + client.revoke_security_group_ingress( + GroupId=security_group_id, + SecurityGroupRuleIds=[rule["SecurityGroupRuleId"]], + ) + logging.info("successfully executed remediation") + except Exception as e: + logging.error(f"{str(e)}") + return 0 + + def run(self, args): + """Run the remediation job. + + :param args: List of arguments provided to the job. + :type args: list. + :returns: int + """ + params, region = self.parse(args[1]) + client = boto3.client("ec2", region_name=region) + logging.info("acquired ec2 client and parsed params - starting remediation") + rc = self.remediate(client=client, **params) + return rc + + +if __name__ == "__main__": + logging.info(f"{sys.argv[0]} called - running now") + obj = EC2ClosePort1433() + obj.run(sys.argv) diff --git a/remediation_worker/jobs/ec2_close_port_1433/minimum_policy.json b/remediation_worker/jobs/ec2_close_port_1433/minimum_policy.json new file mode 100644 index 0000000..e91af08 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_1433/minimum_policy.json @@ -0,0 +1,15 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "EC2ClosePort22", + "Effect": "Allow", + "Action": [ + "ec2:DescribeInstances", + "ec2:RevokeSecurityGroupIngress", + "ec2:DescribeSecurityGroupRules" + ], + "Resource": "*" + } + ] +} diff --git a/remediation_worker/jobs/ec2_close_port_1433/requirements-dev.txt b/remediation_worker/jobs/ec2_close_port_1433/requirements-dev.txt new file mode 100644 index 0000000..9412e93 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_1433/requirements-dev.txt @@ -0,0 +1,6 @@ +-r requirements.txt +-c constraints.txt + +pytest==6.0.1 \ + --hash=sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4 \ + --hash=sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad diff --git a/remediation_worker/jobs/ec2_close_port_1433/requirements.txt b/remediation_worker/jobs/ec2_close_port_1433/requirements.txt new file mode 100644 index 0000000..93f7845 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_1433/requirements.txt @@ -0,0 +1,6 @@ +boto3==1.18.4 \ + --hash=sha256:649ed1ca205f5ee0b0328d54580780aebc1a7a05681a24f6ee05253007ca48d8 \ + --hash=sha256:7079b40bd6621c54a0385a8fc11240cff4318a4d487292653e393e18254f5d94 +botocore==1.21.5 \ + --hash=sha256:0070c5e02b581db40ff5fd1b5e02db90ed88e7e861901894bd78fd998656da68 \ + --hash=sha256:bed34fe7a007180f4208b65515bab1755cdd9fcf2c6720f74ae7ecd2e707f4b7 diff --git a/remediation_worker/jobs/ec2_close_port_1521/README.md b/remediation_worker/jobs/ec2_close_port_1521/README.md new file mode 100644 index 0000000..5133eca --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_1521/README.md @@ -0,0 +1,54 @@ +# Close Port 1521 for all Security Groups associated with an EC2 Instance + +This job blocks public access to port 1521 for both IPv4 and IPv6 for all security groups associated with an EC2 instance by removing all the ingress security group rules containing port 1521 in the port range and source as "0.0.0.0/0" or "::/0". + +### Applicable Rule + +##### Rule ID: +5c8c26417a550e1fb6560c3e + +##### Rule Name: +EC2 instance should restrict public access to Oracle SQL port (1521) + + +## Getting Started + +### Prerequisites + +The provided AWS credential must have access to `ec2:DescribeInstances`, `ec2:RevokeSecurityGroupIngress`, `ec2:DescribeSecurityGroupRules`. + +You may find the latest example policy file [here](minimum_policy.json) + +### Running the script + +You may run this script using following commands: +```shell script + pip install -r ../../requirements.txt + python3 ec2_close_port_1521.py +``` + +## Running the tests +You may run test using following command under vss-remediation-worker-job-code-python directory: +```shell script + python3 -m pytest +``` + +## Contributing +The Secure State team welcomes welcomes contributions from the community. If you wish to contribute code and you have not signed our contributor license agreement (CLA), our bot will update the issue when you open a Pull Request. For any questions about the CLA process, please refer to our [FAQ](https://cla.vmware.com/faq). +All contributions to this repository must be signed as described on that page. Your signature certifies that you wrote the patch or have the right to pass it on as an open-source patch. + +For more detailed information, refer to [CONTRIBUTING.md](../../../CONTRIBUTING.md). + +## Versioning + +We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/vmware-samples/secure-state-remediation-jobs/tags). + +## Authors + +* **VMware Secure State** - *Initial work* + +See also the list of [contributors](https://github.com/vmware-samples/secure-state-remediation-jobs/contributors) who participated in this project. + +## License + +This project is licensed under the Apache License - see the [LICENSE](https://github.com/vmware-samples/secure-state-remediation-jobs/blob/master/LICENSE.txt) file for details diff --git a/remediation_worker/jobs/ec2_close_port_1521/__init__.py b/remediation_worker/jobs/ec2_close_port_1521/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/remediation_worker/jobs/ec2_close_port_1521/constraints.txt b/remediation_worker/jobs/ec2_close_port_1521/constraints.txt new file mode 100644 index 0000000..0d9b3c2 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_1521/constraints.txt @@ -0,0 +1,43 @@ +docutils==0.15.2 \ + --hash=sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0 \ + --hash=sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827 \ + --hash=sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99 +jmespath==0.10.0 \ + --hash=sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9 \ + --hash=sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f +python-dateutil==2.8.1 \ + --hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \ + --hash=sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a +s3transfer==0.5.0 \ + --hash=sha256:50ed823e1dc5868ad40c8dc92072f757aa0e653a192845c94a3b676f4a62da4c \ + --hash=sha256:9c1dc369814391a6bda20ebbf4b70a0f34630592c9aa520856bf384916af2803 +urllib3==1.25.9 \ + --hash=sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527 \ + --hash=sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115 +six==1.15.0 \ + --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \ + --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced +packaging==20.4 \ + --hash=sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8 \ + --hash=sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181 +attrs==19.3.0 \ + --hash=sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c \ + --hash=sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72 +more-itertools==8.4.0 \ + --hash=sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5 \ + --hash=sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2 +pluggy==0.13.1 \ + --hash=sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0 \ + --hash=sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d +py==1.9.0 \ + --hash=sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2 \ + --hash=sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342 +toml==0.10.1 \ + --hash=sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f \ + --hash=sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88 +iniconfig==1.0.1 \ + --hash=sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437 \ + --hash=sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69 +pyparsing==2.4.7 \ + --hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1 \ + --hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b diff --git a/remediation_worker/jobs/ec2_close_port_1521/ec2_close_port_1521.py b/remediation_worker/jobs/ec2_close_port_1521/ec2_close_port_1521.py new file mode 100644 index 0000000..e7f3832 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_1521/ec2_close_port_1521.py @@ -0,0 +1,131 @@ +# Copyright (c) 2020 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations +import json +import logging +import sys + +import boto3 + +logging.basicConfig(level=logging.INFO) + + +class EC2ClosePort1521(object): + def parse(self, payload): + """Parse payload received from Remediation Service. + + :param payload: JSON string containing parameters received from the remediation service. + :type payload: str. + :returns: Dictionary of parsed parameters + :rtype: dict + :raises: Exception, JSONDecodeError + """ + remediation_entry = json.loads(payload) + notification_info = remediation_entry.get("notificationInfo", None) + + finding_info = notification_info.get("FindingInfo", None) + instance_id = finding_info.get("ObjectId", None) + + if instance_id is None: + logging.error("Missing parameters for 'payload.notificationInfo.ObjectId'.") + raise Exception( + "Missing parameters for 'payload.notificationInfo.ObjectId'." + ) + + region = finding_info.get("Region", None) + if region is None: + logging.warning("no region specified - defaulting to us-east-1") + region = "us-east-1" + + logging.info("parsed params") + logging.info(f" instance_id: {instance_id}") + logging.info(f" region: {region}") + + return {"instance_id": instance_id}, region + + def remediate(self, client, instance_id): + """Block public access to port 1521 of all security groups attached to an EC2 instance + by removing all the rules with port 1521 in the port range + :param client: Instance of the AWS boto3 client. + :param instance_id: The ID of the EC2 instance. + :type instance_id: str + :returns: Integer signaling success or failure + :rtype: int + :raises: botocore.exceptions.ClientError + """ + try: + port = 1521 + logging.info(" executing client.describe_instances") + logging.info(f" InstanceId: {instance_id}") + # Extract security group Id + security_groups = client.describe_instances(InstanceIds=[instance_id])[ + "Reservations" + ][0]["Instances"][0]["SecurityGroups"] + for sg_info in security_groups: + security_group_id = sg_info["GroupId"] + logging.info(" executing client.describe_security_group_rules") + logging.info(f" group-id: {security_group_id}") + # List all the security group rules + security_group_rules = client.describe_security_group_rules( + Filters=[{"Name": "group-id", "Values": [security_group_id]},], + MaxResults=1000, + ) + for rule in security_group_rules["SecurityGroupRules"]: + if ( + rule["IpProtocol"] == "tcp" + and rule["IsEgress"] is False + and rule["FromPort"] <= port + and rule["ToPort"] >= port + and ( + ("CidrIpv4" in rule and rule["CidrIpv4"] == "0.0.0.0/0") + or ("CidrIpv6" in rule and rule["CidrIpv6"] == "::/0") + ) + ): + # Removes Ingress security group rule containing port 1521 in the range with + # protocol 'tcp', source '0.0.0.0/0' or '::/0' + logging.info( + " executing client.revoke_security_group_ingress" + ) + logging.info(f" GroupId: {security_group_id}") + logging.info( + f" SecurityGroupRuleIds: {rule['SecurityGroupRuleId']}" + ) + client.revoke_security_group_ingress( + GroupId=security_group_id, + SecurityGroupRuleIds=[rule["SecurityGroupRuleId"]], + ) + logging.info("successfully executed remediation") + except Exception as e: + logging.error(f"{str(e)}") + return 0 + + def run(self, args): + """Run the remediation job. + + :param args: List of arguments provided to the job. + :type args: list. + :returns: int + """ + params, region = self.parse(args[1]) + client = boto3.client("ec2", region_name=region) + logging.info("acquired ec2 client and parsed params - starting remediation") + rc = self.remediate(client=client, **params) + return rc + + +if __name__ == "__main__": + logging.info(f"{sys.argv[0]} called - running now") + obj = EC2ClosePort1521() + obj.run(sys.argv) diff --git a/remediation_worker/jobs/ec2_close_port_1521/minimum_policy.json b/remediation_worker/jobs/ec2_close_port_1521/minimum_policy.json new file mode 100644 index 0000000..e91af08 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_1521/minimum_policy.json @@ -0,0 +1,15 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "EC2ClosePort22", + "Effect": "Allow", + "Action": [ + "ec2:DescribeInstances", + "ec2:RevokeSecurityGroupIngress", + "ec2:DescribeSecurityGroupRules" + ], + "Resource": "*" + } + ] +} diff --git a/remediation_worker/jobs/ec2_close_port_1521/requirements-dev.txt b/remediation_worker/jobs/ec2_close_port_1521/requirements-dev.txt new file mode 100644 index 0000000..9412e93 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_1521/requirements-dev.txt @@ -0,0 +1,6 @@ +-r requirements.txt +-c constraints.txt + +pytest==6.0.1 \ + --hash=sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4 \ + --hash=sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad diff --git a/remediation_worker/jobs/ec2_close_port_1521/requirements.txt b/remediation_worker/jobs/ec2_close_port_1521/requirements.txt new file mode 100644 index 0000000..93f7845 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_1521/requirements.txt @@ -0,0 +1,6 @@ +boto3==1.18.4 \ + --hash=sha256:649ed1ca205f5ee0b0328d54580780aebc1a7a05681a24f6ee05253007ca48d8 \ + --hash=sha256:7079b40bd6621c54a0385a8fc11240cff4318a4d487292653e393e18254f5d94 +botocore==1.21.5 \ + --hash=sha256:0070c5e02b581db40ff5fd1b5e02db90ed88e7e861901894bd78fd998656da68 \ + --hash=sha256:bed34fe7a007180f4208b65515bab1755cdd9fcf2c6720f74ae7ecd2e707f4b7 diff --git a/remediation_worker/jobs/ec2_close_port_20/README.md b/remediation_worker/jobs/ec2_close_port_20/README.md new file mode 100644 index 0000000..c6106fc --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_20/README.md @@ -0,0 +1,53 @@ +# Close Port 20 for all Security Groups associated with an EC2 Instance + +This job blocks public access to port 20 for both IPv4 and IPv6 for all security groups associated with an EC2 instance by removing all the ingress security group rules containing port 20 in the port range and source as "0.0.0.0/0" or "::/0". + +### Applicable Rule + +##### Rule ID: +5c8c263d7a550e1fb6560c39 + +##### Rule Name: +EC2 instance should restrict public access to FTP data port (20) + +## Getting Started + +### Prerequisites + +The provided AWS credential must have access to `ec2:DescribeInstances`, `ec2:RevokeSecurityGroupIngress`, `ec2:DescribeSecurityGroupRules`. + +You may find the latest example policy file [here](minimum_policy.json) + +### Running the script + +You may run this script using following commands: +```shell script + pip install -r ../../requirements.txt + python3 ec2_close_port_20.py +``` + +## Running the tests +You may run test using following command under vss-remediation-worker-job-code-python directory: +```shell script + python3 -m pytest +``` + +## Contributing +The Secure State team welcomes welcomes contributions from the community. If you wish to contribute code and you have not signed our contributor license agreement (CLA), our bot will update the issue when you open a Pull Request. For any questions about the CLA process, please refer to our [FAQ](https://cla.vmware.com/faq). +All contributions to this repository must be signed as described on that page. Your signature certifies that you wrote the patch or have the right to pass it on as an open-source patch. + +For more detailed information, refer to [CONTRIBUTING.md](../../../CONTRIBUTING.md). + +## Versioning + +We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/vmware-samples/secure-state-remediation-jobs/tags). + +## Authors + +* **VMware Secure State** - *Initial work* + +See also the list of [contributors](https://github.com/vmware-samples/secure-state-remediation-jobs/contributors) who participated in this project. + +## License + +This project is licensed under the Apache License - see the [LICENSE](https://github.com/vmware-samples/secure-state-remediation-jobs/blob/master/LICENSE.txt) file for details diff --git a/remediation_worker/jobs/ec2_close_port_20/__init__.py b/remediation_worker/jobs/ec2_close_port_20/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/remediation_worker/jobs/ec2_close_port_20/constraints.txt b/remediation_worker/jobs/ec2_close_port_20/constraints.txt new file mode 100644 index 0000000..0d9b3c2 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_20/constraints.txt @@ -0,0 +1,43 @@ +docutils==0.15.2 \ + --hash=sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0 \ + --hash=sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827 \ + --hash=sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99 +jmespath==0.10.0 \ + --hash=sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9 \ + --hash=sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f +python-dateutil==2.8.1 \ + --hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \ + --hash=sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a +s3transfer==0.5.0 \ + --hash=sha256:50ed823e1dc5868ad40c8dc92072f757aa0e653a192845c94a3b676f4a62da4c \ + --hash=sha256:9c1dc369814391a6bda20ebbf4b70a0f34630592c9aa520856bf384916af2803 +urllib3==1.25.9 \ + --hash=sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527 \ + --hash=sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115 +six==1.15.0 \ + --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \ + --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced +packaging==20.4 \ + --hash=sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8 \ + --hash=sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181 +attrs==19.3.0 \ + --hash=sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c \ + --hash=sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72 +more-itertools==8.4.0 \ + --hash=sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5 \ + --hash=sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2 +pluggy==0.13.1 \ + --hash=sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0 \ + --hash=sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d +py==1.9.0 \ + --hash=sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2 \ + --hash=sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342 +toml==0.10.1 \ + --hash=sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f \ + --hash=sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88 +iniconfig==1.0.1 \ + --hash=sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437 \ + --hash=sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69 +pyparsing==2.4.7 \ + --hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1 \ + --hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b diff --git a/remediation_worker/jobs/ec2_close_port_20/ec2_close_port_20.py b/remediation_worker/jobs/ec2_close_port_20/ec2_close_port_20.py new file mode 100644 index 0000000..9f0bd1d --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_20/ec2_close_port_20.py @@ -0,0 +1,131 @@ +# Copyright (c) 2020 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations +import json +import logging +import sys + +import boto3 + +logging.basicConfig(level=logging.INFO) + + +class EC2ClosePort20(object): + def parse(self, payload): + """Parse payload received from Remediation Service. + + :param payload: JSON string containing parameters received from the remediation service. + :type payload: str. + :returns: Dictionary of parsed parameters + :rtype: dict + :raises: Exception, JSONDecodeError + """ + remediation_entry = json.loads(payload) + notification_info = remediation_entry.get("notificationInfo", None) + + finding_info = notification_info.get("FindingInfo", None) + instance_id = finding_info.get("ObjectId", None) + + if instance_id is None: + logging.error("Missing parameters for 'payload.notificationInfo.ObjectId'.") + raise Exception( + "Missing parameters for 'payload.notificationInfo.ObjectId'." + ) + + region = finding_info.get("Region", None) + if region is None: + logging.warning("no region specified - defaulting to us-east-1") + region = "us-east-1" + + logging.info("parsed params") + logging.info(f" instance_id: {instance_id}") + logging.info(f" region: {region}") + + return {"instance_id": instance_id}, region + + def remediate(self, client, instance_id): + """Block public access to port 20 of all security groups attached to an EC2 instance + by removing all the rules with port 20 in the port range + :param client: Instance of the AWS boto3 client. + :param instance_id: The ID of the EC2 instance. + :type instance_id: str + :returns: Integer signaling success or failure + :rtype: int + :raises: botocore.exceptions.ClientError + """ + try: + port = 20 + logging.info(" executing client.describe_instances") + logging.info(f" InstanceId: {instance_id}") + # Extract security group Id + security_groups = client.describe_instances(InstanceIds=[instance_id])[ + "Reservations" + ][0]["Instances"][0]["SecurityGroups"] + for sg_info in security_groups: + security_group_id = sg_info["GroupId"] + logging.info(" executing client.describe_security_group_rules") + logging.info(f" group-id: {security_group_id}") + # List all the security group rules + security_group_rules = client.describe_security_group_rules( + Filters=[{"Name": "group-id", "Values": [security_group_id]},], + MaxResults=1000, + ) + for rule in security_group_rules["SecurityGroupRules"]: + if ( + rule["IpProtocol"] == "tcp" + and rule["IsEgress"] is False + and rule["FromPort"] <= port + and rule["ToPort"] >= port + and ( + ("CidrIpv4" in rule and rule["CidrIpv4"] == "0.0.0.0/0") + or ("CidrIpv6" in rule and rule["CidrIpv6"] == "::/0") + ) + ): + # Removes Ingress security group rule containing port 20 in the range with + # protocol 'tcp', source '0.0.0.0/0' or '::/0' + logging.info( + " executing client.revoke_security_group_ingress" + ) + logging.info(f" GroupId: {security_group_id}") + logging.info( + f" SecurityGroupRuleIds: {rule['SecurityGroupRuleId']}" + ) + client.revoke_security_group_ingress( + GroupId=security_group_id, + SecurityGroupRuleIds=[rule["SecurityGroupRuleId"]], + ) + logging.info("successfully executed remediation") + except Exception as e: + logging.error(f"{str(e)}") + return 0 + + def run(self, args): + """Run the remediation job. + + :param args: List of arguments provided to the job. + :type args: list. + :returns: int + """ + params, region = self.parse(args[1]) + client = boto3.client("ec2", region_name=region) + logging.info("acquired ec2 client and parsed params - starting remediation") + rc = self.remediate(client=client, **params) + return rc + + +if __name__ == "__main__": + logging.info(f"{sys.argv[0]} called - running now") + obj = EC2ClosePort20() + obj.run(sys.argv) diff --git a/remediation_worker/jobs/ec2_close_port_20/minimum_policy.json b/remediation_worker/jobs/ec2_close_port_20/minimum_policy.json new file mode 100644 index 0000000..e91af08 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_20/minimum_policy.json @@ -0,0 +1,15 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "EC2ClosePort22", + "Effect": "Allow", + "Action": [ + "ec2:DescribeInstances", + "ec2:RevokeSecurityGroupIngress", + "ec2:DescribeSecurityGroupRules" + ], + "Resource": "*" + } + ] +} diff --git a/remediation_worker/jobs/ec2_close_port_20/requirements-dev.txt b/remediation_worker/jobs/ec2_close_port_20/requirements-dev.txt new file mode 100644 index 0000000..9412e93 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_20/requirements-dev.txt @@ -0,0 +1,6 @@ +-r requirements.txt +-c constraints.txt + +pytest==6.0.1 \ + --hash=sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4 \ + --hash=sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad diff --git a/remediation_worker/jobs/ec2_close_port_20/requirements.txt b/remediation_worker/jobs/ec2_close_port_20/requirements.txt new file mode 100644 index 0000000..93f7845 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_20/requirements.txt @@ -0,0 +1,6 @@ +boto3==1.18.4 \ + --hash=sha256:649ed1ca205f5ee0b0328d54580780aebc1a7a05681a24f6ee05253007ca48d8 \ + --hash=sha256:7079b40bd6621c54a0385a8fc11240cff4318a4d487292653e393e18254f5d94 +botocore==1.21.5 \ + --hash=sha256:0070c5e02b581db40ff5fd1b5e02db90ed88e7e861901894bd78fd998656da68 \ + --hash=sha256:bed34fe7a007180f4208b65515bab1755cdd9fcf2c6720f74ae7ecd2e707f4b7 diff --git a/remediation_worker/jobs/ec2_close_port_21/README.md b/remediation_worker/jobs/ec2_close_port_21/README.md new file mode 100644 index 0000000..1f577ab --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_21/README.md @@ -0,0 +1,54 @@ +# Close Port 21 for all Security Groups associated with an EC2 Instance + +This job blocks public access to port 21 for both IPv4 and IPv6 for all security groups associated with an EC2 instance by removing all the ingress security group rules containing port 21 in the port range and source as "0.0.0.0/0" or "::/0". + +### Applicable Rule + +##### Rule ID: +5c8c263d7a550e1fb6560c3a + +##### Rule Name: +EC2 instance should restrict public access to FTP control port (21) + + +## Getting Started + +### Prerequisites + +The provided AWS credential must have access to `ec2:DescribeInstances`, `ec2:RevokeSecurityGroupIngress`, `ec2:DescribeSecurityGroupRules`. + +You may find the latest example policy file [here](minimum_policy.json) + +### Running the script + +You may run this script using following commands: +```shell script + pip install -r ../../requirements.txt + python3 ec2_close_port_21.py +``` + +## Running the tests +You may run test using following command under vss-remediation-worker-job-code-python directory: +```shell script + python3 -m pytest +``` + +## Contributing +The Secure State team welcomes welcomes contributions from the community. If you wish to contribute code and you have not signed our contributor license agreement (CLA), our bot will update the issue when you open a Pull Request. For any questions about the CLA process, please refer to our [FAQ](https://cla.vmware.com/faq). +All contributions to this repository must be signed as described on that page. Your signature certifies that you wrote the patch or have the right to pass it on as an open-source patch. + +For more detailed information, refer to [CONTRIBUTING.md](../../../CONTRIBUTING.md). + +## Versioning + +We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/vmware-samples/secure-state-remediation-jobs/tags). + +## Authors + +* **VMware Secure State** - *Initial work* + +See also the list of [contributors](https://github.com/vmware-samples/secure-state-remediation-jobs/contributors) who participated in this project. + +## License + +This project is licensed under the Apache License - see the [LICENSE](https://github.com/vmware-samples/secure-state-remediation-jobs/blob/master/LICENSE.txt) file for details diff --git a/remediation_worker/jobs/ec2_close_port_21/__init__.py b/remediation_worker/jobs/ec2_close_port_21/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/remediation_worker/jobs/ec2_close_port_21/constraints.txt b/remediation_worker/jobs/ec2_close_port_21/constraints.txt new file mode 100644 index 0000000..0d9b3c2 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_21/constraints.txt @@ -0,0 +1,43 @@ +docutils==0.15.2 \ + --hash=sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0 \ + --hash=sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827 \ + --hash=sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99 +jmespath==0.10.0 \ + --hash=sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9 \ + --hash=sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f +python-dateutil==2.8.1 \ + --hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \ + --hash=sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a +s3transfer==0.5.0 \ + --hash=sha256:50ed823e1dc5868ad40c8dc92072f757aa0e653a192845c94a3b676f4a62da4c \ + --hash=sha256:9c1dc369814391a6bda20ebbf4b70a0f34630592c9aa520856bf384916af2803 +urllib3==1.25.9 \ + --hash=sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527 \ + --hash=sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115 +six==1.15.0 \ + --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \ + --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced +packaging==20.4 \ + --hash=sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8 \ + --hash=sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181 +attrs==19.3.0 \ + --hash=sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c \ + --hash=sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72 +more-itertools==8.4.0 \ + --hash=sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5 \ + --hash=sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2 +pluggy==0.13.1 \ + --hash=sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0 \ + --hash=sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d +py==1.9.0 \ + --hash=sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2 \ + --hash=sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342 +toml==0.10.1 \ + --hash=sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f \ + --hash=sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88 +iniconfig==1.0.1 \ + --hash=sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437 \ + --hash=sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69 +pyparsing==2.4.7 \ + --hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1 \ + --hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b diff --git a/remediation_worker/jobs/ec2_close_port_21/ec2_close_port_21.py b/remediation_worker/jobs/ec2_close_port_21/ec2_close_port_21.py new file mode 100644 index 0000000..53c87f4 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_21/ec2_close_port_21.py @@ -0,0 +1,131 @@ +# Copyright (c) 2020 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations +import json +import logging +import sys + +import boto3 + +logging.basicConfig(level=logging.INFO) + + +class EC2ClosePort21(object): + def parse(self, payload): + """Parse payload received from Remediation Service. + + :param payload: JSON string containing parameters received from the remediation service. + :type payload: str. + :returns: Dictionary of parsed parameters + :rtype: dict + :raises: Exception, JSONDecodeError + """ + remediation_entry = json.loads(payload) + notification_info = remediation_entry.get("notificationInfo", None) + + finding_info = notification_info.get("FindingInfo", None) + instance_id = finding_info.get("ObjectId", None) + + if instance_id is None: + logging.error("Missing parameters for 'payload.notificationInfo.ObjectId'.") + raise Exception( + "Missing parameters for 'payload.notificationInfo.ObjectId'." + ) + + region = finding_info.get("Region", None) + if region is None: + logging.warning("no region specified - defaulting to us-east-1") + region = "us-east-1" + + logging.info("parsed params") + logging.info(f" instance_id: {instance_id}") + logging.info(f" region: {region}") + + return {"instance_id": instance_id}, region + + def remediate(self, client, instance_id): + """Block public access to port 21 of all security groups attached to an EC2 instance + by removing all the rules with port 21 in the port range + :param client: Instance of the AWS boto3 client. + :param instance_id: The ID of the EC2 instance. + :type instance_id: str + :returns: Integer signaling success or failure + :rtype: int + :raises: botocore.exceptions.ClientError + """ + try: + port = 21 + logging.info(" executing client.describe_instances") + logging.info(f" InstanceId: {instance_id}") + # Extract security group Id + security_groups = client.describe_instances(InstanceIds=[instance_id])[ + "Reservations" + ][0]["Instances"][0]["SecurityGroups"] + for sg_info in security_groups: + security_group_id = sg_info["GroupId"] + logging.info(" executing client.describe_security_group_rules") + logging.info(f" group-id: {security_group_id}") + # List all the security group rules + security_group_rules = client.describe_security_group_rules( + Filters=[{"Name": "group-id", "Values": [security_group_id]},], + MaxResults=1000, + ) + for rule in security_group_rules["SecurityGroupRules"]: + if ( + rule["IpProtocol"] == "tcp" + and rule["IsEgress"] is False + and rule["FromPort"] <= port + and rule["ToPort"] >= port + and ( + ("CidrIpv4" in rule and rule["CidrIpv4"] == "0.0.0.0/0") + or ("CidrIpv6" in rule and rule["CidrIpv6"] == "::/0") + ) + ): + # Removes Ingress security group rule containing port 21 in the range with + # protocol 'tcp', source '0.0.0.0/0' or '::/0' + logging.info( + " executing client.revoke_security_group_ingress" + ) + logging.info(f" GroupId: {security_group_id}") + logging.info( + f" SecurityGroupRuleIds: {rule['SecurityGroupRuleId']}" + ) + client.revoke_security_group_ingress( + GroupId=security_group_id, + SecurityGroupRuleIds=[rule["SecurityGroupRuleId"]], + ) + logging.info("successfully executed remediation") + except Exception as e: + logging.error(f"{str(e)}") + return 0 + + def run(self, args): + """Run the remediation job. + + :param args: List of arguments provided to the job. + :type args: list. + :returns: int + """ + params, region = self.parse(args[1]) + client = boto3.client("ec2", region_name=region) + logging.info("acquired ec2 client and parsed params - starting remediation") + rc = self.remediate(client=client, **params) + return rc + + +if __name__ == "__main__": + logging.info(f"{sys.argv[0]} called - running now") + obj = EC2ClosePort21() + obj.run(sys.argv) diff --git a/remediation_worker/jobs/ec2_close_port_21/minimum_policy.json b/remediation_worker/jobs/ec2_close_port_21/minimum_policy.json new file mode 100644 index 0000000..e91af08 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_21/minimum_policy.json @@ -0,0 +1,15 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "EC2ClosePort22", + "Effect": "Allow", + "Action": [ + "ec2:DescribeInstances", + "ec2:RevokeSecurityGroupIngress", + "ec2:DescribeSecurityGroupRules" + ], + "Resource": "*" + } + ] +} diff --git a/remediation_worker/jobs/ec2_close_port_21/requirements-dev.txt b/remediation_worker/jobs/ec2_close_port_21/requirements-dev.txt new file mode 100644 index 0000000..9412e93 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_21/requirements-dev.txt @@ -0,0 +1,6 @@ +-r requirements.txt +-c constraints.txt + +pytest==6.0.1 \ + --hash=sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4 \ + --hash=sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad diff --git a/remediation_worker/jobs/ec2_close_port_21/requirements.txt b/remediation_worker/jobs/ec2_close_port_21/requirements.txt new file mode 100644 index 0000000..93f7845 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_21/requirements.txt @@ -0,0 +1,6 @@ +boto3==1.18.4 \ + --hash=sha256:649ed1ca205f5ee0b0328d54580780aebc1a7a05681a24f6ee05253007ca48d8 \ + --hash=sha256:7079b40bd6621c54a0385a8fc11240cff4318a4d487292653e393e18254f5d94 +botocore==1.21.5 \ + --hash=sha256:0070c5e02b581db40ff5fd1b5e02db90ed88e7e861901894bd78fd998656da68 \ + --hash=sha256:bed34fe7a007180f4208b65515bab1755cdd9fcf2c6720f74ae7ecd2e707f4b7 diff --git a/remediation_worker/jobs/ec2_close_port_22/README.md b/remediation_worker/jobs/ec2_close_port_22/README.md index 54c0278..a102713 100644 --- a/remediation_worker/jobs/ec2_close_port_22/README.md +++ b/remediation_worker/jobs/ec2_close_port_22/README.md @@ -1,6 +1,6 @@ # Close Port 22 for all Security Groups associated with an EC2 Instance -This job blocks public access to port 22 for both IPv4 and IPv6 for all security groups associated with an EC2 instance. +This job blocks public access to port 22 for both IPv4 and IPv6 for all security groups associated with an EC2 instance by removing all the ingress security group rules containing port 22 in the port range and source as "0.0.0.0/0" or "::/0". ### Applicable Rule @@ -14,7 +14,7 @@ An EC2 instance's SSH port (22) is accessible from the public Internet for any s ### Prerequisites -The provided AWS credential must have access to `ec2:DescribeInstances` and `ec2:RevokeSecurityGroupIngress`. +The provided AWS credential must have access to `ec2:DescribeInstances`, `ec2:RevokeSecurityGroupIngress`, `ec2:DescribeSecurityGroupRules`. You may find the latest example policy file [here](minimum_policy.json) diff --git a/remediation_worker/jobs/ec2_close_port_22/constraints.txt b/remediation_worker/jobs/ec2_close_port_22/constraints.txt index 6b211d2..0d9b3c2 100644 --- a/remediation_worker/jobs/ec2_close_port_22/constraints.txt +++ b/remediation_worker/jobs/ec2_close_port_22/constraints.txt @@ -8,9 +8,9 @@ jmespath==0.10.0 \ python-dateutil==2.8.1 \ --hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \ --hash=sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a -s3transfer==0.3.3 \ - --hash=sha256:2482b4259524933a022d59da830f51bd746db62f047d6eb213f2f8855dcb8a13 \ - --hash=sha256:921a37e2aefc64145e7b73d50c71bb4f26f46e4c9f414dc648c6245ff92cf7db +s3transfer==0.5.0 \ + --hash=sha256:50ed823e1dc5868ad40c8dc92072f757aa0e653a192845c94a3b676f4a62da4c \ + --hash=sha256:9c1dc369814391a6bda20ebbf4b70a0f34630592c9aa520856bf384916af2803 urllib3==1.25.9 \ --hash=sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527 \ --hash=sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115 diff --git a/remediation_worker/jobs/ec2_close_port_22/ec2_close_port_22.py b/remediation_worker/jobs/ec2_close_port_22/ec2_close_port_22.py index 608f7a8..51ee27a 100644 --- a/remediation_worker/jobs/ec2_close_port_22/ec2_close_port_22.py +++ b/remediation_worker/jobs/ec2_close_port_22/ec2_close_port_22.py @@ -13,7 +13,6 @@ # limitations under the License. from __future__ import annotations -from botocore.exceptions import ClientError import json import logging import sys @@ -57,8 +56,8 @@ def parse(self, payload): return {"instance_id": instance_id}, region def remediate(self, client, instance_id): - """Block public access to port 22 of all security groups attached to an EC2 instance. - + """Block public access to port 22 of all security groups attached to an EC2 instance + by removing all the rules with port 22 in the port range :param client: Instance of the AWS boto3 client. :param instance_id: The ID of the EC2 instance. :type instance_id: str @@ -66,62 +65,50 @@ def remediate(self, client, instance_id): :rtype: int :raises: botocore.exceptions.ClientError """ - - port = 22 - security_groups = client.describe_instances(InstanceIds=[instance_id])[ - "Reservations" - ][0]["Instances"][0]["SecurityGroups"] - for sg_info in security_groups: - security_group_id = sg_info["GroupId"] - - # Revoke ipv4 permission - logging.info("revoking ivp4 permissions") - try: - logging.info(" executing client.revoke_security_group_ingress") - logging.info(' CidrIp="0.0.0.0/0"') - logging.info(f" FromPort={port}") - logging.info(f" GroupId={security_group_id}") - logging.info(' IpProtocol="tcp"') - logging.info(f" ToPort={port}") - client.revoke_security_group_ingress( - CidrIp="0.0.0.0/0", - FromPort=port, - GroupId=security_group_id, - IpProtocol="tcp", - ToPort=port, - ) - except ClientError as e: - if "InvalidPermission.NotFound" not in str(e): - logging.error(f"{str(e)}") - raise - - # Revoke ipv6 permission - logging.info("revoking ivp6 permissions") - try: - logging.info(" executing client.revoke_security_group_ingress") - logging.info(f" FromPort={port}") - logging.info(f" GroupId={security_group_id}") - logging.info(' IpProtocol="tcp"') - logging.info(' "Ipv6Ranges": [{"CidrIpv6": "::/0"}]') - logging.info(f" ToPort={port}") - client.revoke_security_group_ingress( - GroupId=security_group_id, - IpPermissions=[ - { - "FromPort": port, - "IpProtocol": "tcp", - "Ipv6Ranges": [{"CidrIpv6": "::/0"}], - "ToPort": port, - }, - ], + try: + port = 22 + logging.info(" executing client.describe_instances") + logging.info(f" InstanceId: {instance_id}") + # Extract security group Id + security_groups = client.describe_instances(InstanceIds=[instance_id])[ + "Reservations" + ][0]["Instances"][0]["SecurityGroups"] + for sg_info in security_groups: + security_group_id = sg_info["GroupId"] + logging.info(" executing client.describe_security_group_rules") + logging.info(f" group-id: {security_group_id}") + # List all the security group rules + security_group_rules = client.describe_security_group_rules( + Filters=[{"Name": "group-id", "Values": [security_group_id]},], + MaxResults=1000, ) - except ClientError as e: - if "InvalidPermission.NotFound" not in str(e): - logging.error(f"{str(e)}") - raise - - logging.info("successfully executed remediation") - + for rule in security_group_rules["SecurityGroupRules"]: + if ( + rule["IpProtocol"] == "tcp" + and rule["IsEgress"] is False + and rule["FromPort"] <= port + and rule["ToPort"] >= port + and ( + ("CidrIpv4" in rule and rule["CidrIpv4"] == "0.0.0.0/0") + or ("CidrIpv6" in rule and rule["CidrIpv6"] == "::/0") + ) + ): + # Removes Ingress security group rule containing port 22 in the range with + # protocol 'tcp', source '0.0.0.0/0' or '::/0' + logging.info( + " executing client.revoke_security_group_ingress" + ) + logging.info(f" GroupId: {security_group_id}") + logging.info( + f" SecurityGroupRuleIds: {rule['SecurityGroupRuleId']}" + ) + client.revoke_security_group_ingress( + GroupId=security_group_id, + SecurityGroupRuleIds=[rule["SecurityGroupRuleId"]], + ) + logging.info("successfully executed remediation") + except Exception as e: + logging.error(f"{str(e)}") return 0 def run(self, args): diff --git a/remediation_worker/jobs/ec2_close_port_22/minimum_policy.json b/remediation_worker/jobs/ec2_close_port_22/minimum_policy.json index 3140049..e91af08 100644 --- a/remediation_worker/jobs/ec2_close_port_22/minimum_policy.json +++ b/remediation_worker/jobs/ec2_close_port_22/minimum_policy.json @@ -6,7 +6,8 @@ "Effect": "Allow", "Action": [ "ec2:DescribeInstances", - "ec2:RevokeSecurityGroupIngress" + "ec2:RevokeSecurityGroupIngress", + "ec2:DescribeSecurityGroupRules" ], "Resource": "*" } diff --git a/remediation_worker/jobs/ec2_close_port_22/requirements.txt b/remediation_worker/jobs/ec2_close_port_22/requirements.txt index b239388..93f7845 100644 --- a/remediation_worker/jobs/ec2_close_port_22/requirements.txt +++ b/remediation_worker/jobs/ec2_close_port_22/requirements.txt @@ -1,6 +1,6 @@ -boto3==1.14.9 \ - --hash=sha256:185f7b36c16f76e501d8dfc5cd209113426e078e4968dd13cc355c916bc99597 \ - --hash=sha256:51243ba0e976343ca0b98bb4a15fc3d588526220f6ba45bfed7ea45472b1e033 -botocore==1.17.9 \ - --hash=sha256:7dd59bc766d567ca83bc6113aa139d92ba447738ccdfcd40788848553d329a52 \ - --hash=sha256:cd4bb2d96ff2ec6bf4fbcdb2f241d0fb6ba1e7955b4721cf1d81f13db02768b6 +boto3==1.18.4 \ + --hash=sha256:649ed1ca205f5ee0b0328d54580780aebc1a7a05681a24f6ee05253007ca48d8 \ + --hash=sha256:7079b40bd6621c54a0385a8fc11240cff4318a4d487292653e393e18254f5d94 +botocore==1.21.5 \ + --hash=sha256:0070c5e02b581db40ff5fd1b5e02db90ed88e7e861901894bd78fd998656da68 \ + --hash=sha256:bed34fe7a007180f4208b65515bab1755cdd9fcf2c6720f74ae7ecd2e707f4b7 diff --git a/remediation_worker/jobs/ec2_close_port_23/README.md b/remediation_worker/jobs/ec2_close_port_23/README.md new file mode 100644 index 0000000..5762a01 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_23/README.md @@ -0,0 +1,53 @@ +# Close Port 23 for all Security Groups associated with an EC2 Instance + +This job blocks public access to port 23 for both IPv4 and IPv6 for all security groups associated with an EC2 instance by removing all the ingress security group rules containing port 23 in the port range and source as "0.0.0.0/0" or "::/0". + +### Applicable Rule + +##### Rule ID: +5c8c263e7a550e1fb6560c3b + +##### Rule Name: +EC2 instance should restrict public access to Telnet port (23) + +## Getting Started + +### Prerequisites + +The provided AWS credential must have access to `ec2:DescribeInstances`, `ec2:RevokeSecurityGroupIngress`, `ec2:DescribeSecurityGroupRules`. + +You may find the latest example policy file [here](minimum_policy.json) + +### Running the script + +You may run this script using following commands: +```shell script + pip install -r ../../requirements.txt + python3 ec2_close_port_23.py +``` + +## Running the tests +You may run test using following command under vss-remediation-worker-job-code-python directory: +```shell script + python3 -m pytest +``` + +## Contributing +The Secure State team welcomes welcomes contributions from the community. If you wish to contribute code and you have not signed our contributor license agreement (CLA), our bot will update the issue when you open a Pull Request. For any questions about the CLA process, please refer to our [FAQ](https://cla.vmware.com/faq). +All contributions to this repository must be signed as described on that page. Your signature certifies that you wrote the patch or have the right to pass it on as an open-source patch. + +For more detailed information, refer to [CONTRIBUTING.md](../../../CONTRIBUTING.md). + +## Versioning + +We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/vmware-samples/secure-state-remediation-jobs/tags). + +## Authors + +* **VMware Secure State** - *Initial work* + +See also the list of [contributors](https://github.com/vmware-samples/secure-state-remediation-jobs/contributors) who participated in this project. + +## License + +This project is licensed under the Apache License - see the [LICENSE](https://github.com/vmware-samples/secure-state-remediation-jobs/blob/master/LICENSE.txt) file for details diff --git a/remediation_worker/jobs/ec2_close_port_23/__init__.py b/remediation_worker/jobs/ec2_close_port_23/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/remediation_worker/jobs/ec2_close_port_23/constraints.txt b/remediation_worker/jobs/ec2_close_port_23/constraints.txt new file mode 100644 index 0000000..0d9b3c2 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_23/constraints.txt @@ -0,0 +1,43 @@ +docutils==0.15.2 \ + --hash=sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0 \ + --hash=sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827 \ + --hash=sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99 +jmespath==0.10.0 \ + --hash=sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9 \ + --hash=sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f +python-dateutil==2.8.1 \ + --hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \ + --hash=sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a +s3transfer==0.5.0 \ + --hash=sha256:50ed823e1dc5868ad40c8dc92072f757aa0e653a192845c94a3b676f4a62da4c \ + --hash=sha256:9c1dc369814391a6bda20ebbf4b70a0f34630592c9aa520856bf384916af2803 +urllib3==1.25.9 \ + --hash=sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527 \ + --hash=sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115 +six==1.15.0 \ + --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \ + --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced +packaging==20.4 \ + --hash=sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8 \ + --hash=sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181 +attrs==19.3.0 \ + --hash=sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c \ + --hash=sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72 +more-itertools==8.4.0 \ + --hash=sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5 \ + --hash=sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2 +pluggy==0.13.1 \ + --hash=sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0 \ + --hash=sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d +py==1.9.0 \ + --hash=sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2 \ + --hash=sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342 +toml==0.10.1 \ + --hash=sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f \ + --hash=sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88 +iniconfig==1.0.1 \ + --hash=sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437 \ + --hash=sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69 +pyparsing==2.4.7 \ + --hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1 \ + --hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b diff --git a/remediation_worker/jobs/ec2_close_port_23/ec2_close_port_23.py b/remediation_worker/jobs/ec2_close_port_23/ec2_close_port_23.py new file mode 100644 index 0000000..69a28bc --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_23/ec2_close_port_23.py @@ -0,0 +1,131 @@ +# Copyright (c) 2020 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations +import json +import logging +import sys + +import boto3 + +logging.basicConfig(level=logging.INFO) + + +class EC2ClosePort23(object): + def parse(self, payload): + """Parse payload received from Remediation Service. + + :param payload: JSON string containing parameters received from the remediation service. + :type payload: str. + :returns: Dictionary of parsed parameters + :rtype: dict + :raises: Exception, JSONDecodeError + """ + remediation_entry = json.loads(payload) + notification_info = remediation_entry.get("notificationInfo", None) + + finding_info = notification_info.get("FindingInfo", None) + instance_id = finding_info.get("ObjectId", None) + + if instance_id is None: + logging.error("Missing parameters for 'payload.notificationInfo.ObjectId'.") + raise Exception( + "Missing parameters for 'payload.notificationInfo.ObjectId'." + ) + + region = finding_info.get("Region", None) + if region is None: + logging.warning("no region specified - defaulting to us-east-1") + region = "us-east-1" + + logging.info("parsed params") + logging.info(f" instance_id: {instance_id}") + logging.info(f" region: {region}") + + return {"instance_id": instance_id}, region + + def remediate(self, client, instance_id): + """Block public access to port 23 of all security groups attached to an EC2 instance + by removing all the rules with port 23 in the port range + :param client: Instance of the AWS boto3 client. + :param instance_id: The ID of the EC2 instance. + :type instance_id: str + :returns: Integer signaling success or failure + :rtype: int + :raises: botocore.exceptions.ClientError + """ + try: + port = 23 + logging.info(" executing client.describe_instances") + logging.info(f" InstanceId: {instance_id}") + # Extract security group Id + security_groups = client.describe_instances(InstanceIds=[instance_id])[ + "Reservations" + ][0]["Instances"][0]["SecurityGroups"] + for sg_info in security_groups: + security_group_id = sg_info["GroupId"] + logging.info(" executing client.describe_security_group_rules") + logging.info(f" group-id: {security_group_id}") + # List all the security group rules + security_group_rules = client.describe_security_group_rules( + Filters=[{"Name": "group-id", "Values": [security_group_id]},], + MaxResults=1000, + ) + for rule in security_group_rules["SecurityGroupRules"]: + if ( + rule["IpProtocol"] == "tcp" + and rule["IsEgress"] is False + and rule["FromPort"] <= port + and rule["ToPort"] >= port + and ( + ("CidrIpv4" in rule and rule["CidrIpv4"] == "0.0.0.0/0") + or ("CidrIpv6" in rule and rule["CidrIpv6"] == "::/0") + ) + ): + # Removes Ingress security group rule containing port 23 in the range with + # protocol 'tcp', source '0.0.0.0/0' or '::/0' + logging.info( + " executing client.revoke_security_group_ingress" + ) + logging.info(f" GroupId: {security_group_id}") + logging.info( + f" SecurityGroupRuleIds: {rule['SecurityGroupRuleId']}" + ) + client.revoke_security_group_ingress( + GroupId=security_group_id, + SecurityGroupRuleIds=[rule["SecurityGroupRuleId"]], + ) + logging.info("successfully executed remediation") + except Exception as e: + logging.error(f"{str(e)}") + return 0 + + def run(self, args): + """Run the remediation job. + + :param args: List of arguments provided to the job. + :type args: list. + :returns: int + """ + params, region = self.parse(args[1]) + client = boto3.client("ec2", region_name=region) + logging.info("acquired ec2 client and parsed params - starting remediation") + rc = self.remediate(client=client, **params) + return rc + + +if __name__ == "__main__": + logging.info(f"{sys.argv[0]} called - running now") + obj = EC2ClosePort23() + obj.run(sys.argv) diff --git a/remediation_worker/jobs/ec2_close_port_23/minimum_policy.json b/remediation_worker/jobs/ec2_close_port_23/minimum_policy.json new file mode 100644 index 0000000..e91af08 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_23/minimum_policy.json @@ -0,0 +1,15 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "EC2ClosePort22", + "Effect": "Allow", + "Action": [ + "ec2:DescribeInstances", + "ec2:RevokeSecurityGroupIngress", + "ec2:DescribeSecurityGroupRules" + ], + "Resource": "*" + } + ] +} diff --git a/remediation_worker/jobs/ec2_close_port_23/requirements-dev.txt b/remediation_worker/jobs/ec2_close_port_23/requirements-dev.txt new file mode 100644 index 0000000..9412e93 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_23/requirements-dev.txt @@ -0,0 +1,6 @@ +-r requirements.txt +-c constraints.txt + +pytest==6.0.1 \ + --hash=sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4 \ + --hash=sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad diff --git a/remediation_worker/jobs/ec2_close_port_23/requirements.txt b/remediation_worker/jobs/ec2_close_port_23/requirements.txt new file mode 100644 index 0000000..93f7845 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_23/requirements.txt @@ -0,0 +1,6 @@ +boto3==1.18.4 \ + --hash=sha256:649ed1ca205f5ee0b0328d54580780aebc1a7a05681a24f6ee05253007ca48d8 \ + --hash=sha256:7079b40bd6621c54a0385a8fc11240cff4318a4d487292653e393e18254f5d94 +botocore==1.21.5 \ + --hash=sha256:0070c5e02b581db40ff5fd1b5e02db90ed88e7e861901894bd78fd998656da68 \ + --hash=sha256:bed34fe7a007180f4208b65515bab1755cdd9fcf2c6720f74ae7ecd2e707f4b7 diff --git a/remediation_worker/jobs/ec2_close_port_27017/README.md b/remediation_worker/jobs/ec2_close_port_27017/README.md new file mode 100644 index 0000000..f413f3b --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_27017/README.md @@ -0,0 +1,54 @@ +# Close Port 27017 for all Security Groups associated with an EC2 Instance + +This job blocks public access to port 27017 for both IPv4 and IPv6 for all security groups associated with an EC2 instance by removing all the ingress security group rules containing port 27017 in the port range and source as "0.0.0.0/0" or "::/0". + +### Applicable Rule + +##### Rule ID: +5c8c26427a550e1fb6560c40 + +##### Rule Name: +EC2 instance should restrict public access to MongoDB server port (27017) + + +## Getting Started + +### Prerequisites + +The provided AWS credential must have access to `ec2:DescribeInstances`, `ec2:RevokeSecurityGroupIngress`, `ec2:DescribeSecurityGroupRules`. + +You may find the latest example policy file [here](minimum_policy.json) + +### Running the script + +You may run this script using following commands: +```shell script + pip install -r ../../requirements.txt + python3 ec2_close_port_27017.py +``` + +## Running the tests +You may run test using following command under vss-remediation-worker-job-code-python directory: +```shell script + python3 -m pytest +``` + +## Contributing +The Secure State team welcomes welcomes contributions from the community. If you wish to contribute code and you have not signed our contributor license agreement (CLA), our bot will update the issue when you open a Pull Request. For any questions about the CLA process, please refer to our [FAQ](https://cla.vmware.com/faq). +All contributions to this repository must be signed as described on that page. Your signature certifies that you wrote the patch or have the right to pass it on as an open-source patch. + +For more detailed information, refer to [CONTRIBUTING.md](../../../CONTRIBUTING.md). + +## Versioning + +We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/vmware-samples/secure-state-remediation-jobs/tags). + +## Authors + +* **VMware Secure State** - *Initial work* + +See also the list of [contributors](https://github.com/vmware-samples/secure-state-remediation-jobs/contributors) who participated in this project. + +## License + +This project is licensed under the Apache License - see the [LICENSE](https://github.com/vmware-samples/secure-state-remediation-jobs/blob/master/LICENSE.txt) file for details diff --git a/remediation_worker/jobs/ec2_close_port_27017/__init__.py b/remediation_worker/jobs/ec2_close_port_27017/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/remediation_worker/jobs/ec2_close_port_27017/constraints.txt b/remediation_worker/jobs/ec2_close_port_27017/constraints.txt new file mode 100644 index 0000000..0d9b3c2 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_27017/constraints.txt @@ -0,0 +1,43 @@ +docutils==0.15.2 \ + --hash=sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0 \ + --hash=sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827 \ + --hash=sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99 +jmespath==0.10.0 \ + --hash=sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9 \ + --hash=sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f +python-dateutil==2.8.1 \ + --hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \ + --hash=sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a +s3transfer==0.5.0 \ + --hash=sha256:50ed823e1dc5868ad40c8dc92072f757aa0e653a192845c94a3b676f4a62da4c \ + --hash=sha256:9c1dc369814391a6bda20ebbf4b70a0f34630592c9aa520856bf384916af2803 +urllib3==1.25.9 \ + --hash=sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527 \ + --hash=sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115 +six==1.15.0 \ + --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \ + --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced +packaging==20.4 \ + --hash=sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8 \ + --hash=sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181 +attrs==19.3.0 \ + --hash=sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c \ + --hash=sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72 +more-itertools==8.4.0 \ + --hash=sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5 \ + --hash=sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2 +pluggy==0.13.1 \ + --hash=sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0 \ + --hash=sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d +py==1.9.0 \ + --hash=sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2 \ + --hash=sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342 +toml==0.10.1 \ + --hash=sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f \ + --hash=sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88 +iniconfig==1.0.1 \ + --hash=sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437 \ + --hash=sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69 +pyparsing==2.4.7 \ + --hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1 \ + --hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b diff --git a/remediation_worker/jobs/ec2_close_port_27017/ec2_close_port_27017.py b/remediation_worker/jobs/ec2_close_port_27017/ec2_close_port_27017.py new file mode 100644 index 0000000..85f1bea --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_27017/ec2_close_port_27017.py @@ -0,0 +1,131 @@ +# Copyright (c) 2020 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations +import json +import logging +import sys + +import boto3 + +logging.basicConfig(level=logging.INFO) + + +class EC2ClosePort27017(object): + def parse(self, payload): + """Parse payload received from Remediation Service. + + :param payload: JSON string containing parameters received from the remediation service. + :type payload: str. + :returns: Dictionary of parsed parameters + :rtype: dict + :raises: Exception, JSONDecodeError + """ + remediation_entry = json.loads(payload) + notification_info = remediation_entry.get("notificationInfo", None) + + finding_info = notification_info.get("FindingInfo", None) + instance_id = finding_info.get("ObjectId", None) + + if instance_id is None: + logging.error("Missing parameters for 'payload.notificationInfo.ObjectId'.") + raise Exception( + "Missing parameters for 'payload.notificationInfo.ObjectId'." + ) + + region = finding_info.get("Region", None) + if region is None: + logging.warning("no region specified - defaulting to us-east-1") + region = "us-east-1" + + logging.info("parsed params") + logging.info(f" instance_id: {instance_id}") + logging.info(f" region: {region}") + + return {"instance_id": instance_id}, region + + def remediate(self, client, instance_id): + """Block public access to port 27017 of all security groups attached to an EC2 instance + by removing all the rules with port 27017 in the port range + :param client: Instance of the AWS boto3 client. + :param instance_id: The ID of the EC2 instance. + :type instance_id: str + :returns: Integer signaling success or failure + :rtype: int + :raises: botocore.exceptions.ClientError + """ + try: + port = 27017 + logging.info(" executing client.describe_instances") + logging.info(f" InstanceId: {instance_id}") + # Extract security group Id + security_groups = client.describe_instances(InstanceIds=[instance_id])[ + "Reservations" + ][0]["Instances"][0]["SecurityGroups"] + for sg_info in security_groups: + security_group_id = sg_info["GroupId"] + logging.info(" executing client.describe_security_group_rules") + logging.info(f" group-id: {security_group_id}") + # List all the security group rules + security_group_rules = client.describe_security_group_rules( + Filters=[{"Name": "group-id", "Values": [security_group_id]},], + MaxResults=1000, + ) + for rule in security_group_rules["SecurityGroupRules"]: + if ( + rule["IpProtocol"] == "tcp" + and rule["IsEgress"] is False + and rule["FromPort"] <= port + and rule["ToPort"] >= port + and ( + ("CidrIpv4" in rule and rule["CidrIpv4"] == "0.0.0.0/0") + or ("CidrIpv6" in rule and rule["CidrIpv6"] == "::/0") + ) + ): + # Removes Ingress security group rule containing port 27017 in the range with + # protocol 'tcp', source '0.0.0.0/0' or '::/0' + logging.info( + " executing client.revoke_security_group_ingress" + ) + logging.info(f" GroupId: {security_group_id}") + logging.info( + f" SecurityGroupRuleIds: {rule['SecurityGroupRuleId']}" + ) + client.revoke_security_group_ingress( + GroupId=security_group_id, + SecurityGroupRuleIds=[rule["SecurityGroupRuleId"]], + ) + logging.info("successfully executed remediation") + except Exception as e: + logging.error(f"{str(e)}") + return 0 + + def run(self, args): + """Run the remediation job. + + :param args: List of arguments provided to the job. + :type args: list. + :returns: int + """ + params, region = self.parse(args[1]) + client = boto3.client("ec2", region_name=region) + logging.info("acquired ec2 client and parsed params - starting remediation") + rc = self.remediate(client=client, **params) + return rc + + +if __name__ == "__main__": + logging.info(f"{sys.argv[0]} called - running now") + obj = EC2ClosePort27017() + obj.run(sys.argv) diff --git a/remediation_worker/jobs/ec2_close_port_27017/minimum_policy.json b/remediation_worker/jobs/ec2_close_port_27017/minimum_policy.json new file mode 100644 index 0000000..e91af08 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_27017/minimum_policy.json @@ -0,0 +1,15 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "EC2ClosePort22", + "Effect": "Allow", + "Action": [ + "ec2:DescribeInstances", + "ec2:RevokeSecurityGroupIngress", + "ec2:DescribeSecurityGroupRules" + ], + "Resource": "*" + } + ] +} diff --git a/remediation_worker/jobs/ec2_close_port_27017/requirements-dev.txt b/remediation_worker/jobs/ec2_close_port_27017/requirements-dev.txt new file mode 100644 index 0000000..9412e93 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_27017/requirements-dev.txt @@ -0,0 +1,6 @@ +-r requirements.txt +-c constraints.txt + +pytest==6.0.1 \ + --hash=sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4 \ + --hash=sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad diff --git a/remediation_worker/jobs/ec2_close_port_27017/requirements.txt b/remediation_worker/jobs/ec2_close_port_27017/requirements.txt new file mode 100644 index 0000000..93f7845 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_27017/requirements.txt @@ -0,0 +1,6 @@ +boto3==1.18.4 \ + --hash=sha256:649ed1ca205f5ee0b0328d54580780aebc1a7a05681a24f6ee05253007ca48d8 \ + --hash=sha256:7079b40bd6621c54a0385a8fc11240cff4318a4d487292653e393e18254f5d94 +botocore==1.21.5 \ + --hash=sha256:0070c5e02b581db40ff5fd1b5e02db90ed88e7e861901894bd78fd998656da68 \ + --hash=sha256:bed34fe7a007180f4208b65515bab1755cdd9fcf2c6720f74ae7ecd2e707f4b7 diff --git a/remediation_worker/jobs/ec2_close_port_3306/README.md b/remediation_worker/jobs/ec2_close_port_3306/README.md new file mode 100644 index 0000000..ef6615f --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_3306/README.md @@ -0,0 +1,54 @@ +# Close Port 3306 for all Security Groups associated with an EC2 Instance + +This job blocks public access to port 3306 for both IPv4 and IPv6 for all security groups associated with an EC2 instance by removing all the ingress security group rules containing port 3306 in the port range and source as "0.0.0.0/0" or "::/0". + +### Applicable Rule + +##### Rule ID: +5c8c26427a550e1fb6560c41 + +##### Rule Name: +EC2 instance should restrict public access to MySQL server port (3306) + + +## Getting Started + +### Prerequisites + +The provided AWS credential must have access to `ec2:DescribeInstances`, `ec2:RevokeSecurityGroupIngress`, `ec2:DescribeSecurityGroupRules`. + +You may find the latest example policy file [here](minimum_policy.json) + +### Running the script + +You may run this script using following commands: +```shell script + pip install -r ../../requirements.txt + python3 ec2_close_port_3306.py +``` + +## Running the tests +You may run test using following command under vss-remediation-worker-job-code-python directory: +```shell script + python3 -m pytest +``` + +## Contributing +The Secure State team welcomes welcomes contributions from the community. If you wish to contribute code and you have not signed our contributor license agreement (CLA), our bot will update the issue when you open a Pull Request. For any questions about the CLA process, please refer to our [FAQ](https://cla.vmware.com/faq). +All contributions to this repository must be signed as described on that page. Your signature certifies that you wrote the patch or have the right to pass it on as an open-source patch. + +For more detailed information, refer to [CONTRIBUTING.md](../../../CONTRIBUTING.md). + +## Versioning + +We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/vmware-samples/secure-state-remediation-jobs/tags). + +## Authors + +* **VMware Secure State** - *Initial work* + +See also the list of [contributors](https://github.com/vmware-samples/secure-state-remediation-jobs/contributors) who participated in this project. + +## License + +This project is licensed under the Apache License - see the [LICENSE](https://github.com/vmware-samples/secure-state-remediation-jobs/blob/master/LICENSE.txt) file for details diff --git a/remediation_worker/jobs/ec2_close_port_3306/__init__.py b/remediation_worker/jobs/ec2_close_port_3306/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/remediation_worker/jobs/ec2_close_port_3306/constraints.txt b/remediation_worker/jobs/ec2_close_port_3306/constraints.txt new file mode 100644 index 0000000..0d9b3c2 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_3306/constraints.txt @@ -0,0 +1,43 @@ +docutils==0.15.2 \ + --hash=sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0 \ + --hash=sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827 \ + --hash=sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99 +jmespath==0.10.0 \ + --hash=sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9 \ + --hash=sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f +python-dateutil==2.8.1 \ + --hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \ + --hash=sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a +s3transfer==0.5.0 \ + --hash=sha256:50ed823e1dc5868ad40c8dc92072f757aa0e653a192845c94a3b676f4a62da4c \ + --hash=sha256:9c1dc369814391a6bda20ebbf4b70a0f34630592c9aa520856bf384916af2803 +urllib3==1.25.9 \ + --hash=sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527 \ + --hash=sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115 +six==1.15.0 \ + --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \ + --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced +packaging==20.4 \ + --hash=sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8 \ + --hash=sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181 +attrs==19.3.0 \ + --hash=sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c \ + --hash=sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72 +more-itertools==8.4.0 \ + --hash=sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5 \ + --hash=sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2 +pluggy==0.13.1 \ + --hash=sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0 \ + --hash=sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d +py==1.9.0 \ + --hash=sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2 \ + --hash=sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342 +toml==0.10.1 \ + --hash=sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f \ + --hash=sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88 +iniconfig==1.0.1 \ + --hash=sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437 \ + --hash=sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69 +pyparsing==2.4.7 \ + --hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1 \ + --hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b diff --git a/remediation_worker/jobs/ec2_close_port_3306/ec2_close_port_3306.py b/remediation_worker/jobs/ec2_close_port_3306/ec2_close_port_3306.py new file mode 100644 index 0000000..a7f72bf --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_3306/ec2_close_port_3306.py @@ -0,0 +1,131 @@ +# Copyright (c) 2020 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations +import json +import logging +import sys + +import boto3 + +logging.basicConfig(level=logging.INFO) + + +class EC2ClosePort3306(object): + def parse(self, payload): + """Parse payload received from Remediation Service. + + :param payload: JSON string containing parameters received from the remediation service. + :type payload: str. + :returns: Dictionary of parsed parameters + :rtype: dict + :raises: Exception, JSONDecodeError + """ + remediation_entry = json.loads(payload) + notification_info = remediation_entry.get("notificationInfo", None) + + finding_info = notification_info.get("FindingInfo", None) + instance_id = finding_info.get("ObjectId", None) + + if instance_id is None: + logging.error("Missing parameters for 'payload.notificationInfo.ObjectId'.") + raise Exception( + "Missing parameters for 'payload.notificationInfo.ObjectId'." + ) + + region = finding_info.get("Region", None) + if region is None: + logging.warning("no region specified - defaulting to us-east-1") + region = "us-east-1" + + logging.info("parsed params") + logging.info(f" instance_id: {instance_id}") + logging.info(f" region: {region}") + + return {"instance_id": instance_id}, region + + def remediate(self, client, instance_id): + """Block public access to port 3306 of all security groups attached to an EC2 instance + by removing all the rules with port 3306 in the port range + :param client: Instance of the AWS boto3 client. + :param instance_id: The ID of the EC2 instance. + :type instance_id: str + :returns: Integer signaling success or failure + :rtype: int + :raises: botocore.exceptions.ClientError + """ + try: + port = 3306 + logging.info(" executing client.describe_instances") + logging.info(f" InstanceId: {instance_id}") + # Extract security group Id + security_groups = client.describe_instances(InstanceIds=[instance_id])[ + "Reservations" + ][0]["Instances"][0]["SecurityGroups"] + for sg_info in security_groups: + security_group_id = sg_info["GroupId"] + logging.info(" executing client.describe_security_group_rules") + logging.info(f" group-id: {security_group_id}") + # List all the security group rules + security_group_rules = client.describe_security_group_rules( + Filters=[{"Name": "group-id", "Values": [security_group_id]},], + MaxResults=1000, + ) + for rule in security_group_rules["SecurityGroupRules"]: + if ( + rule["IpProtocol"] == "tcp" + and rule["IsEgress"] is False + and rule["FromPort"] <= port + and rule["ToPort"] >= port + and ( + ("CidrIpv4" in rule and rule["CidrIpv4"] == "0.0.0.0/0") + or ("CidrIpv6" in rule and rule["CidrIpv6"] == "::/0") + ) + ): + # Removes Ingress security group rule containing port 3306 in the range with + # protocol 'tcp', source '0.0.0.0/0' or '::/0' + logging.info( + " executing client.revoke_security_group_ingress" + ) + logging.info(f" GroupId: {security_group_id}") + logging.info( + f" SecurityGroupRuleIds: {rule['SecurityGroupRuleId']}" + ) + client.revoke_security_group_ingress( + GroupId=security_group_id, + SecurityGroupRuleIds=[rule["SecurityGroupRuleId"]], + ) + logging.info("successfully executed remediation") + except Exception as e: + logging.error(f"{str(e)}") + return 0 + + def run(self, args): + """Run the remediation job. + + :param args: List of arguments provided to the job. + :type args: list. + :returns: int + """ + params, region = self.parse(args[1]) + client = boto3.client("ec2", region_name=region) + logging.info("acquired ec2 client and parsed params - starting remediation") + rc = self.remediate(client=client, **params) + return rc + + +if __name__ == "__main__": + logging.info(f"{sys.argv[0]} called - running now") + obj = EC2ClosePort3306() + obj.run(sys.argv) diff --git a/remediation_worker/jobs/ec2_close_port_3306/minimum_policy.json b/remediation_worker/jobs/ec2_close_port_3306/minimum_policy.json new file mode 100644 index 0000000..e91af08 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_3306/minimum_policy.json @@ -0,0 +1,15 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "EC2ClosePort22", + "Effect": "Allow", + "Action": [ + "ec2:DescribeInstances", + "ec2:RevokeSecurityGroupIngress", + "ec2:DescribeSecurityGroupRules" + ], + "Resource": "*" + } + ] +} diff --git a/remediation_worker/jobs/ec2_close_port_3306/requirements-dev.txt b/remediation_worker/jobs/ec2_close_port_3306/requirements-dev.txt new file mode 100644 index 0000000..9412e93 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_3306/requirements-dev.txt @@ -0,0 +1,6 @@ +-r requirements.txt +-c constraints.txt + +pytest==6.0.1 \ + --hash=sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4 \ + --hash=sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad diff --git a/remediation_worker/jobs/ec2_close_port_3306/requirements.txt b/remediation_worker/jobs/ec2_close_port_3306/requirements.txt new file mode 100644 index 0000000..93f7845 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_3306/requirements.txt @@ -0,0 +1,6 @@ +boto3==1.18.4 \ + --hash=sha256:649ed1ca205f5ee0b0328d54580780aebc1a7a05681a24f6ee05253007ca48d8 \ + --hash=sha256:7079b40bd6621c54a0385a8fc11240cff4318a4d487292653e393e18254f5d94 +botocore==1.21.5 \ + --hash=sha256:0070c5e02b581db40ff5fd1b5e02db90ed88e7e861901894bd78fd998656da68 \ + --hash=sha256:bed34fe7a007180f4208b65515bab1755cdd9fcf2c6720f74ae7ecd2e707f4b7 diff --git a/remediation_worker/jobs/ec2_close_port_3389/README.md b/remediation_worker/jobs/ec2_close_port_3389/README.md index 6245d6f..0f07d7a 100644 --- a/remediation_worker/jobs/ec2_close_port_3389/README.md +++ b/remediation_worker/jobs/ec2_close_port_3389/README.md @@ -1,6 +1,6 @@ # Close Port 3389 for all Security Groups associated with an EC2 Instance -This job blocks public access to port 3389 for both IPv4 and IPv6 for all security groups associated with an EC2 instance. +This job blocks public access to port 3389 for both IPv4 and IPv6 for all security groups associated with an EC2 instance by removing all the ingress security group rules containing port 3389 in the port range and source as "0.0.0.0/0" or "::/0". ### Applicable Rule @@ -14,7 +14,7 @@ An EC2 instance's Remote Desktop port (3389) is accessible from the public Inter ### Prerequisites -The provided AWS credential must have access to `ec2:DescribeInstances` and `ec2:RevokeSecurityGroupIngress`. +The provided AWS credential must have access to `ec2:DescribeInstances`, `ec2:RevokeSecurityGroupIngress`, `ec2:DescribeSecurityGroupRules`. You may find the latest example policy file [here](minimum_policy.json) diff --git a/remediation_worker/jobs/ec2_close_port_3389/constraints.txt b/remediation_worker/jobs/ec2_close_port_3389/constraints.txt index 6b211d2..1c0a19d 100644 --- a/remediation_worker/jobs/ec2_close_port_3389/constraints.txt +++ b/remediation_worker/jobs/ec2_close_port_3389/constraints.txt @@ -2,15 +2,15 @@ docutils==0.15.2 \ --hash=sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0 \ --hash=sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827 \ --hash=sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99 -jmespath==0.10.0 \ - --hash=sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9 \ - --hash=sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f +jmespath==0.7.1 \ + --hash=sha256:cb99b58dcf853f791bec28aa016c121663b79ee6125c892297d773d7fd4dcecf \ + --hash=sha256:cd5a12ee3dfa470283a020a35e69e83b0700d44fe413014fd35ad5584c5f5fd1 python-dateutil==2.8.1 \ --hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \ --hash=sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a -s3transfer==0.3.3 \ - --hash=sha256:2482b4259524933a022d59da830f51bd746db62f047d6eb213f2f8855dcb8a13 \ - --hash=sha256:921a37e2aefc64145e7b73d50c71bb4f26f46e4c9f414dc648c6245ff92cf7db +s3transfer==0.5.0 \ + --hash=sha256:50ed823e1dc5868ad40c8dc92072f757aa0e653a192845c94a3b676f4a62da4c \ + --hash=sha256:9c1dc369814391a6bda20ebbf4b70a0f34630592c9aa520856bf384916af2803 urllib3==1.25.9 \ --hash=sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527 \ --hash=sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115 diff --git a/remediation_worker/jobs/ec2_close_port_3389/ec2_close_port_3389.py b/remediation_worker/jobs/ec2_close_port_3389/ec2_close_port_3389.py index 4510429..1e4344b 100644 --- a/remediation_worker/jobs/ec2_close_port_3389/ec2_close_port_3389.py +++ b/remediation_worker/jobs/ec2_close_port_3389/ec2_close_port_3389.py @@ -13,7 +13,6 @@ # limitations under the License. from __future__ import annotations -from botocore.exceptions import ClientError import json import logging @@ -58,8 +57,8 @@ def parse(self, payload): return {"instance_id": instance_id}, region def remediate(self, client, instance_id): - """Block public access to port 3389 of all security groups attached to an EC2 instance. - + """Block public access to port 3389 of all security groups attached to an EC2 instance + by removing all the rules with port 3389 in the port range :param client: Instance of the AWS boto3 client. :param instance_id: The ID of the EC2 instance. :type instance_id: str @@ -67,62 +66,50 @@ def remediate(self, client, instance_id): :rtype: int :raises: botocore.exceptions.ClientError """ - - port = 3389 - security_groups = client.describe_instances(InstanceIds=[instance_id])[ - "Reservations" - ][0]["Instances"][0]["SecurityGroups"] - for sg_info in security_groups: - security_group_id = sg_info["GroupId"] - - # Revoke ipv4 permission - logging.info("revoking ivp4 permissions") - try: - logging.info(" executing client.revoke_security_group_ingress") - logging.info(' CidrIp="0.0.0.0/0"') - logging.info(f" FromPort={port}") - logging.info(f" GroupId={security_group_id}") - logging.info(' IpProtocol="tcp"') - logging.info(f" ToPort={port}") - client.revoke_security_group_ingress( - CidrIp="0.0.0.0/0", - FromPort=port, - GroupId=security_group_id, - IpProtocol="tcp", - ToPort=port, - ) - except ClientError as e: - if "InvalidPermission.NotFound" not in str(e): - logging.error(f"{str(e)}") - raise - - # Revoke ipv6 permission - logging.info("revoking ivp6 permissions") - try: - logging.info(" executing client.revoke_security_group_ingress") - logging.info(f" FromPort={port}") - logging.info(f" GroupId={security_group_id}") - logging.info(' IpProtocol="tcp"') - logging.info(' "Ipv6Ranges": [{"CidrIpv6": "::/0"}]') - logging.info(f" ToPort={port}") - client.revoke_security_group_ingress( - GroupId=security_group_id, - IpPermissions=[ - { - "FromPort": port, - "IpProtocol": "tcp", - "Ipv6Ranges": [{"CidrIpv6": "::/0"}], - "ToPort": port, - }, - ], + try: + port = 3389 + logging.info(" executing client.describe_instances") + logging.info(f" InstanceId: {instance_id}") + # Extract security group Id + security_groups = client.describe_instances(InstanceIds=[instance_id])[ + "Reservations" + ][0]["Instances"][0]["SecurityGroups"] + for sg_info in security_groups: + security_group_id = sg_info["GroupId"] + logging.info(" executing client.describe_security_group_rules") + logging.info(f" group-id: {security_group_id}") + # List all the security group rules + security_group_rules = client.describe_security_group_rules( + Filters=[{"Name": "group-id", "Values": [security_group_id]},], + MaxResults=1000, ) - except ClientError as e: - if "InvalidPermission.NotFound" not in str(e): - logging.error(f"{str(e)}") - raise - - logging.info("successfully executed remediation") - + for rule in security_group_rules["SecurityGroupRules"]: + if ( + rule["IpProtocol"] == "tcp" + and rule["IsEgress"] is False + and rule["FromPort"] <= port + and rule["ToPort"] >= port + and ( + ("CidrIpv4" in rule and rule["CidrIpv4"] == "0.0.0.0/0") + or ("CidrIpv6" in rule and rule["CidrIpv6"] == "::/0") + ) + ): + # Removes Ingress security group rule containing port 3389 in the range with + # protocol 'tcp', source '0.0.0.0/0' or '::/0' + logging.info( + " executing client.revoke_security_group_ingress" + ) + logging.info(f" GroupId: {security_group_id}") + logging.info( + f" SecurityGroupRuleIds: {rule['SecurityGroupRuleId']}" + ) + client.revoke_security_group_ingress( + GroupId=security_group_id, + SecurityGroupRuleIds=[rule["SecurityGroupRuleId"]], + ) + logging.info("successfully executed remediation") + except Exception as e: + logging.error(f"{str(e)}") return 0 def run(self, args): diff --git a/remediation_worker/jobs/ec2_close_port_3389/minimum_policy.json b/remediation_worker/jobs/ec2_close_port_3389/minimum_policy.json index ba2ba9a..e91af08 100644 --- a/remediation_worker/jobs/ec2_close_port_3389/minimum_policy.json +++ b/remediation_worker/jobs/ec2_close_port_3389/minimum_policy.json @@ -2,11 +2,12 @@ "Version": "2012-10-17", "Statement": [ { - "Sid": "EC2ClosePort3389", + "Sid": "EC2ClosePort22", "Effect": "Allow", "Action": [ "ec2:DescribeInstances", - "ec2:RevokeSecurityGroupIngress" + "ec2:RevokeSecurityGroupIngress", + "ec2:DescribeSecurityGroupRules" ], "Resource": "*" } diff --git a/remediation_worker/jobs/ec2_close_port_3389/requirements.txt b/remediation_worker/jobs/ec2_close_port_3389/requirements.txt index b239388..93f7845 100644 --- a/remediation_worker/jobs/ec2_close_port_3389/requirements.txt +++ b/remediation_worker/jobs/ec2_close_port_3389/requirements.txt @@ -1,6 +1,6 @@ -boto3==1.14.9 \ - --hash=sha256:185f7b36c16f76e501d8dfc5cd209113426e078e4968dd13cc355c916bc99597 \ - --hash=sha256:51243ba0e976343ca0b98bb4a15fc3d588526220f6ba45bfed7ea45472b1e033 -botocore==1.17.9 \ - --hash=sha256:7dd59bc766d567ca83bc6113aa139d92ba447738ccdfcd40788848553d329a52 \ - --hash=sha256:cd4bb2d96ff2ec6bf4fbcdb2f241d0fb6ba1e7955b4721cf1d81f13db02768b6 +boto3==1.18.4 \ + --hash=sha256:649ed1ca205f5ee0b0328d54580780aebc1a7a05681a24f6ee05253007ca48d8 \ + --hash=sha256:7079b40bd6621c54a0385a8fc11240cff4318a4d487292653e393e18254f5d94 +botocore==1.21.5 \ + --hash=sha256:0070c5e02b581db40ff5fd1b5e02db90ed88e7e861901894bd78fd998656da68 \ + --hash=sha256:bed34fe7a007180f4208b65515bab1755cdd9fcf2c6720f74ae7ecd2e707f4b7 diff --git a/remediation_worker/jobs/ec2_close_port_5439/README.md b/remediation_worker/jobs/ec2_close_port_5439/README.md new file mode 100644 index 0000000..c7faec7 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_5439/README.md @@ -0,0 +1,53 @@ +# Close Port 5439 for all Security Groups associated with an EC2 Instance + +This job blocks public access to port 5439 for both IPv4 and IPv6 for all security groups associated with an EC2 instance by removing all the ingress security group rules containing port 5439 in the port range and source as "0.0.0.0/0" or "::/0". + +### Applicable Rule + +##### Rule ID: +5c8c26447a550e1fb6560c44 + +##### Rule Name: +EC2 instance should restrict public access to Redshift port (5439) + +## Getting Started + +### Prerequisites + +The provided AWS credential must have access to `ec2:DescribeInstances`, `ec2:RevokeSecurityGroupIngress`, `ec2:DescribeSecurityGroupRules`. + +You may find the latest example policy file [here](minimum_policy.json) + +### Running the script + +You may run this script using following commands: +```shell script + pip install -r ../../requirements.txt + python3 ec2_close_port_5439.py +``` + +## Running the tests +You may run test using following command under vss-remediation-worker-job-code-python directory: +```shell script + python3 -m pytest +``` + +## Contributing +The Secure State team welcomes welcomes contributions from the community. If you wish to contribute code and you have not signed our contributor license agreement (CLA), our bot will update the issue when you open a Pull Request. For any questions about the CLA process, please refer to our [FAQ](https://cla.vmware.com/faq). +All contributions to this repository must be signed as described on that page. Your signature certifies that you wrote the patch or have the right to pass it on as an open-source patch. + +For more detailed information, refer to [CONTRIBUTING.md](../../../CONTRIBUTING.md). + +## Versioning + +We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/vmware-samples/secure-state-remediation-jobs/tags). + +## Authors + +* **VMware Secure State** - *Initial work* + +See also the list of [contributors](https://github.com/vmware-samples/secure-state-remediation-jobs/contributors) who participated in this project. + +## License + +This project is licensed under the Apache License - see the [LICENSE](https://github.com/vmware-samples/secure-state-remediation-jobs/blob/master/LICENSE.txt) file for details diff --git a/remediation_worker/jobs/ec2_close_port_5439/__init__.py b/remediation_worker/jobs/ec2_close_port_5439/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/remediation_worker/jobs/ec2_close_port_5439/constraints.txt b/remediation_worker/jobs/ec2_close_port_5439/constraints.txt new file mode 100644 index 0000000..0d9b3c2 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_5439/constraints.txt @@ -0,0 +1,43 @@ +docutils==0.15.2 \ + --hash=sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0 \ + --hash=sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827 \ + --hash=sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99 +jmespath==0.10.0 \ + --hash=sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9 \ + --hash=sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f +python-dateutil==2.8.1 \ + --hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \ + --hash=sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a +s3transfer==0.5.0 \ + --hash=sha256:50ed823e1dc5868ad40c8dc92072f757aa0e653a192845c94a3b676f4a62da4c \ + --hash=sha256:9c1dc369814391a6bda20ebbf4b70a0f34630592c9aa520856bf384916af2803 +urllib3==1.25.9 \ + --hash=sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527 \ + --hash=sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115 +six==1.15.0 \ + --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \ + --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced +packaging==20.4 \ + --hash=sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8 \ + --hash=sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181 +attrs==19.3.0 \ + --hash=sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c \ + --hash=sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72 +more-itertools==8.4.0 \ + --hash=sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5 \ + --hash=sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2 +pluggy==0.13.1 \ + --hash=sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0 \ + --hash=sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d +py==1.9.0 \ + --hash=sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2 \ + --hash=sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342 +toml==0.10.1 \ + --hash=sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f \ + --hash=sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88 +iniconfig==1.0.1 \ + --hash=sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437 \ + --hash=sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69 +pyparsing==2.4.7 \ + --hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1 \ + --hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b diff --git a/remediation_worker/jobs/ec2_close_port_5439/ec2_close_port_5439.py b/remediation_worker/jobs/ec2_close_port_5439/ec2_close_port_5439.py new file mode 100644 index 0000000..77802c5 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_5439/ec2_close_port_5439.py @@ -0,0 +1,131 @@ +# Copyright (c) 2020 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations +import json +import logging +import sys + +import boto3 + +logging.basicConfig(level=logging.INFO) + + +class EC2ClosePort5439(object): + def parse(self, payload): + """Parse payload received from Remediation Service. + + :param payload: JSON string containing parameters received from the remediation service. + :type payload: str. + :returns: Dictionary of parsed parameters + :rtype: dict + :raises: Exception, JSONDecodeError + """ + remediation_entry = json.loads(payload) + notification_info = remediation_entry.get("notificationInfo", None) + + finding_info = notification_info.get("FindingInfo", None) + instance_id = finding_info.get("ObjectId", None) + + if instance_id is None: + logging.error("Missing parameters for 'payload.notificationInfo.ObjectId'.") + raise Exception( + "Missing parameters for 'payload.notificationInfo.ObjectId'." + ) + + region = finding_info.get("Region", None) + if region is None: + logging.warning("no region specified - defaulting to us-east-1") + region = "us-east-1" + + logging.info("parsed params") + logging.info(f" instance_id: {instance_id}") + logging.info(f" region: {region}") + + return {"instance_id": instance_id}, region + + def remediate(self, client, instance_id): + """Block public access to port 5439 of all security groups attached to an EC2 instance + by removing all the rules with port 5439 in the port range + :param client: Instance of the AWS boto3 client. + :param instance_id: The ID of the EC2 instance. + :type instance_id: str + :returns: Integer signaling success or failure + :rtype: int + :raises: botocore.exceptions.ClientError + """ + try: + port = 5439 + logging.info(" executing client.describe_instances") + logging.info(f" InstanceId: {instance_id}") + # Extract security group Id + security_groups = client.describe_instances(InstanceIds=[instance_id])[ + "Reservations" + ][0]["Instances"][0]["SecurityGroups"] + for sg_info in security_groups: + security_group_id = sg_info["GroupId"] + logging.info(" executing client.describe_security_group_rules") + logging.info(f" group-id: {security_group_id}") + # List all the security group rules + security_group_rules = client.describe_security_group_rules( + Filters=[{"Name": "group-id", "Values": [security_group_id]},], + MaxResults=1000, + ) + for rule in security_group_rules["SecurityGroupRules"]: + if ( + rule["IpProtocol"] == "tcp" + and rule["IsEgress"] is False + and rule["FromPort"] <= port + and rule["ToPort"] >= port + and ( + ("CidrIpv4" in rule and rule["CidrIpv4"] == "0.0.0.0/0") + or ("CidrIpv6" in rule and rule["CidrIpv6"] == "::/0") + ) + ): + # Removes Ingress security group rule containing port 5439 in the range with + # protocol 'tcp', source '0.0.0.0/0' or '::/0' + logging.info( + " executing client.revoke_security_group_ingress" + ) + logging.info(f" GroupId: {security_group_id}") + logging.info( + f" SecurityGroupRuleIds: {rule['SecurityGroupRuleId']}" + ) + client.revoke_security_group_ingress( + GroupId=security_group_id, + SecurityGroupRuleIds=[rule["SecurityGroupRuleId"]], + ) + logging.info("successfully executed remediation") + except Exception as e: + logging.error(f"{str(e)}") + return 0 + + def run(self, args): + """Run the remediation job. + + :param args: List of arguments provided to the job. + :type args: list. + :returns: int + """ + params, region = self.parse(args[1]) + client = boto3.client("ec2", region_name=region) + logging.info("acquired ec2 client and parsed params - starting remediation") + rc = self.remediate(client=client, **params) + return rc + + +if __name__ == "__main__": + logging.info(f"{sys.argv[0]} called - running now") + obj = EC2ClosePort5439() + obj.run(sys.argv) diff --git a/remediation_worker/jobs/ec2_close_port_5439/minimum_policy.json b/remediation_worker/jobs/ec2_close_port_5439/minimum_policy.json new file mode 100644 index 0000000..e91af08 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_5439/minimum_policy.json @@ -0,0 +1,15 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "EC2ClosePort22", + "Effect": "Allow", + "Action": [ + "ec2:DescribeInstances", + "ec2:RevokeSecurityGroupIngress", + "ec2:DescribeSecurityGroupRules" + ], + "Resource": "*" + } + ] +} diff --git a/remediation_worker/jobs/ec2_close_port_5439/requirements-dev.txt b/remediation_worker/jobs/ec2_close_port_5439/requirements-dev.txt new file mode 100644 index 0000000..9412e93 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_5439/requirements-dev.txt @@ -0,0 +1,6 @@ +-r requirements.txt +-c constraints.txt + +pytest==6.0.1 \ + --hash=sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4 \ + --hash=sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad diff --git a/remediation_worker/jobs/ec2_close_port_5439/requirements.txt b/remediation_worker/jobs/ec2_close_port_5439/requirements.txt new file mode 100644 index 0000000..93f7845 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_5439/requirements.txt @@ -0,0 +1,6 @@ +boto3==1.18.4 \ + --hash=sha256:649ed1ca205f5ee0b0328d54580780aebc1a7a05681a24f6ee05253007ca48d8 \ + --hash=sha256:7079b40bd6621c54a0385a8fc11240cff4318a4d487292653e393e18254f5d94 +botocore==1.21.5 \ + --hash=sha256:0070c5e02b581db40ff5fd1b5e02db90ed88e7e861901894bd78fd998656da68 \ + --hash=sha256:bed34fe7a007180f4208b65515bab1755cdd9fcf2c6720f74ae7ecd2e707f4b7 diff --git a/remediation_worker/jobs/ec2_close_port_5601/README.md b/remediation_worker/jobs/ec2_close_port_5601/README.md new file mode 100644 index 0000000..ce8c0e2 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_5601/README.md @@ -0,0 +1,53 @@ +# Close Port 5601 for all Security Groups associated with an EC2 Instance + +This job blocks public access to port 5601 for both IPv4 and IPv6 for all security groups associated with an EC2 instance by removing all the ingress security group rules containing port 5601 in the port range and source as "0.0.0.0/0" or "::/0". + +### Applicable Rule + +##### Rule ID: +4823ede0-7bed-4af0-a182-81c2ada80203 + +##### Rule Name: +EC2 instance should restrict public access to Kibana port (5601) + +## Getting Started + +### Prerequisites + +The provided AWS credential must have access to `ec2:DescribeInstances`, `ec2:RevokeSecurityGroupIngress`, `ec2:DescribeSecurityGroupRules`. + +You may find the latest example policy file [here](minimum_policy.json) + +### Running the script + +You may run this script using following commands: +```shell script + pip install -r ../../requirements.txt + python3 ec2_close_port_5601.py +``` + +## Running the tests +You may run test using following command under vss-remediation-worker-job-code-python directory: +```shell script + python3 -m pytest +``` + +## Contributing +The Secure State team welcomes welcomes contributions from the community. If you wish to contribute code and you have not signed our contributor license agreement (CLA), our bot will update the issue when you open a Pull Request. For any questions about the CLA process, please refer to our [FAQ](https://cla.vmware.com/faq). +All contributions to this repository must be signed as described on that page. Your signature certifies that you wrote the patch or have the right to pass it on as an open-source patch. + +For more detailed information, refer to [CONTRIBUTING.md](../../../CONTRIBUTING.md). + +## Versioning + +We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/vmware-samples/secure-state-remediation-jobs/tags). + +## Authors + +* **VMware Secure State** - *Initial work* + +See also the list of [contributors](https://github.com/vmware-samples/secure-state-remediation-jobs/contributors) who participated in this project. + +## License + +This project is licensed under the Apache License - see the [LICENSE](https://github.com/vmware-samples/secure-state-remediation-jobs/blob/master/LICENSE.txt) file for details diff --git a/remediation_worker/jobs/ec2_close_port_5601/__init__.py b/remediation_worker/jobs/ec2_close_port_5601/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/remediation_worker/jobs/ec2_close_port_5601/constraints.txt b/remediation_worker/jobs/ec2_close_port_5601/constraints.txt new file mode 100644 index 0000000..1c0a19d --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_5601/constraints.txt @@ -0,0 +1,43 @@ +docutils==0.15.2 \ + --hash=sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0 \ + --hash=sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827 \ + --hash=sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99 +jmespath==0.7.1 \ + --hash=sha256:cb99b58dcf853f791bec28aa016c121663b79ee6125c892297d773d7fd4dcecf \ + --hash=sha256:cd5a12ee3dfa470283a020a35e69e83b0700d44fe413014fd35ad5584c5f5fd1 +python-dateutil==2.8.1 \ + --hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \ + --hash=sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a +s3transfer==0.5.0 \ + --hash=sha256:50ed823e1dc5868ad40c8dc92072f757aa0e653a192845c94a3b676f4a62da4c \ + --hash=sha256:9c1dc369814391a6bda20ebbf4b70a0f34630592c9aa520856bf384916af2803 +urllib3==1.25.9 \ + --hash=sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527 \ + --hash=sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115 +six==1.15.0 \ + --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \ + --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced +packaging==20.4 \ + --hash=sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8 \ + --hash=sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181 +attrs==19.3.0 \ + --hash=sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c \ + --hash=sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72 +more-itertools==8.4.0 \ + --hash=sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5 \ + --hash=sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2 +pluggy==0.13.1 \ + --hash=sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0 \ + --hash=sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d +py==1.9.0 \ + --hash=sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2 \ + --hash=sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342 +toml==0.10.1 \ + --hash=sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f \ + --hash=sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88 +iniconfig==1.0.1 \ + --hash=sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437 \ + --hash=sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69 +pyparsing==2.4.7 \ + --hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1 \ + --hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b diff --git a/remediation_worker/jobs/ec2_close_port_5601/ec2_close_port_5601.py b/remediation_worker/jobs/ec2_close_port_5601/ec2_close_port_5601.py new file mode 100644 index 0000000..2b19fbc --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_5601/ec2_close_port_5601.py @@ -0,0 +1,131 @@ +# Copyright (c) 2020 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations +import json +import logging +import sys + +import boto3 + +logging.basicConfig(level=logging.INFO) + + +class EC2ClosePort5601(object): + def parse(self, payload): + """Parse payload received from Remediation Service. + + :param payload: JSON string containing parameters received from the remediation service. + :type payload: str. + :returns: Dictionary of parsed parameters + :rtype: dict + :raises: Exception, JSONDecodeError + """ + remediation_entry = json.loads(payload) + notification_info = remediation_entry.get("notificationInfo", None) + + finding_info = notification_info.get("FindingInfo", None) + instance_id = finding_info.get("ObjectId", None) + + if instance_id is None: + logging.error("Missing parameters for 'payload.notificationInfo.ObjectId'.") + raise Exception( + "Missing parameters for 'payload.notificationInfo.ObjectId'." + ) + + region = finding_info.get("Region", None) + if region is None: + logging.warning("no region specified - defaulting to us-east-1") + region = "us-east-1" + + logging.info("parsed params") + logging.info(f" instance_id: {instance_id}") + logging.info(f" region: {region}") + + return {"instance_id": instance_id}, region + + def remediate(self, client, instance_id): + """Block public access to port 5601 of all security groups attached to an EC2 instance + by removing all the rules with port 5601 in the port range + :param client: Instance of the AWS boto3 client. + :param instance_id: The ID of the EC2 instance. + :type instance_id: str + :returns: Integer signaling success or failure + :rtype: int + :raises: botocore.exceptions.ClientError + """ + try: + port = 5601 + logging.info(" executing client.describe_instances") + logging.info(f" InstanceId: {instance_id}") + # Extract security group Id + security_groups = client.describe_instances(InstanceIds=[instance_id])[ + "Reservations" + ][0]["Instances"][0]["SecurityGroups"] + for sg_info in security_groups: + security_group_id = sg_info["GroupId"] + logging.info(" executing client.describe_security_group_rules") + logging.info(f" group-id: {security_group_id}") + # List all the security group rules + security_group_rules = client.describe_security_group_rules( + Filters=[{"Name": "group-id", "Values": [security_group_id]},], + MaxResults=1000, + ) + for rule in security_group_rules["SecurityGroupRules"]: + if ( + rule["IpProtocol"] == "tcp" + and rule["IsEgress"] is False + and rule["FromPort"] <= port + and rule["ToPort"] >= port + and ( + ("CidrIpv4" in rule and rule["CidrIpv4"] == "0.0.0.0/0") + or ("CidrIpv6" in rule and rule["CidrIpv6"] == "::/0") + ) + ): + # Removes Ingress security group rule containing port 5601 in the range with + # protocol 'tcp', source '0.0.0.0/0' or '::/0' + logging.info( + " executing client.revoke_security_group_ingress" + ) + logging.info(f" GroupId: {security_group_id}") + logging.info( + f" SecurityGroupRuleIds: {rule['SecurityGroupRuleId']}" + ) + client.revoke_security_group_ingress( + GroupId=security_group_id, + SecurityGroupRuleIds=[rule["SecurityGroupRuleId"]], + ) + logging.info("successfully executed remediation") + except Exception as e: + logging.error(f"{str(e)}") + return 0 + + def run(self, args): + """Run the remediation job. + + :param args: List of arguments provided to the job. + :type args: list. + :returns: int + """ + params, region = self.parse(args[1]) + client = boto3.client("ec2", region_name=region) + logging.info("acquired ec2 client and parsed params - starting remediation") + rc = self.remediate(client=client, **params) + return rc + + +if __name__ == "__main__": + logging.info(f"{sys.argv[0]} called - running now") + obj = EC2ClosePort5601() + obj.run(sys.argv) diff --git a/remediation_worker/jobs/ec2_close_port_5601/minimum_policy.json b/remediation_worker/jobs/ec2_close_port_5601/minimum_policy.json new file mode 100644 index 0000000..e91af08 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_5601/minimum_policy.json @@ -0,0 +1,15 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "EC2ClosePort22", + "Effect": "Allow", + "Action": [ + "ec2:DescribeInstances", + "ec2:RevokeSecurityGroupIngress", + "ec2:DescribeSecurityGroupRules" + ], + "Resource": "*" + } + ] +} diff --git a/remediation_worker/jobs/ec2_close_port_5601/requirements-dev.txt b/remediation_worker/jobs/ec2_close_port_5601/requirements-dev.txt new file mode 100644 index 0000000..c9dd3c9 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_5601/requirements-dev.txt @@ -0,0 +1,6 @@ +-r requirements.txt +-c constraints.txt + +pytest==6.0.1 \ + --hash=sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4 \ + --hash=sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad \ No newline at end of file diff --git a/remediation_worker/jobs/ec2_close_port_5601/requirements.txt b/remediation_worker/jobs/ec2_close_port_5601/requirements.txt new file mode 100644 index 0000000..93f7845 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_5601/requirements.txt @@ -0,0 +1,6 @@ +boto3==1.18.4 \ + --hash=sha256:649ed1ca205f5ee0b0328d54580780aebc1a7a05681a24f6ee05253007ca48d8 \ + --hash=sha256:7079b40bd6621c54a0385a8fc11240cff4318a4d487292653e393e18254f5d94 +botocore==1.21.5 \ + --hash=sha256:0070c5e02b581db40ff5fd1b5e02db90ed88e7e861901894bd78fd998656da68 \ + --hash=sha256:bed34fe7a007180f4208b65515bab1755cdd9fcf2c6720f74ae7ecd2e707f4b7 diff --git a/remediation_worker/jobs/ec2_close_port_8080/README.md b/remediation_worker/jobs/ec2_close_port_8080/README.md new file mode 100644 index 0000000..67d5265 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_8080/README.md @@ -0,0 +1,53 @@ +# Close Port 8080 for all Security Groups associated with an EC2 Instance + +This job blocks public access to port 8080 for both IPv4 and IPv6 for all security groups associated with an EC2 instance by removing all the ingress security group rules containing port 8080 in the port range and source as "0.0.0.0/0" or "::/0". + +### Applicable Rule + +##### Rule ID: +5c8c26407a550e1fb6560c3c + +##### Rule Name: +EC2 instance should restrict public access to TCP port (8080) + +## Getting Started + +### Prerequisites + +The provided AWS credential must have access to `ec2:DescribeInstances`, `ec2:RevokeSecurityGroupIngress`, `ec2:DescribeSecurityGroupRules`. + +You may find the latest example policy file [here](minimum_policy.json) + +### Running the script + +You may run this script using following commands: +```shell script + pip install -r ../../requirements.txt + python3 ec2_close_port_8080.py +``` + +## Running the tests +You may run test using following command under vss-remediation-worker-job-code-python directory: +```shell script + python3 -m pytest +``` + +## Contributing +The Secure State team welcomes welcomes contributions from the community. If you wish to contribute code and you have not signed our contributor license agreement (CLA), our bot will update the issue when you open a Pull Request. For any questions about the CLA process, please refer to our [FAQ](https://cla.vmware.com/faq). +All contributions to this repository must be signed as described on that page. Your signature certifies that you wrote the patch or have the right to pass it on as an open-source patch. + +For more detailed information, refer to [CONTRIBUTING.md](../../../CONTRIBUTING.md). + +## Versioning + +We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/vmware-samples/secure-state-remediation-jobs/tags). + +## Authors + +* **VMware Secure State** - *Initial work* + +See also the list of [contributors](https://github.com/vmware-samples/secure-state-remediation-jobs/contributors) who participated in this project. + +## License + +This project is licensed under the Apache License - see the [LICENSE](https://github.com/vmware-samples/secure-state-remediation-jobs/blob/master/LICENSE.txt) file for details diff --git a/remediation_worker/jobs/ec2_close_port_8080/__init__.py b/remediation_worker/jobs/ec2_close_port_8080/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/remediation_worker/jobs/ec2_close_port_8080/constraints.txt b/remediation_worker/jobs/ec2_close_port_8080/constraints.txt new file mode 100644 index 0000000..0d9b3c2 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_8080/constraints.txt @@ -0,0 +1,43 @@ +docutils==0.15.2 \ + --hash=sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0 \ + --hash=sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827 \ + --hash=sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99 +jmespath==0.10.0 \ + --hash=sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9 \ + --hash=sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f +python-dateutil==2.8.1 \ + --hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \ + --hash=sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a +s3transfer==0.5.0 \ + --hash=sha256:50ed823e1dc5868ad40c8dc92072f757aa0e653a192845c94a3b676f4a62da4c \ + --hash=sha256:9c1dc369814391a6bda20ebbf4b70a0f34630592c9aa520856bf384916af2803 +urllib3==1.25.9 \ + --hash=sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527 \ + --hash=sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115 +six==1.15.0 \ + --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \ + --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced +packaging==20.4 \ + --hash=sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8 \ + --hash=sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181 +attrs==19.3.0 \ + --hash=sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c \ + --hash=sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72 +more-itertools==8.4.0 \ + --hash=sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5 \ + --hash=sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2 +pluggy==0.13.1 \ + --hash=sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0 \ + --hash=sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d +py==1.9.0 \ + --hash=sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2 \ + --hash=sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342 +toml==0.10.1 \ + --hash=sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f \ + --hash=sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88 +iniconfig==1.0.1 \ + --hash=sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437 \ + --hash=sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69 +pyparsing==2.4.7 \ + --hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1 \ + --hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b diff --git a/remediation_worker/jobs/ec2_close_port_8080/ec2_close_port_8080.py b/remediation_worker/jobs/ec2_close_port_8080/ec2_close_port_8080.py new file mode 100644 index 0000000..75865c5 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_8080/ec2_close_port_8080.py @@ -0,0 +1,131 @@ +# Copyright (c) 2020 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations +import json +import logging +import sys + +import boto3 + +logging.basicConfig(level=logging.INFO) + + +class EC2ClosePort8080(object): + def parse(self, payload): + """Parse payload received from Remediation Service. + + :param payload: JSON string containing parameters received from the remediation service. + :type payload: str. + :returns: Dictionary of parsed parameters + :rtype: dict + :raises: Exception, JSONDecodeError + """ + remediation_entry = json.loads(payload) + notification_info = remediation_entry.get("notificationInfo", None) + + finding_info = notification_info.get("FindingInfo", None) + instance_id = finding_info.get("ObjectId", None) + + if instance_id is None: + logging.error("Missing parameters for 'payload.notificationInfo.ObjectId'.") + raise Exception( + "Missing parameters for 'payload.notificationInfo.ObjectId'." + ) + + region = finding_info.get("Region", None) + if region is None: + logging.warning("no region specified - defaulting to us-east-1") + region = "us-east-1" + + logging.info("parsed params") + logging.info(f" instance_id: {instance_id}") + logging.info(f" region: {region}") + + return {"instance_id": instance_id}, region + + def remediate(self, client, instance_id): + """Block public access to port 8080 of all security groups attached to an EC2 instance + by removing all the rules with port 8080 in the port range + :param client: Instance of the AWS boto3 client. + :param instance_id: The ID of the EC2 instance. + :type instance_id: str + :returns: Integer signaling success or failure + :rtype: int + :raises: botocore.exceptions.ClientError + """ + try: + port = 8080 + logging.info(" executing client.describe_instances") + logging.info(f" InstanceId: {instance_id}") + # Extract security group Id + security_groups = client.describe_instances(InstanceIds=[instance_id])[ + "Reservations" + ][0]["Instances"][0]["SecurityGroups"] + for sg_info in security_groups: + security_group_id = sg_info["GroupId"] + logging.info(" executing client.describe_security_group_rules") + logging.info(f" group-id: {security_group_id}") + # List all the security group rules + security_group_rules = client.describe_security_group_rules( + Filters=[{"Name": "group-id", "Values": [security_group_id]},], + MaxResults=1000, + ) + for rule in security_group_rules["SecurityGroupRules"]: + if ( + rule["IpProtocol"] == "tcp" + and rule["IsEgress"] is False + and rule["FromPort"] <= port + and rule["ToPort"] >= port + and ( + ("CidrIpv4" in rule and rule["CidrIpv4"] == "0.0.0.0/0") + or ("CidrIpv6" in rule and rule["CidrIpv6"] == "::/0") + ) + ): + # Removes Ingress security group rule containing port 8080 in the range with + # protocol 'tcp', source '0.0.0.0/0' or '::/0' + logging.info( + " executing client.revoke_security_group_ingress" + ) + logging.info(f" GroupId: {security_group_id}") + logging.info( + f" SecurityGroupRuleIds: {rule['SecurityGroupRuleId']}" + ) + client.revoke_security_group_ingress( + GroupId=security_group_id, + SecurityGroupRuleIds=[rule["SecurityGroupRuleId"]], + ) + logging.info("successfully executed remediation") + except Exception as e: + logging.error(f"{str(e)}") + return 0 + + def run(self, args): + """Run the remediation job. + + :param args: List of arguments provided to the job. + :type args: list. + :returns: int + """ + params, region = self.parse(args[1]) + client = boto3.client("ec2", region_name=region) + logging.info("acquired ec2 client and parsed params - starting remediation") + rc = self.remediate(client=client, **params) + return rc + + +if __name__ == "__main__": + logging.info(f"{sys.argv[0]} called - running now") + obj = EC2ClosePort8080() + obj.run(sys.argv) diff --git a/remediation_worker/jobs/ec2_close_port_8080/finding.json b/remediation_worker/jobs/ec2_close_port_8080/finding.json new file mode 100644 index 0000000..1e9d64a --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_8080/finding.json @@ -0,0 +1,8 @@ +{ + "notificationInfo": { + "FindingInfo": { + "ObjectId": "i-0b0c39c38f5a42f95", + "Region": "us-west-1" + } + } +} diff --git a/remediation_worker/jobs/ec2_close_port_8080/minimum_policy.json b/remediation_worker/jobs/ec2_close_port_8080/minimum_policy.json new file mode 100644 index 0000000..e91af08 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_8080/minimum_policy.json @@ -0,0 +1,15 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "EC2ClosePort22", + "Effect": "Allow", + "Action": [ + "ec2:DescribeInstances", + "ec2:RevokeSecurityGroupIngress", + "ec2:DescribeSecurityGroupRules" + ], + "Resource": "*" + } + ] +} diff --git a/remediation_worker/jobs/ec2_close_port_8080/requirements-dev.txt b/remediation_worker/jobs/ec2_close_port_8080/requirements-dev.txt new file mode 100644 index 0000000..9412e93 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_8080/requirements-dev.txt @@ -0,0 +1,6 @@ +-r requirements.txt +-c constraints.txt + +pytest==6.0.1 \ + --hash=sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4 \ + --hash=sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad diff --git a/remediation_worker/jobs/ec2_close_port_8080/requirements.txt b/remediation_worker/jobs/ec2_close_port_8080/requirements.txt new file mode 100644 index 0000000..93f7845 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_8080/requirements.txt @@ -0,0 +1,6 @@ +boto3==1.18.4 \ + --hash=sha256:649ed1ca205f5ee0b0328d54580780aebc1a7a05681a24f6ee05253007ca48d8 \ + --hash=sha256:7079b40bd6621c54a0385a8fc11240cff4318a4d487292653e393e18254f5d94 +botocore==1.21.5 \ + --hash=sha256:0070c5e02b581db40ff5fd1b5e02db90ed88e7e861901894bd78fd998656da68 \ + --hash=sha256:bed34fe7a007180f4208b65515bab1755cdd9fcf2c6720f74ae7ecd2e707f4b7 diff --git a/remediation_worker/jobs/ec2_close_port_9200_9300/README.md b/remediation_worker/jobs/ec2_close_port_9200_9300/README.md new file mode 100644 index 0000000..a322715 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_9200_9300/README.md @@ -0,0 +1,53 @@ +# Close Port 9200, 9300 for all Security Groups associated with an EC2 Instance + +This job blocks public access to port 9200, 9300 for both IPv4 and IPv6 for all security groups associated with an EC2 instance by removing all the ingress security group rules containing port 9200, 9300 in the port range and source as "0.0.0.0/0" or "::/0". + +### Applicable Rule + +##### Rule ID: +04700175-adbe-49e1-bc7a-bc9605597ce2 + +##### Rule Name: +EC2 instance should restrict public access to Elasticsearch ports (9200 and 9300) + +## Getting Started + +### Prerequisites + +The provided AWS credential must have access to `ec2:DescribeInstances`, `ec2:RevokeSecurityGroupIngress`, `ec2:DescribeSecurityGroupRules`. + +You may find the latest example policy file [here](minimum_policy.json) + +### Running the script + +You may run this script using following commands: +```shell script + pip install -r ../../requirements.txt + python3 ec2_close_port_9200_9300.py +``` + +## Running the tests +You may run test using following command under vss-remediation-worker-job-code-python directory: +```shell script + python3 -m pytest +``` + +## Contributing +The Secure State team welcomes welcomes contributions from the community. If you wish to contribute code and you have not signed our contributor license agreement (CLA), our bot will update the issue when you open a Pull Request. For any questions about the CLA process, please refer to our [FAQ](https://cla.vmware.com/faq). +All contributions to this repository must be signed as described on that page. Your signature certifies that you wrote the patch or have the right to pass it on as an open-source patch. + +For more detailed information, refer to [CONTRIBUTING.md](../../../CONTRIBUTING.md). + +## Versioning + +We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/vmware-samples/secure-state-remediation-jobs/tags). + +## Authors + +* **VMware Secure State** - *Initial work* + +See also the list of [contributors](https://github.com/vmware-samples/secure-state-remediation-jobs/contributors) who participated in this project. + +## License + +This project is licensed under the Apache License - see the [LICENSE](https://github.com/vmware-samples/secure-state-remediation-jobs/blob/master/LICENSE.txt) file for details diff --git a/remediation_worker/jobs/ec2_close_port_9200_9300/__init__.py b/remediation_worker/jobs/ec2_close_port_9200_9300/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/remediation_worker/jobs/ec2_close_port_9200_9300/constraints.txt b/remediation_worker/jobs/ec2_close_port_9200_9300/constraints.txt new file mode 100644 index 0000000..0d9b3c2 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_9200_9300/constraints.txt @@ -0,0 +1,43 @@ +docutils==0.15.2 \ + --hash=sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0 \ + --hash=sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827 \ + --hash=sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99 +jmespath==0.10.0 \ + --hash=sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9 \ + --hash=sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f +python-dateutil==2.8.1 \ + --hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \ + --hash=sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a +s3transfer==0.5.0 \ + --hash=sha256:50ed823e1dc5868ad40c8dc92072f757aa0e653a192845c94a3b676f4a62da4c \ + --hash=sha256:9c1dc369814391a6bda20ebbf4b70a0f34630592c9aa520856bf384916af2803 +urllib3==1.25.9 \ + --hash=sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527 \ + --hash=sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115 +six==1.15.0 \ + --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \ + --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced +packaging==20.4 \ + --hash=sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8 \ + --hash=sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181 +attrs==19.3.0 \ + --hash=sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c \ + --hash=sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72 +more-itertools==8.4.0 \ + --hash=sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5 \ + --hash=sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2 +pluggy==0.13.1 \ + --hash=sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0 \ + --hash=sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d +py==1.9.0 \ + --hash=sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2 \ + --hash=sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342 +toml==0.10.1 \ + --hash=sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f \ + --hash=sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88 +iniconfig==1.0.1 \ + --hash=sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437 \ + --hash=sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69 +pyparsing==2.4.7 \ + --hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1 \ + --hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b diff --git a/remediation_worker/jobs/ec2_close_port_9200_9300/ec2_close_port_9200_9300.py b/remediation_worker/jobs/ec2_close_port_9200_9300/ec2_close_port_9200_9300.py new file mode 100644 index 0000000..b3979c1 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_9200_9300/ec2_close_port_9200_9300.py @@ -0,0 +1,133 @@ +# Copyright (c) 2020 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations +import json +import logging +import sys + +import boto3 + +logging.basicConfig(level=logging.INFO) + + +class EC2ClosePort9200_9300(object): + def parse(self, payload): + """Parse payload received from Remediation Service. + + :param payload: JSON string containing parameters received from the remediation service. + :type payload: str. + :returns: Dictionary of parsed parameters + :rtype: dict + :raises: Exception, JSONDecodeError + """ + remediation_entry = json.loads(payload) + notification_info = remediation_entry.get("notificationInfo", None) + + finding_info = notification_info.get("FindingInfo", None) + instance_id = finding_info.get("ObjectId", None) + + if instance_id is None: + logging.error("Missing parameters for 'payload.notificationInfo.ObjectId'.") + raise Exception( + "Missing parameters for 'payload.notificationInfo.ObjectId'." + ) + + region = finding_info.get("Region", None) + if region is None: + logging.warning("no region specified - defaulting to us-east-1") + region = "us-east-1" + + logging.info("parsed params") + logging.info(f" instance_id: {instance_id}") + logging.info(f" region: {region}") + + return {"instance_id": instance_id}, region + + def remediate(self, client, instance_id): + """Block public access to port 9200 and 9300 of all security groups attached to an EC2 instance + by removing all the rules with port 9200 and 9300 in the port range + :param client: Instance of the AWS boto3 client. + :param instance_id: The ID of the EC2 instance. + :type instance_id: str + :returns: Integer signaling success or failure + :rtype: int + :raises: botocore.exceptions.ClientError + """ + try: + ports = [9200, 9300] + logging.info(" executing client.describe_instances") + logging.info(f" InstanceId: {instance_id}") + # Extract security group Id + security_groups = client.describe_instances(InstanceIds=[instance_id])[ + "Reservations" + ][0]["Instances"][0]["SecurityGroups"] + for sg_info in security_groups: + security_group_id = sg_info["GroupId"] + for port in ports: + logging.info(" executing client.describe_security_group_rules") + logging.info(f" group-id: {security_group_id}") + # List all the security group rules + security_group_rules = client.describe_security_group_rules( + Filters=[{"Name": "group-id", "Values": [security_group_id]},], + MaxResults=1000, + ) + for rule in security_group_rules["SecurityGroupRules"]: + if ( + rule["IpProtocol"] == "tcp" + and rule["IsEgress"] is False + and rule["FromPort"] <= port + and rule["ToPort"] >= port + and ( + ("CidrIpv4" in rule and rule["CidrIpv4"] == "0.0.0.0/0") + or ("CidrIpv6" in rule and rule["CidrIpv6"] == "::/0") + ) + ): + # Removes Ingress security group rule containing port 27017 in the range with + # protocol 'tcp', source '0.0.0.0/0' or '::/0' + logging.info( + " executing client.revoke_security_group_ingress" + ) + logging.info(f" GroupId: {security_group_id}") + logging.info( + f" SecurityGroupRuleIds: {rule['SecurityGroupRuleId']}" + ) + client.revoke_security_group_ingress( + GroupId=security_group_id, + SecurityGroupRuleIds=[rule["SecurityGroupRuleId"]], + ) + logging.info("successfully executed remediation") + except Exception as e: + logging.error(f"{str(e)}") + + return 0 + + def run(self, args): + """Run the remediation job. + + :param args: List of arguments provided to the job. + :type args: list. + :returns: int + """ + params, region = self.parse(args[1]) + client = boto3.client("ec2", region_name=region) + logging.info("acquired ec2 client and parsed params - starting remediation") + rc = self.remediate(client=client, **params) + return rc + + +if __name__ == "__main__": + logging.info(f"{sys.argv[0]} called - running now") + obj = EC2ClosePort9200_9300() + obj.run(sys.argv) diff --git a/remediation_worker/jobs/ec2_close_port_9200_9300/minimum_policy.json b/remediation_worker/jobs/ec2_close_port_9200_9300/minimum_policy.json new file mode 100644 index 0000000..e91af08 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_9200_9300/minimum_policy.json @@ -0,0 +1,15 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "EC2ClosePort22", + "Effect": "Allow", + "Action": [ + "ec2:DescribeInstances", + "ec2:RevokeSecurityGroupIngress", + "ec2:DescribeSecurityGroupRules" + ], + "Resource": "*" + } + ] +} diff --git a/remediation_worker/jobs/ec2_close_port_9200_9300/requirements-dev.txt b/remediation_worker/jobs/ec2_close_port_9200_9300/requirements-dev.txt new file mode 100644 index 0000000..9412e93 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_9200_9300/requirements-dev.txt @@ -0,0 +1,6 @@ +-r requirements.txt +-c constraints.txt + +pytest==6.0.1 \ + --hash=sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4 \ + --hash=sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad diff --git a/remediation_worker/jobs/ec2_close_port_9200_9300/requirements.txt b/remediation_worker/jobs/ec2_close_port_9200_9300/requirements.txt new file mode 100644 index 0000000..93f7845 --- /dev/null +++ b/remediation_worker/jobs/ec2_close_port_9200_9300/requirements.txt @@ -0,0 +1,6 @@ +boto3==1.18.4 \ + --hash=sha256:649ed1ca205f5ee0b0328d54580780aebc1a7a05681a24f6ee05253007ca48d8 \ + --hash=sha256:7079b40bd6621c54a0385a8fc11240cff4318a4d487292653e393e18254f5d94 +botocore==1.21.5 \ + --hash=sha256:0070c5e02b581db40ff5fd1b5e02db90ed88e7e861901894bd78fd998656da68 \ + --hash=sha256:bed34fe7a007180f4208b65515bab1755cdd9fcf2c6720f74ae7ecd2e707f4b7 diff --git a/remediation_worker/jobs/kinesis_encrypt_stream/README.md b/remediation_worker/jobs/kinesis_encrypt_stream/README.md new file mode 100644 index 0000000..bec73bc --- /dev/null +++ b/remediation_worker/jobs/kinesis_encrypt_stream/README.md @@ -0,0 +1,77 @@ +# Encrypt Kinesis data stream + +This job encrypts the Kinesis data stream. + +## Getting Started + +##### Rule ID: +ce603728-d631-4bae-8657-c22da6e5944e + +##### Rule Name: +Kinesis data stream should be encrypted + +### Prerequisites + +The provided AWS credential must have access to the Kinesis data stream. + +You may find the latest example policy file [here](minimum_policy.json) + +### Running the script + +You may run this script using following commands: +```shell script + pip install -r ../../requirements.txt + python3 kinesis_encrypt_stream.py "`cat finding.json`" +``` +where finding.json has kinesis data stream and region info: +```json +{ + "notificationInfo": { + "FindingInfo": { + "ObjectId": "kinesis-1", + "Region": "us-west-2" + } + } +} +``` + +## Running the tests +You may run test using following command under vss-remediation-worker-job-code-python directory: +```shell script + python3 -m pytest test +``` + +## Deployment +1. Provision a Virtual Machine +Create an EC2 instance to use for the worker. The minimum required specifications are 128 MB memory and 1/2 Core CPU. +2. Setup Docker +Install Docker on the newly provisioned EC2 instance. You can refer to the [docs here](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/docker-basics.html) for more information. +3. Deploy the worker image +SSH into the EC2 instance and run the command below to deploy the worker image: +```shell script + docker run --rm -it --name worker \ + -e VSS_CLIENT_ID={ENTER CLIENT ID} + -e VSS_CLIENT_SECRET={ENTER CLIENT SECRET} \ + vmware/vss-remediation-worker:latest-python +``` + + +## Contributing +The Secure State team welcomes welcomes contributions from the community. If you wish to contribute code and you have not signed our contributor license agreement (CLA), our bot will update the issue when you open a Pull Request. For any questions about the CLA process, please refer to our [FAQ](https://cla.vmware.com/faq). +All contributions to this repository must be signed as described on that page. Your signature certifies that you wrote the patch or have the right to pass it on as an open-source patch. + +For more detailed information, refer to [CONTRIBUTING.md](../../../CONTRIBUTING.md). + +## Versioning + +We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/vmware-samples/secure-state-remediation-jobs/tags). + +## Authors + +* **VMware Secure State** - *Initial work* + +See also the list of [contributors](https://github.com/vmware-samples/secure-state-remediation-jobs/contributors) who participated in this project. + +## License + +This project is licensed under the Apache License - see the [LICENSE](https://github.com/vmware-samples/secure-state-remediation-jobs/blob/master/LICENSE.txt) file for details diff --git a/remediation_worker/jobs/kinesis_encrypt_stream/__init__.py b/remediation_worker/jobs/kinesis_encrypt_stream/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/remediation_worker/jobs/kinesis_encrypt_stream/constraints.txt b/remediation_worker/jobs/kinesis_encrypt_stream/constraints.txt new file mode 100644 index 0000000..6b211d2 --- /dev/null +++ b/remediation_worker/jobs/kinesis_encrypt_stream/constraints.txt @@ -0,0 +1,43 @@ +docutils==0.15.2 \ + --hash=sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0 \ + --hash=sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827 \ + --hash=sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99 +jmespath==0.10.0 \ + --hash=sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9 \ + --hash=sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f +python-dateutil==2.8.1 \ + --hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \ + --hash=sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a +s3transfer==0.3.3 \ + --hash=sha256:2482b4259524933a022d59da830f51bd746db62f047d6eb213f2f8855dcb8a13 \ + --hash=sha256:921a37e2aefc64145e7b73d50c71bb4f26f46e4c9f414dc648c6245ff92cf7db +urllib3==1.25.9 \ + --hash=sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527 \ + --hash=sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115 +six==1.15.0 \ + --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \ + --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced +packaging==20.4 \ + --hash=sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8 \ + --hash=sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181 +attrs==19.3.0 \ + --hash=sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c \ + --hash=sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72 +more-itertools==8.4.0 \ + --hash=sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5 \ + --hash=sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2 +pluggy==0.13.1 \ + --hash=sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0 \ + --hash=sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d +py==1.9.0 \ + --hash=sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2 \ + --hash=sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342 +toml==0.10.1 \ + --hash=sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f \ + --hash=sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88 +iniconfig==1.0.1 \ + --hash=sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437 \ + --hash=sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69 +pyparsing==2.4.7 \ + --hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1 \ + --hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b diff --git a/remediation_worker/jobs/kinesis_encrypt_stream/kinesis_encrypt_stream.py b/remediation_worker/jobs/kinesis_encrypt_stream/kinesis_encrypt_stream.py new file mode 100644 index 0000000..0141569 --- /dev/null +++ b/remediation_worker/jobs/kinesis_encrypt_stream/kinesis_encrypt_stream.py @@ -0,0 +1,188 @@ +# Copyright (c) 2021 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import json +import logging +import sys + +import boto3 +from botocore.exceptions import ClientError + +logging.basicConfig(level=logging.INFO) + + +class KinesisEncryptStream(): + def parse(self, payload): + """Parse payload received from Remediation Service. + Args: + payload: (str) JSON string containing parameters + received from the remediation service. + Raises: + Exception: JSONDecodeError + Returns: + (dict) Dictionary of parsed parameters. + """ + remediation_entry = json.loads(payload) + notification_info = remediation_entry.get("notificationInfo", None) + finding_info = notification_info.get("FindingInfo", None) + + stream_name = finding_info.get("ObjectId", None) + region = finding_info.get("Region", None) + + if stream_name is None: + logging.error("Missing parameters for 'STREAM_NAME'.") + raise Exception("Missing parameters for 'stream_name'.") + + if region is None: + logging.error("Missing parameters for 'REGION'.") + raise Exception("Missing parameters for 'REGION'.") + + logging.info("parsed params:") + logging.info(" stream_name: %s", stream_name) + logging.info(" region: %s", region) + + return { + "stream_name": stream_name, + "region": region + } + + def remediate(self, stream_name, client, region): + """Encrypts unencrypted kinesis data streams. + Args: + stream_name ([str]): The name of the kinesis stream. + client: Instance of the AWS boto3 client. + """ + + logging.info( + "Beginning kinesis data stream encryption evaluation." + ) + + # Check to see if kinesis data stream is encrypted + encryption_type = self.check_encryption(client, stream_name) + + # If kinesis data stream is not encrypted + if encryption_type == "NONE": + res = self.enable_encryption(client, stream_name) + return res + # If encryption is enabled + # We have nothing to do. + # log and exit + elif encryption_type == "KMS": + logging.info( + "Kinesis data stream %s is already encrypted, taking no action.", + stream_name + ) + return 0 + # If anything else returns, + # log and exit + else: + error = "An unknown encryption type {0} returned for kinesis data stream {1}. Exiting".format(encryption_type, stream_name) + logging.error(error) + return 1 + + def check_encryption(self, client, stream_name): + """Checks the kinesis data stream encryption setting. + Args: + client: Instance of the AWS boto3 client + stream_name ([str]): The name of the kinesis stream. + Returns: + [string]: "NONE" if no encryption is enabled. + """ + + # Check if stream is encrypted + try: + encryption_type = client.describe_stream( + StreamName=stream_name)["StreamDescription"]["EncryptionType"] + return encryption_type + + except ClientError as user_error: + if user_error.response["Error"]["Code"] == "ResourceNotFoundException": + logging.error( + "A failure occured with error: %s. Check if the resource exists.", user_error.response['Error']['Code'] + ) + elif user_error.response["Error"]["Code"] == "LimitExceededException": + error = "Receiving LimitExceededException exception: {0} while trying to encrypt kinesis data stream {1}".format(stream_name,user_error.response["Error"]["Code"]) + logging.error(error) + + + except Exception as e: + error = "Receiving other exceptions {0} reading kinesis data stream {1}".format(str(e), stream_name) + logging.error(error) + + return "" + + def enable_encryption(self, client, stream_name): + """Enable encryption on the kinesis data stream. + Args: + client: Instance of the AWS boto3 client. + stream_name ([str]): The name of the kinesis stream. + """ + logging.info( + "Enabling encryption on kinesis data stream %s", + stream_name + ) + try: + client.start_stream_encryption( + StreamName=stream_name, + EncryptionType="KMS", + KeyId="alias/aws/kinesis" # KEK owned by Kinesis Data Streams + ) + logging.info( + "Encryption enabled on kinesis data stream %s", + stream_name + ) + return 0 + except ClientError as user_error: + if user_error.response["Error"]["Code"] == "ResourceInUseException": + logging.error( + "Kinesis data stream %s is in an unavailable state.", + stream_name + ) + else: + error = "Receiving Kinesis exception: {0} while tyring to encrypt kinesis data stream {1}".format(stream_name,user_error.response["Error"]["Code"]) + logging.error(error) + return 1 + except Exception as e: + error = "Receiving exception {0} for kinesis data stream {1}".format(str(e), stream_name) + logging.error(error) + return 1 + + + def run(self, args): + """Run the remediation job. + Args: + args ([list]): List of arguments provided to the job. + Returns: + [int] + """ + + params = self.parse(args[1]) + client = boto3.client("kinesis", region_name=params['region']) + + logging.info( + "Acquired kinesis client and parsed params - starting remediation." + ) + + return self.remediate(client=client, **params) + + +if __name__ == "__main__": + logging.info( + "kinesis_encrypt_stream.py called - running now %s", + sys.argv[0] + ) + obj = KinesisEncryptStream() + obj.run(sys.argv) diff --git a/remediation_worker/jobs/kinesis_encrypt_stream/minimum_policy.json b/remediation_worker/jobs/kinesis_encrypt_stream/minimum_policy.json new file mode 100644 index 0000000..e3bf2b9 --- /dev/null +++ b/remediation_worker/jobs/kinesis_encrypt_stream/minimum_policy.json @@ -0,0 +1,14 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "Kinesis", + "Effect": "Allow", + "Action": [ + "Kinesis:DescribeStream", + "Kinesis:StartStreamEncryption" + ], + "Resource": "*" + } + ] +} diff --git a/remediation_worker/jobs/kinesis_encrypt_stream/requirements-dev.txt b/remediation_worker/jobs/kinesis_encrypt_stream/requirements-dev.txt new file mode 100644 index 0000000..c9dd3c9 --- /dev/null +++ b/remediation_worker/jobs/kinesis_encrypt_stream/requirements-dev.txt @@ -0,0 +1,6 @@ +-r requirements.txt +-c constraints.txt + +pytest==6.0.1 \ + --hash=sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4 \ + --hash=sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad \ No newline at end of file diff --git a/remediation_worker/jobs/kinesis_encrypt_stream/requirements.txt b/remediation_worker/jobs/kinesis_encrypt_stream/requirements.txt new file mode 100644 index 0000000..b239388 --- /dev/null +++ b/remediation_worker/jobs/kinesis_encrypt_stream/requirements.txt @@ -0,0 +1,6 @@ +boto3==1.14.9 \ + --hash=sha256:185f7b36c16f76e501d8dfc5cd209113426e078e4968dd13cc355c916bc99597 \ + --hash=sha256:51243ba0e976343ca0b98bb4a15fc3d588526220f6ba45bfed7ea45472b1e033 +botocore==1.17.9 \ + --hash=sha256:7dd59bc766d567ca83bc6113aa139d92ba447738ccdfcd40788848553d329a52 \ + --hash=sha256:cd4bb2d96ff2ec6bf4fbcdb2f241d0fb6ba1e7955b4721cf1d81f13db02768b6 diff --git a/remediation_worker/jobs/rds_enable_version_update/README.md b/remediation_worker/jobs/rds_enable_version_update/README.md new file mode 100644 index 0000000..a8e4066 --- /dev/null +++ b/remediation_worker/jobs/rds_enable_version_update/README.md @@ -0,0 +1,75 @@ +# Enable automatic minor version upgrade for RDS DBInstance + +This job enables automatic minor version upgrade RDS DBInstance. + +## Getting Started + +##### Rule ID: +5c8c264a7a550e1fb6560c4c + +##### Rule Name: +RDS should have automatic minor version upgrades enabled + +### Prerequisites + +The provided AWS credential must have permissions that listed in the policy file [here](minimum_policy.json) + +### Running the script + +You may run this script using following commands: +```shell script + pip install -r ../../requirements.txt + python3 rds_enable_version_upgrade.py "`cat finding.json`" +``` + where finding.json has launch config name and region info: +```json + { + "notificationInfo": { + "FindingInfo": { + "ObjectId": "rds-name", + "Region": "us-west-1" + } + } +} +``` + +## Running the tests +You may run test using following command under vss-remediation-worker-job-code-python directory: +```shell script + python3 -m pytest test +``` + +## Deployment +1. Provision a Virtual Machine +Create an EC2 instance to use for the worker. The minimum required specifications are 128 MB memory and 1/2 Core CPU. +2. Setup Docker +Install Docker on the newly provisioned EC2 instance. You can refer to the [docs here](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/docker-basics.html) for more information. +3. Deploy the worker image +SSH into the EC2 instance and run the command below to deploy the worker image: +```shell script + docker run --rm -it --name worker \ + -e VSS_CLIENT_ID={ENTER CLIENT ID} + -e VSS_CLIENT_SECRET={ENTER CLIENT SECRET} \ + vmware/vss-remediation-worker:latest-python +``` + + +## Contributing +The Secure State team welcomes welcomes contributions from the community. If you wish to contribute code and you have not signed our contributor license agreement (CLA), our bot will update the issue when you open a Pull Request. For any questions about the CLA process, please refer to our [FAQ](https://cla.vmware.com/faq). +All contributions to this repository must be signed as described on that page. Your signature certifies that you wrote the patch or have the right to pass it on as an open-source patch. + +For more detailed information, refer to [CONTRIBUTING.md](../../../CONTRIBUTING.md). + +## Versioning + +We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/vmware-samples/secure-state-remediation-jobs/tags). + +## Authors + +* **VMware Secure State** - *Initial work* + +See also the list of [contributors](https://github.com/vmware-samples/secure-state-remediation-jobs/contributors) who participated in this project. + +## License + +This project is licensed under the Apache License - see the [LICENSE](https://github.com/vmware-samples/secure-state-remediation-jobs/blob/master/LICENSE.txt) file for details diff --git a/remediation_worker/jobs/rds_enable_version_update/__init__.py b/remediation_worker/jobs/rds_enable_version_update/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/remediation_worker/jobs/rds_enable_version_update/constraints.txt b/remediation_worker/jobs/rds_enable_version_update/constraints.txt new file mode 100644 index 0000000..6b211d2 --- /dev/null +++ b/remediation_worker/jobs/rds_enable_version_update/constraints.txt @@ -0,0 +1,43 @@ +docutils==0.15.2 \ + --hash=sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0 \ + --hash=sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827 \ + --hash=sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99 +jmespath==0.10.0 \ + --hash=sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9 \ + --hash=sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f +python-dateutil==2.8.1 \ + --hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \ + --hash=sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a +s3transfer==0.3.3 \ + --hash=sha256:2482b4259524933a022d59da830f51bd746db62f047d6eb213f2f8855dcb8a13 \ + --hash=sha256:921a37e2aefc64145e7b73d50c71bb4f26f46e4c9f414dc648c6245ff92cf7db +urllib3==1.25.9 \ + --hash=sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527 \ + --hash=sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115 +six==1.15.0 \ + --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \ + --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced +packaging==20.4 \ + --hash=sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8 \ + --hash=sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181 +attrs==19.3.0 \ + --hash=sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c \ + --hash=sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72 +more-itertools==8.4.0 \ + --hash=sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5 \ + --hash=sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2 +pluggy==0.13.1 \ + --hash=sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0 \ + --hash=sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d +py==1.9.0 \ + --hash=sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2 \ + --hash=sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342 +toml==0.10.1 \ + --hash=sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f \ + --hash=sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88 +iniconfig==1.0.1 \ + --hash=sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437 \ + --hash=sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69 +pyparsing==2.4.7 \ + --hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1 \ + --hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b diff --git a/remediation_worker/jobs/rds_enable_version_update/minimum_policy.json b/remediation_worker/jobs/rds_enable_version_update/minimum_policy.json new file mode 100644 index 0000000..5569777 --- /dev/null +++ b/remediation_worker/jobs/rds_enable_version_update/minimum_policy.json @@ -0,0 +1,14 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "RDSPermissions", + "Effect": "Allow", + "Action": [ + "rds:DescribeDBInstances", + "rds:ModifyDBInstance" + ], + "Resource": "*" + } + ] +} diff --git a/remediation_worker/jobs/rds_enable_version_update/rds_enable_version_update.py b/remediation_worker/jobs/rds_enable_version_update/rds_enable_version_update.py new file mode 100644 index 0000000..cb49a3e --- /dev/null +++ b/remediation_worker/jobs/rds_enable_version_update/rds_enable_version_update.py @@ -0,0 +1,121 @@ +# Copyright (c) 2021 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import logging +import sys + +import boto3 +from botocore.exceptions import ClientError + +logging.basicConfig(level=logging.INFO) + + +class RDSUpgradMinorVersion: + def parse(self, payload): + """Parse payload received from Remediation Service. + :param payload: JSON string containing parameters sent to the remediation job. + :type payload: str. + :returns: Dictionary of parsed parameters + :rtype: dict + :raises: Exception, JSONDecodeError + """ + remediation_entry = json.loads(payload) + notification_info = remediation_entry.get("notificationInfo", None) + finding_info = notification_info.get("FindingInfo", None) + + database_id = finding_info.get("ObjectId", None) + region = finding_info.get("Region", None) + + if database_id is None: + logging.error("Missing parameters for 'DATABASE_ID'.") + raise Exception("Missing parameters for 'DATABASE_ID'.") + + logging.info("parsed params") + logging.info(" database_id: %s", database_id) + logging.info(" region: %s", region) + + return { + "database_id": database_id, + "region": region + } + + def remediate(self, client, database_id): + """Enable automatic minor version upgrades. Any instance with this value + disabled will be converted to true. + :param client: Instance of the AWS boto3 client. + :param database_id: The id of the RDS instanace. + :returns: updates flag for RDS auto upgrade minor version + """ + + logging.info("Updating auto minor version upgrade for %s", database_id) + + # convert the RDS auto minor version flag to True + try: + logging.info("Setting AutoMinorVersionUpgrade to True by calling client.modify_db_instance") + logging.info(f"DBInstanceIdentifier={database_id}") + client.modify_db_instance( + DBInstanceIdentifier=database_id, + AutoMinorVersionUpgrade=True, + ApplyImmediately=True) + + except ClientError as state_err: + # If the remediation is run during maintenance + # or a creation/modification, inform it is not in the right state + # user should retry. + # This is b/c you can describe a bad instance state + # with no errors, but not modify. + # Otherwise, we would catch this error + # on the "describe_db_instances" call + if state_err.response["Error"]["Code"] == "InvalidDBInstanceState": + logging.info( + "RDS database %s is in an unavailable state.", + database_id + ) + + logging.error( + "Remediation RDS database %s failed", + database_id + ) + return 1 + except Exception as e: + error = "Receiving other exceptions {0} while changing RDS database {1} minor version upgrade".format(str(e), instance_id) + logging.error(error) + return 1 + + logging.info("Updated auto minor version upgrade for %s successfully.", database_id) + return 0 + + def run(self, args): + """Run the remediation job. + :param args: List of arguments provided to the job. + :type args: list. + :returns: int + """ + params = self.parse(args[1]) + client = boto3.client("rds", region_name=params['region']) + + logging.info( + "acquired rds client and parsed params - starting remediation." + ) + + return self.remediate(client, params['database_id']) + + +if __name__ == "__main__": + logging.info("rds_enable_version_update.py called - running now %s", + sys.argv[0]) + + obj = RDSUpgradMinorVersion() + obj.run(sys.argv) diff --git a/remediation_worker/jobs/rds_enable_version_update/requirements-dev.txt b/remediation_worker/jobs/rds_enable_version_update/requirements-dev.txt new file mode 100644 index 0000000..9412e93 --- /dev/null +++ b/remediation_worker/jobs/rds_enable_version_update/requirements-dev.txt @@ -0,0 +1,6 @@ +-r requirements.txt +-c constraints.txt + +pytest==6.0.1 \ + --hash=sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4 \ + --hash=sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad diff --git a/remediation_worker/jobs/rds_enable_version_update/requirements.txt b/remediation_worker/jobs/rds_enable_version_update/requirements.txt new file mode 100644 index 0000000..b239388 --- /dev/null +++ b/remediation_worker/jobs/rds_enable_version_update/requirements.txt @@ -0,0 +1,6 @@ +boto3==1.14.9 \ + --hash=sha256:185f7b36c16f76e501d8dfc5cd209113426e078e4968dd13cc355c916bc99597 \ + --hash=sha256:51243ba0e976343ca0b98bb4a15fc3d588526220f6ba45bfed7ea45472b1e033 +botocore==1.17.9 \ + --hash=sha256:7dd59bc766d567ca83bc6113aa139d92ba447738ccdfcd40788848553d329a52 \ + --hash=sha256:cd4bb2d96ff2ec6bf4fbcdb2f241d0fb6ba1e7955b4721cf1d81f13db02768b6 diff --git a/remediation_worker/jobs/rds_remove_public_endpoint/README.md b/remediation_worker/jobs/rds_remove_public_endpoint/README.md new file mode 100644 index 0000000..d2a271b --- /dev/null +++ b/remediation_worker/jobs/rds_remove_public_endpoint/README.md @@ -0,0 +1,74 @@ +# Disable public access to RDS DBInstances + +This job removes public access from RDS instances and makes the instances private. + +## Getting Started + +##### Rule ID: +5c8c26467a550e1fb6560c48 + +##### Rule Name: +RDS instance should restrict public access + +### Prerequisites + +The provided AWS credential must have permissions that listed in the policy file [here](minimum_policy.json) + +### Running the script +You may run this script using following commands: +```shell script + pip install -r ../../requirements.txt + python3 rds_remove_public_endpoint.py "`cat finding.json`" +``` + where finding.json has volume id and region info: +```json + { + "notificationInfo": { + "FindingInfo": { + "ObjectId": "database-1", + "Region": "us-west-2" + } + } +} +``` + +## Running the tests +You may run test using following command under vss-remediation-worker-job-code-python directory: +```shell script + python3 -m pytest test +``` + +## Deployment +1. Provision a Virtual Machine +Create an EC2 instance to use for the worker. The minimum required specifications are 128 MB memory and 1/2 Core CPU. +2. Setup Docker +Install Docker on the newly provisioned EC2 instance. You can refer to the [docs here](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/docker-basics.html) for more information. +3. Deploy the worker image +SSH into the EC2 instance and run the command below to deploy the worker image: +```shell script + docker run --rm -it --name worker \ + -e VSS_CLIENT_ID={ENTER CLIENT ID} + -e VSS_CLIENT_SECRET={ENTER CLIENT SECRET} \ + vmware/vss-remediation-worker:latest-python +``` + + +## Contributing +The Secure State team welcomes welcomes contributions from the community. If you wish to contribute code and you have not signed our contributor license agreement (CLA), our bot will update the issue when you open a Pull Request. For any questions about the CLA process, please refer to our [FAQ](https://cla.vmware.com/faq). +All contributions to this repository must be signed as described on that page. Your signature certifies that you wrote the patch or have the right to pass it on as an open-source patch. + +For more detailed information, refer to [CONTRIBUTING.md](../../../CONTRIBUTING.md). + +## Versioning + +We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/vmware-samples/secure-state-remediation-jobs/tags). + +## Authors + +* **VMware Secure State** - *Initial work* + +See also the list of [contributors](https://github.com/vmware-samples/secure-state-remediation-jobs/contributors) who participated in this project. + +## License + +This project is licensed under the Apache License - see the [LICENSE](https://github.com/vmware-samples/secure-state-remediation-jobs/blob/master/LICENSE.txt) file for details diff --git a/remediation_worker/jobs/rds_remove_public_endpoint/__init__.py b/remediation_worker/jobs/rds_remove_public_endpoint/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/remediation_worker/jobs/rds_remove_public_endpoint/constraints.txt b/remediation_worker/jobs/rds_remove_public_endpoint/constraints.txt new file mode 100644 index 0000000..6b211d2 --- /dev/null +++ b/remediation_worker/jobs/rds_remove_public_endpoint/constraints.txt @@ -0,0 +1,43 @@ +docutils==0.15.2 \ + --hash=sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0 \ + --hash=sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827 \ + --hash=sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99 +jmespath==0.10.0 \ + --hash=sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9 \ + --hash=sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f +python-dateutil==2.8.1 \ + --hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \ + --hash=sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a +s3transfer==0.3.3 \ + --hash=sha256:2482b4259524933a022d59da830f51bd746db62f047d6eb213f2f8855dcb8a13 \ + --hash=sha256:921a37e2aefc64145e7b73d50c71bb4f26f46e4c9f414dc648c6245ff92cf7db +urllib3==1.25.9 \ + --hash=sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527 \ + --hash=sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115 +six==1.15.0 \ + --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \ + --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced +packaging==20.4 \ + --hash=sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8 \ + --hash=sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181 +attrs==19.3.0 \ + --hash=sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c \ + --hash=sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72 +more-itertools==8.4.0 \ + --hash=sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5 \ + --hash=sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2 +pluggy==0.13.1 \ + --hash=sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0 \ + --hash=sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d +py==1.9.0 \ + --hash=sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2 \ + --hash=sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342 +toml==0.10.1 \ + --hash=sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f \ + --hash=sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88 +iniconfig==1.0.1 \ + --hash=sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437 \ + --hash=sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69 +pyparsing==2.4.7 \ + --hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1 \ + --hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b diff --git a/remediation_worker/jobs/rds_remove_public_endpoint/minimum_policy.json b/remediation_worker/jobs/rds_remove_public_endpoint/minimum_policy.json new file mode 100644 index 0000000..5569777 --- /dev/null +++ b/remediation_worker/jobs/rds_remove_public_endpoint/minimum_policy.json @@ -0,0 +1,14 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "RDSPermissions", + "Effect": "Allow", + "Action": [ + "rds:DescribeDBInstances", + "rds:ModifyDBInstance" + ], + "Resource": "*" + } + ] +} diff --git a/remediation_worker/jobs/rds_remove_public_endpoint/rds_remove_public_endpoint.py b/remediation_worker/jobs/rds_remove_public_endpoint/rds_remove_public_endpoint.py new file mode 100644 index 0000000..b56e143 --- /dev/null +++ b/remediation_worker/jobs/rds_remove_public_endpoint/rds_remove_public_endpoint.py @@ -0,0 +1,160 @@ +# Copyright (c) 2021 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import json +import logging +import sys + +import boto3 +from botocore.exceptions import ClientError + +logging.basicConfig(level=logging.INFO) + + +class RDSRemovePublicEndpoint(): + def parse(self, payload): + """Parse payload received from Remediation Service. + + Args: + payload: (str) JSON string containing parameters + received from the remediation service. + + Raises: + Exception: JSONDecodeError + + Returns: + (dict) Dictionary of parsed parameters. + """ + remediation_entry = json.loads(payload) + notification_info = remediation_entry.get("notificationInfo", None) + finding_info = notification_info.get("FindingInfo", None) + + instance_id = finding_info.get("ObjectId", None) + region = finding_info.get("Region", None) + + if instance_id is None: + logging.error("Missing parameters for 'INSTANCE_ID'.") + raise Exception("Missing parameters for 'INSTANCE_ID'.") + + if region is None: + logging.error("Missing parameters for 'REGION'.") + raise Exception("Missing parameters for 'REGION'.") + + logging.info("parsed params") + logging.info(" instance_id: %s", instance_id) + logging.info(" region: %s", region) + + return { + "instance_id": instance_id, + "region": region + } + + def remediate(self, client, instance_id): + """ + Removes public access from RDS instances + and makes the instances private. + + Args: + instance_id ([str]): The ID of the RDS instance. + client: Instance of the AWS boto3 client. + """ + + logging.info("Beginning RDS public endpoint evaluation.") + + # Check to see if the database + # is publicly accessible + logging.info("executing client.describe_db_instances") + logging.info(f"RDS instance={instance_id}") + is_public = client.describe_db_instances( + DBInstanceIdentifier=instance_id).get( + "DBInstances")[0]["PubliclyAccessible"] + + # If it is public, set to private + if is_public: + logging.info( + "RDS database %s is public, setting to private...", + instance_id + ) + try: + logging.info("executing client.modify_db_instance and apply immediately") + logging.info("Attribute=PubliclyAccessible") + client.modify_db_instance( + DBInstanceIdentifier=instance_id, + PubliclyAccessible=False, + ApplyImmediately=True + ) + logging.info( + "RDS database %s is now private.", + instance_id + ) + except ClientError as state_err: + # If the remediation is run during maintenance + # or a creation/modification, inform it is not in the right state + # user should retry. + # This is b/c you can describe a bad instance state + # with no errors, but not modify. + # Otherwise, we would catch this error + # on the "describe_db_instances" call + if state_err.response["Error"]["Code"] == "InvalidDBInstanceState": + logging.info( + "RDS database %s is in an unavailable state. Waiting..", + instance_id + ) + + logging.error( + "Remediation RDS database %s failed", + instance_id + ) + return 1 + except Exception as e: + error = "Receiving other exceptions {0} while setting RDS database {1} to private".format(str(e), instance_id) + logging.error(error) + return 1 + + else: + logging.info( + "RDS database %s is private, taking no action.", + instance_id + ) + return 0 + + def run(self, args): + """ + Run the remediation job. + + Args: + args ([list]): List of arguments provided to the job. + + Returns: + [int] + """ + params = self.parse(args[1]) + client = boto3.client("rds", region_name=params['region']) + + logging.info( + "acquired rds client and parsed params - starting remediation" + ) + + return self.remediate(client, params['instance_id']) + + +if __name__ == "__main__": + logging.info( + "rds_remove_public_endpoint.py called - running now %s", + sys.argv[0] + ) + obj = RDSRemovePublicEndpoint() + obj.run(sys.argv) diff --git a/remediation_worker/jobs/rds_remove_public_endpoint/requirements-dev.txt b/remediation_worker/jobs/rds_remove_public_endpoint/requirements-dev.txt new file mode 100644 index 0000000..9412e93 --- /dev/null +++ b/remediation_worker/jobs/rds_remove_public_endpoint/requirements-dev.txt @@ -0,0 +1,6 @@ +-r requirements.txt +-c constraints.txt + +pytest==6.0.1 \ + --hash=sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4 \ + --hash=sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad diff --git a/remediation_worker/jobs/rds_remove_public_endpoint/requirements.txt b/remediation_worker/jobs/rds_remove_public_endpoint/requirements.txt new file mode 100644 index 0000000..b239388 --- /dev/null +++ b/remediation_worker/jobs/rds_remove_public_endpoint/requirements.txt @@ -0,0 +1,6 @@ +boto3==1.14.9 \ + --hash=sha256:185f7b36c16f76e501d8dfc5cd209113426e078e4968dd13cc355c916bc99597 \ + --hash=sha256:51243ba0e976343ca0b98bb4a15fc3d588526220f6ba45bfed7ea45472b1e033 +botocore==1.17.9 \ + --hash=sha256:7dd59bc766d567ca83bc6113aa139d92ba447738ccdfcd40788848553d329a52 \ + --hash=sha256:cd4bb2d96ff2ec6bf4fbcdb2f241d0fb6ba1e7955b4721cf1d81f13db02768b6 diff --git a/remediation_worker/jobs/security_group_close_port_22/README.md b/remediation_worker/jobs/security_group_close_port_22/README.md index 112feaa..b6f2a54 100644 --- a/remediation_worker/jobs/security_group_close_port_22/README.md +++ b/remediation_worker/jobs/security_group_close_port_22/README.md @@ -1,6 +1,6 @@ # Close Port 22 for a Security Group -This job blocks public access to port 22 for both IPv4 and IPv6. +This job blocks public access to port 22 for both IPv4 and IPv6 by removing all the ingress security group rules containing port 22 in the port range and source as "0.0.0.0/0" or "::/0". ### Applicable Rule @@ -14,7 +14,7 @@ A security group's SSH port (22) is accessible through any source address ### Prerequisites -The provided AWS credential must have access to `ec2:RevokeSecurityGroupIngress`. +The provided AWS credential must have access to `ec2:RevokeSecurityGroupIngress`, `ec2:DescribeSecurityGroupRules`. You may find the latest example policy file [here](minimum_policy.json) diff --git a/remediation_worker/jobs/security_group_close_port_22/constraints.txt b/remediation_worker/jobs/security_group_close_port_22/constraints.txt index 6b211d2..0d9b3c2 100644 --- a/remediation_worker/jobs/security_group_close_port_22/constraints.txt +++ b/remediation_worker/jobs/security_group_close_port_22/constraints.txt @@ -8,9 +8,9 @@ jmespath==0.10.0 \ python-dateutil==2.8.1 \ --hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \ --hash=sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a -s3transfer==0.3.3 \ - --hash=sha256:2482b4259524933a022d59da830f51bd746db62f047d6eb213f2f8855dcb8a13 \ - --hash=sha256:921a37e2aefc64145e7b73d50c71bb4f26f46e4c9f414dc648c6245ff92cf7db +s3transfer==0.5.0 \ + --hash=sha256:50ed823e1dc5868ad40c8dc92072f757aa0e653a192845c94a3b676f4a62da4c \ + --hash=sha256:9c1dc369814391a6bda20ebbf4b70a0f34630592c9aa520856bf384916af2803 urllib3==1.25.9 \ --hash=sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527 \ --hash=sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115 diff --git a/remediation_worker/jobs/security_group_close_port_22/minimum_policy.json b/remediation_worker/jobs/security_group_close_port_22/minimum_policy.json index 960ac21..c6ceba1 100644 --- a/remediation_worker/jobs/security_group_close_port_22/minimum_policy.json +++ b/remediation_worker/jobs/security_group_close_port_22/minimum_policy.json @@ -5,7 +5,8 @@ "Sid": "SecurityGroupClosePort22", "Effect": "Allow", "Action": [ - "ec2:RevokeSecurityGroupIngress" + "ec2:RevokeSecurityGroupIngress", + "ec2:DescribeSecurityGroupRules" ], "Resource": "*" } diff --git a/remediation_worker/jobs/security_group_close_port_22/requirements.txt b/remediation_worker/jobs/security_group_close_port_22/requirements.txt index b239388..93f7845 100644 --- a/remediation_worker/jobs/security_group_close_port_22/requirements.txt +++ b/remediation_worker/jobs/security_group_close_port_22/requirements.txt @@ -1,6 +1,6 @@ -boto3==1.14.9 \ - --hash=sha256:185f7b36c16f76e501d8dfc5cd209113426e078e4968dd13cc355c916bc99597 \ - --hash=sha256:51243ba0e976343ca0b98bb4a15fc3d588526220f6ba45bfed7ea45472b1e033 -botocore==1.17.9 \ - --hash=sha256:7dd59bc766d567ca83bc6113aa139d92ba447738ccdfcd40788848553d329a52 \ - --hash=sha256:cd4bb2d96ff2ec6bf4fbcdb2f241d0fb6ba1e7955b4721cf1d81f13db02768b6 +boto3==1.18.4 \ + --hash=sha256:649ed1ca205f5ee0b0328d54580780aebc1a7a05681a24f6ee05253007ca48d8 \ + --hash=sha256:7079b40bd6621c54a0385a8fc11240cff4318a4d487292653e393e18254f5d94 +botocore==1.21.5 \ + --hash=sha256:0070c5e02b581db40ff5fd1b5e02db90ed88e7e861901894bd78fd998656da68 \ + --hash=sha256:bed34fe7a007180f4208b65515bab1755cdd9fcf2c6720f74ae7ecd2e707f4b7 diff --git a/remediation_worker/jobs/security_group_close_port_22/security_group_close_port_22.py b/remediation_worker/jobs/security_group_close_port_22/security_group_close_port_22.py index 7c42b60..91a4b7d 100644 --- a/remediation_worker/jobs/security_group_close_port_22/security_group_close_port_22.py +++ b/remediation_worker/jobs/security_group_close_port_22/security_group_close_port_22.py @@ -49,7 +49,8 @@ def parse(self, payload): } def remediate(self, client, security_group_id): - """Block public access to port 22 for both IPv4 and IPv6. + """Block public access to port 22 for both IPv4 and IPv6 by + removing all the rules containing port 22 in the range. :param client: Instance of the AWS boto3 client. :param security_group_id: The ID of the security group. You must specify either the security group ID or the @@ -62,33 +63,39 @@ def remediate(self, client, security_group_id): """ port = 22 - - # Revoke ipv4 permission - logging.info("revoking ivp4 permissions") - logcall( - client.revoke_security_group_ingress, - CidrIp="0.0.0.0/0", - FromPort=port, - GroupId=security_group_id, - IpProtocol="tcp", - ToPort=port, - ) - - # Revoke ipv6 permission - logging.info("revoking ivp6 permissions") - logcall( - client.revoke_security_group_ingress, - GroupId=security_group_id, - IpPermissions=[ - { - "FromPort": port, - "IpProtocol": "tcp", - "Ipv6Ranges": [{"CidrIpv6": "::/0"}], - "ToPort": port, - }, - ], - ) - + try: + logging.info(" executing client.describe_security_group_rules") + logging.info(f" group-id: {security_group_id}") + # List all the security group rules + security_group_rules = client.describe_security_group_rules( + Filters=[{"Name": "group-id", "Values": [security_group_id]},], + MaxResults=1000, + ) + for rule in security_group_rules["SecurityGroupRules"]: + if ( + rule["IpProtocol"] == "tcp" + and rule["IsEgress"] is False + and rule["FromPort"] <= port + and rule["ToPort"] >= port + and ( + ("CidrIpv4" in rule and rule["CidrIpv4"] == "0.0.0.0/0") + or ("CidrIpv6" in rule and rule["CidrIpv6"] == "::/0") + ) + ): + # Removes Ingress security group rule containing port 22 in the range with + # protocol 'tcp', source '0.0.0.0/0' or '::/0' + logging.info(" executing client.revoke_security_group_ingress") + logging.info(f" GroupId: {security_group_id}") + logging.info( + f" SecurityGroupRuleIds: {rule['SecurityGroupRuleId']}" + ) + client.revoke_security_group_ingress( + GroupId=security_group_id, + SecurityGroupRuleIds=[rule["SecurityGroupRuleId"]], + ) + logging.info("successfully executed remediation") + except Exception as e: + logging.error(f"{str(e)}") return 0 def run(self, args): diff --git a/remediation_worker/jobs/security_group_close_port_3389/README.md b/remediation_worker/jobs/security_group_close_port_3389/README.md index b431123..dcf3ab7 100644 --- a/remediation_worker/jobs/security_group_close_port_3389/README.md +++ b/remediation_worker/jobs/security_group_close_port_3389/README.md @@ -1,6 +1,6 @@ # Close Port 3389 for a Security Group -This job blocks public access to port 3389 for both IPv4 and IPv6. +This job blocks public access to port 3389 for both IPv4 and IPv6 by removing all the ingress security group rules containing port 3389 in the port range and source as "0.0.0.0/0" or "::/0". ### Applicable Rule @@ -14,7 +14,7 @@ A security group's Remote Desktop port (3389) is accessible through any source a ### Prerequisites -The provided AWS credential must have access to `ec2:RevokeSecurityGroupIngress`. +The provided AWS credential must have access to `ec2:RevokeSecurityGroupIngress`, `ec2:DescribeSecurityGroupRules`. You may find the latest example policy file [here](minimum_policy.json) diff --git a/remediation_worker/jobs/security_group_close_port_3389/constraints.txt b/remediation_worker/jobs/security_group_close_port_3389/constraints.txt index 6b211d2..0d9b3c2 100644 --- a/remediation_worker/jobs/security_group_close_port_3389/constraints.txt +++ b/remediation_worker/jobs/security_group_close_port_3389/constraints.txt @@ -8,9 +8,9 @@ jmespath==0.10.0 \ python-dateutil==2.8.1 \ --hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \ --hash=sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a -s3transfer==0.3.3 \ - --hash=sha256:2482b4259524933a022d59da830f51bd746db62f047d6eb213f2f8855dcb8a13 \ - --hash=sha256:921a37e2aefc64145e7b73d50c71bb4f26f46e4c9f414dc648c6245ff92cf7db +s3transfer==0.5.0 \ + --hash=sha256:50ed823e1dc5868ad40c8dc92072f757aa0e653a192845c94a3b676f4a62da4c \ + --hash=sha256:9c1dc369814391a6bda20ebbf4b70a0f34630592c9aa520856bf384916af2803 urllib3==1.25.9 \ --hash=sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527 \ --hash=sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115 diff --git a/remediation_worker/jobs/security_group_close_port_3389/minimum_policy.json b/remediation_worker/jobs/security_group_close_port_3389/minimum_policy.json index 5e8d6d9..c6ceba1 100644 --- a/remediation_worker/jobs/security_group_close_port_3389/minimum_policy.json +++ b/remediation_worker/jobs/security_group_close_port_3389/minimum_policy.json @@ -2,10 +2,11 @@ "Version": "2012-10-17", "Statement": [ { - "Sid": "EC2ClosePort22And3389", + "Sid": "SecurityGroupClosePort22", "Effect": "Allow", "Action": [ - "ec2:RevokeSecurityGroupIngress" + "ec2:RevokeSecurityGroupIngress", + "ec2:DescribeSecurityGroupRules" ], "Resource": "*" } diff --git a/remediation_worker/jobs/security_group_close_port_3389/requirements.txt b/remediation_worker/jobs/security_group_close_port_3389/requirements.txt index b239388..93f7845 100644 --- a/remediation_worker/jobs/security_group_close_port_3389/requirements.txt +++ b/remediation_worker/jobs/security_group_close_port_3389/requirements.txt @@ -1,6 +1,6 @@ -boto3==1.14.9 \ - --hash=sha256:185f7b36c16f76e501d8dfc5cd209113426e078e4968dd13cc355c916bc99597 \ - --hash=sha256:51243ba0e976343ca0b98bb4a15fc3d588526220f6ba45bfed7ea45472b1e033 -botocore==1.17.9 \ - --hash=sha256:7dd59bc766d567ca83bc6113aa139d92ba447738ccdfcd40788848553d329a52 \ - --hash=sha256:cd4bb2d96ff2ec6bf4fbcdb2f241d0fb6ba1e7955b4721cf1d81f13db02768b6 +boto3==1.18.4 \ + --hash=sha256:649ed1ca205f5ee0b0328d54580780aebc1a7a05681a24f6ee05253007ca48d8 \ + --hash=sha256:7079b40bd6621c54a0385a8fc11240cff4318a4d487292653e393e18254f5d94 +botocore==1.21.5 \ + --hash=sha256:0070c5e02b581db40ff5fd1b5e02db90ed88e7e861901894bd78fd998656da68 \ + --hash=sha256:bed34fe7a007180f4208b65515bab1755cdd9fcf2c6720f74ae7ecd2e707f4b7 diff --git a/remediation_worker/jobs/security_group_close_port_3389/security_group_close_port_3389.py b/remediation_worker/jobs/security_group_close_port_3389/security_group_close_port_3389.py index fddf675..1a27438 100644 --- a/remediation_worker/jobs/security_group_close_port_3389/security_group_close_port_3389.py +++ b/remediation_worker/jobs/security_group_close_port_3389/security_group_close_port_3389.py @@ -13,7 +13,6 @@ # limitations under the License. from __future__ import annotations -from botocore.exceptions import ClientError import json import logging @@ -58,7 +57,8 @@ def parse(self, payload): return {"security_group_id": security_group_id}, region def remediate(self, client, security_group_id): - """Block public access to port 3389 for both IPv4 and IPv6. + """Block public access to port 3389 for both IPv4 and IPv6 by + removing all the rules containing port 3389 in the range. :param client: Instance of the AWS boto3 client. :param security_group_id: The ID of the security group. You must specify either the security group ID or the @@ -70,55 +70,40 @@ def remediate(self, client, security_group_id): :raises: botocore.exceptions.ClientError """ - # Revoke ipv4 permission - logging.info("revoking ivp4 permissions") port = 3389 try: - logging.info(" executing client.revoke_security_group_ingress") - logging.info(' CidrIp="0.0.0.0/0"') - logging.info(f" FromPort={port}") - logging.info(f" GroupId={security_group_id}") - logging.info(' IpProtocol="tcp"') - logging.info(f" ToPort={port}") - client.revoke_security_group_ingress( - CidrIp="0.0.0.0/0", - FromPort=port, - GroupId=security_group_id, - IpProtocol="tcp", - ToPort=port, + logging.info(" executing client.describe_security_group_rules") + logging.info(f" group-id: {security_group_id}") + # List all the security group rules + security_group_rules = client.describe_security_group_rules( + Filters=[{"Name": "group-id", "Values": [security_group_id]},], + MaxResults=1000, ) - except ClientError as e: - if "InvalidPermission.NotFound" not in str(e): - logging.error(f"{str(e)}") - raise - - # Revoke ipv6 permission - logging.info("revoking ivp6 permissions") - try: - logging.info(" executing client.revoke_security_group_ingress") - logging.info(f" FromPort={port}") - logging.info(f" GroupId={security_group_id}") - logging.info(' IpProtocol="tcp"') - logging.info(' "Ipv6Ranges": [{"CidrIpv6": "::/0"}]') - logging.info(f" ToPort={port}") - client.revoke_security_group_ingress( - GroupId=security_group_id, - IpPermissions=[ - { - "FromPort": port, - "IpProtocol": "tcp", - "Ipv6Ranges": [{"CidrIpv6": "::/0"}], - "ToPort": port, - }, - ], - ) - except ClientError as e: - if "InvalidPermission.NotFound" not in str(e): - logging.error(f"{str(e)}") - raise - - logging.info("successfully executed remediation") - + for rule in security_group_rules["SecurityGroupRules"]: + if ( + rule["IpProtocol"] == "tcp" + and rule["IsEgress"] is False + and rule["FromPort"] <= port + and rule["ToPort"] >= port + and ( + ("CidrIpv4" in rule and rule["CidrIpv4"] == "0.0.0.0/0") + or ("CidrIpv6" in rule and rule["CidrIpv6"] == "::/0") + ) + ): + # Removes Ingress security group rule containing port 3389 in the range with + # protocol 'tcp', source '0.0.0.0/0' or '::/0' + logging.info(" executing client.revoke_security_group_ingress") + logging.info(f" GroupId: {security_group_id}") + logging.info( + f" SecurityGroupRuleIds: {rule['SecurityGroupRuleId']}" + ) + client.revoke_security_group_ingress( + GroupId=security_group_id, + SecurityGroupRuleIds=[rule["SecurityGroupRuleId"]], + ) + logging.info("successfully executed remediation") + except Exception as e: + logging.error(f"{str(e)}") return 0 def run(self, args): diff --git a/remediation_worker/jobs/security_group_close_port_5432/README.md b/remediation_worker/jobs/security_group_close_port_5432/README.md index f4682b9..ca3afb7 100644 --- a/remediation_worker/jobs/security_group_close_port_5432/README.md +++ b/remediation_worker/jobs/security_group_close_port_5432/README.md @@ -1,6 +1,6 @@ # Close Port 5432 for a Security Group -This job blocks public access to port 5432 for both IPv4 and IPv6. +This job blocks public access to port 5432 for both IPv4 and IPv6 by removing all the ingress security group rules containing port 5432 in the port range and source as "0.0.0.0/0" or "::/0". ### Applicable Rule @@ -14,7 +14,7 @@ A security group's PostgreSQL Server port (5432) is accessible through any sourc ### Prerequisites -The provided AWS credential must have access to `ec2:RevokeSecurityGroupIngress`. +The provided AWS credential must have access to `ec2:RevokeSecurityGroupIngress`, `ec2:DescribeSecurityGroupRules`. You may find the latest example policy file [here](minimum_policy.json) diff --git a/remediation_worker/jobs/security_group_close_port_5432/constraints.txt b/remediation_worker/jobs/security_group_close_port_5432/constraints.txt index 6b211d2..0d9b3c2 100644 --- a/remediation_worker/jobs/security_group_close_port_5432/constraints.txt +++ b/remediation_worker/jobs/security_group_close_port_5432/constraints.txt @@ -8,9 +8,9 @@ jmespath==0.10.0 \ python-dateutil==2.8.1 \ --hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \ --hash=sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a -s3transfer==0.3.3 \ - --hash=sha256:2482b4259524933a022d59da830f51bd746db62f047d6eb213f2f8855dcb8a13 \ - --hash=sha256:921a37e2aefc64145e7b73d50c71bb4f26f46e4c9f414dc648c6245ff92cf7db +s3transfer==0.5.0 \ + --hash=sha256:50ed823e1dc5868ad40c8dc92072f757aa0e653a192845c94a3b676f4a62da4c \ + --hash=sha256:9c1dc369814391a6bda20ebbf4b70a0f34630592c9aa520856bf384916af2803 urllib3==1.25.9 \ --hash=sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527 \ --hash=sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115 diff --git a/remediation_worker/jobs/security_group_close_port_5432/minimum_policy.json b/remediation_worker/jobs/security_group_close_port_5432/minimum_policy.json index 660300e..c6ceba1 100644 --- a/remediation_worker/jobs/security_group_close_port_5432/minimum_policy.json +++ b/remediation_worker/jobs/security_group_close_port_5432/minimum_policy.json @@ -2,10 +2,11 @@ "Version": "2012-10-17", "Statement": [ { - "Sid": "SecurityGroupClosePort5432", + "Sid": "SecurityGroupClosePort22", "Effect": "Allow", "Action": [ - "ec2:RevokeSecurityGroupIngress" + "ec2:RevokeSecurityGroupIngress", + "ec2:DescribeSecurityGroupRules" ], "Resource": "*" } diff --git a/remediation_worker/jobs/security_group_close_port_5432/requirements.txt b/remediation_worker/jobs/security_group_close_port_5432/requirements.txt index b239388..93f7845 100644 --- a/remediation_worker/jobs/security_group_close_port_5432/requirements.txt +++ b/remediation_worker/jobs/security_group_close_port_5432/requirements.txt @@ -1,6 +1,6 @@ -boto3==1.14.9 \ - --hash=sha256:185f7b36c16f76e501d8dfc5cd209113426e078e4968dd13cc355c916bc99597 \ - --hash=sha256:51243ba0e976343ca0b98bb4a15fc3d588526220f6ba45bfed7ea45472b1e033 -botocore==1.17.9 \ - --hash=sha256:7dd59bc766d567ca83bc6113aa139d92ba447738ccdfcd40788848553d329a52 \ - --hash=sha256:cd4bb2d96ff2ec6bf4fbcdb2f241d0fb6ba1e7955b4721cf1d81f13db02768b6 +boto3==1.18.4 \ + --hash=sha256:649ed1ca205f5ee0b0328d54580780aebc1a7a05681a24f6ee05253007ca48d8 \ + --hash=sha256:7079b40bd6621c54a0385a8fc11240cff4318a4d487292653e393e18254f5d94 +botocore==1.21.5 \ + --hash=sha256:0070c5e02b581db40ff5fd1b5e02db90ed88e7e861901894bd78fd998656da68 \ + --hash=sha256:bed34fe7a007180f4208b65515bab1755cdd9fcf2c6720f74ae7ecd2e707f4b7 diff --git a/remediation_worker/jobs/security_group_close_port_5432/security_group_close_port_5432.py b/remediation_worker/jobs/security_group_close_port_5432/security_group_close_port_5432.py index 4a2b11e..acdbfcc 100644 --- a/remediation_worker/jobs/security_group_close_port_5432/security_group_close_port_5432.py +++ b/remediation_worker/jobs/security_group_close_port_5432/security_group_close_port_5432.py @@ -13,7 +13,6 @@ # limitations under the License. from __future__ import annotations -from botocore.exceptions import ClientError import json import logging @@ -58,7 +57,8 @@ def parse(self, payload): return {"security_group_id": security_group_id}, region def remediate(self, client, security_group_id): - """Block public access to port 5432 for both IPv4 and IPv6. + """Block public access to port 5432 for both IPv4 and IPv6 by + removing all the rules containing port 5432 in the range. :param client: Instance of the AWS boto3 client. :param security_group_id: The ID of the security group. You must specify either the security group ID or the @@ -69,56 +69,40 @@ def remediate(self, client, security_group_id): :rtype: int :raises: botocore.exceptions.ClientError """ - - # Revoke ipv4 permission - logging.info("revoking ivp4 permissions") port = 5432 try: - logging.info(" executing client.revoke_security_group_ingress") - logging.info(' CidrIp="0.0.0.0/0"') - logging.info(f" FromPort={port}") - logging.info(f" GroupId={security_group_id}") - logging.info(' IpProtocol="tcp"') - logging.info(f" ToPort={port}") - client.revoke_security_group_ingress( - CidrIp="0.0.0.0/0", - FromPort=port, - GroupId=security_group_id, - IpProtocol="tcp", - ToPort=port, - ) - except ClientError as e: - if "InvalidPermission.NotFound" not in str(e): - logging.error(f"{str(e)}") - raise - - # Revoke ipv6 permission - logging.info("revoking ivp6 permissions") - try: - logging.info(" executing client.revoke_security_group_ingress") - logging.info(f" FromPort={port}") - logging.info(f" GroupId={security_group_id}") - logging.info(' IpProtocol="tcp"') - logging.info(' "Ipv6Ranges": [{"CidrIpv6": "::/0"}]') - logging.info(f" ToPort={port}") - client.revoke_security_group_ingress( - GroupId=security_group_id, - IpPermissions=[ - { - "FromPort": port, - "IpProtocol": "tcp", - "Ipv6Ranges": [{"CidrIpv6": "::/0"}], - "ToPort": port, - }, - ], + logging.info(" executing client.describe_security_group_rules") + logging.info(f" group-id: {security_group_id}") + # List all the security group rules + security_group_rules = client.describe_security_group_rules( + Filters=[{"Name": "group-id", "Values": [security_group_id]},], + MaxResults=1000, ) - except ClientError as e: - if "InvalidPermission.NotFound" not in str(e): - logging.error(f"{str(e)}") - raise - - logging.info("successfully executed remediation") - + for rule in security_group_rules["SecurityGroupRules"]: + if ( + rule["IpProtocol"] == "tcp" + and rule["IsEgress"] is False + and rule["FromPort"] <= port + and rule["ToPort"] >= port + and ( + ("CidrIpv4" in rule and rule["CidrIpv4"] == "0.0.0.0/0") + or ("CidrIpv6" in rule and rule["CidrIpv6"] == "::/0") + ) + ): + # Removes Ingress security group rule containing port 5432 in the range with + # protocol 'tcp', source '0.0.0.0/0' or '::/0' + logging.info(" executing client.revoke_security_group_ingress") + logging.info(f" GroupId: {security_group_id}") + logging.info( + f" SecurityGroupRuleIds: {rule['SecurityGroupRuleId']}" + ) + client.revoke_security_group_ingress( + GroupId=security_group_id, + SecurityGroupRuleIds=[rule["SecurityGroupRuleId"]], + ) + logging.info("successfully executed remediation") + except Exception as e: + logging.error(f"{str(e)}") return 0 def run(self, args): diff --git a/test/unit/test_aws_ec2_default_security_group_traffic.py b/test/unit/test_aws_ec2_default_security_group_traffic.py new file mode 100644 index 0000000..ce451bf --- /dev/null +++ b/test/unit/test_aws_ec2_default_security_group_traffic.py @@ -0,0 +1,62 @@ +# Copyright (c) 2020 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +from mock import Mock + +from remediation_worker.jobs.aws_ec2_default_security_group_traffic.aws_ec2_default_security_group_traffic import ( + DefaultSecurityGroupRemoveRules, +) + + +@pytest.fixture +def valid_payload(): + return """ +{ + "notificationInfo": { + "RuleId": "5c6cc5e103dcc90f363146cd", + "Service": "EC2", + "FindingInfo": { + "FindingId": "d0431afd-b82e-4021-8aa6-ba3cf5c60ef7", + "ObjectId": "security_group_id", + "ObjectChain": "{\\"cloudAccountId\\":\\"cloud_account_id\\",\\"entityId\\":\\"AWS.EC2.159636093902.us-west-2.Trail.test-remediation\\",\\"entityName\\":\\"remediation-cloudtrail\\",\\"entityType\\":\\"AWS.CloudTrail.Trail\\",\\"lastUpdateTime\\":\\"2020-09-09T00:36:35.000Z\\",\\"partitionKey\\":\\"153894897389\\",\\"provider\\":\\"AWS\\",\\"region\\":\\"us-west-2\\",\\"service\\":\\"EC2\\", \\"properties\\":[{\\"name\\":\\"S3BucketName\\",\\"stringV\\":\\"remediation-cloudtrail\\",\\"type\\":\\"string\\"}]}", + "Region": "region" + } + } +} +""" + + +class TestCloudtrailS3PublicAccess(object): + def test_parse_payload(self, valid_payload): + params = DefaultSecurityGroupRemoveRules().parse(valid_payload) + assert params["security_group_id"] == "security_group_id" + assert params["cloud_account_id"] == "cloud_account_id" + assert params["region"] == "region" + + def test_remediate_success(self): + client = Mock() + action = DefaultSecurityGroupRemoveRules() + + assert ( + action.remediate(client, "security_group_id", "region", "cloud_account_id") + == 0 + ) + assert client.describe_security_groups.call_count == 1 + + def test_remediate_with_exception(self): + client = Mock() + action = DefaultSecurityGroupRemoveRules() + with pytest.raises(Exception): + assert action.remediate(client, "cloud_account_id") \ No newline at end of file diff --git a/test/unit/test_aws_iam_password_policy_min_length.py b/test/unit/test_aws_iam_password_policy_min_length.py new file mode 100644 index 0000000..21adbdf --- /dev/null +++ b/test/unit/test_aws_iam_password_policy_min_length.py @@ -0,0 +1,59 @@ +# Copyright (c) 2020 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +from mock import Mock + +from remediation_worker.jobs.aws_iam_password_policy_min_length.aws_iam_password_policy_min_length import ( + SetPasswordMinimumLength, +) + + +@pytest.fixture +def valid_payload(): + return """ +{ + "notificationInfo": { + "RuleId": "5c6cc5e103dcc90f363146cd", + "Service": "IAM", + "FindingInfo": { + "FindingId": "d0431afd-b82e-4021-8aa6-ba3cf5c60ef7", + "ObjectId": "account_id", + "ObjectChain": "{\\"cloudAccountId\\":\\"cloud_account_id\\",\\"entityId\\":\\"AWS.IAM.159026902.us-west-2.Key.key_id\\",\\"entityName\\":\\"key_id\\",\\"entityType\\":\\"AWS.IAM.AccountPasswordPolicy\\",\\"lastUpdateTime\\":\\"2020-09-09T00:36:35.000Z\\",\\"partitionKey\\":\\"156898827089\\",\\"provider\\":\\"AWS\\",\\"region\\":\\"us-west-2\\",\\"service\\":\\"CloudTrail\\", \\"properties\\":[{\\"name\\":\\"KeyState\\",\\"stringV\\":\\"Enabled\\",\\"type\\":\\"string\\"}]}", + "Region": "region" + } + } +} +""" + + +class TestSetPasswordMinLength(object): + def test_parse_payload(self, valid_payload): + params = SetPasswordMinimumLength().parse(valid_payload) + assert params["account_id"] == "account_id" + + def test_remediate_success(self): + client = Mock() + action = SetPasswordMinimumLength() + assert action.remediate(client, "account_id") == 0 + assert client.update_account_password_policy.call_count == 1 + call_args = client.update_account_password_policy.call_args + password_reuse_policy = call_args[1]["MinimumPasswordLength"] + assert password_reuse_policy == 14 + + def test_remediate_with_exception(self): + client = Mock() + action = SetPasswordMinimumLength() + with pytest.raises(Exception): + assert action.remediate(client, "account_id") diff --git a/test/unit/test_aws_iam_password_reuse_prevention.py b/test/unit/test_aws_iam_password_reuse_prevention.py new file mode 100644 index 0000000..2d8484e --- /dev/null +++ b/test/unit/test_aws_iam_password_reuse_prevention.py @@ -0,0 +1,59 @@ +# Copyright (c) 2020 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +from mock import Mock + +from remediation_worker.jobs.aws_iam_password_reuse_prevention.aws_iam_password_reuse_prevention import ( + SetPasswordReusePrevention, +) + + +@pytest.fixture +def valid_payload(): + return """ +{ + "notificationInfo": { + "RuleId": "5c6cc5e103dcc90f363146cd", + "Service": "IAM", + "FindingInfo": { + "FindingId": "d0431afd-b82e-4021-8aa6-ba3cf5c60ef7", + "ObjectId": "account_id", + "ObjectChain": "{\\"cloudAccountId\\":\\"cloud_account_id\\",\\"entityId\\":\\"AWS.IAM.159026902.us-west-2.Key.key_id\\",\\"entityName\\":\\"key_id\\",\\"entityType\\":\\"AWS.IAM.AccountPasswordPolicy\\",\\"lastUpdateTime\\":\\"2020-09-09T00:36:35.000Z\\",\\"partitionKey\\":\\"156898827089\\",\\"provider\\":\\"AWS\\",\\"region\\":\\"us-west-2\\",\\"service\\":\\"CloudTrail\\", \\"properties\\":[{\\"name\\":\\"KeyState\\",\\"stringV\\":\\"Enabled\\",\\"type\\":\\"string\\"}]}", + "Region": "region" + } + } +} +""" + + +class TestSetPasswordReusePolicy(object): + def test_parse_payload(self, valid_payload): + params = SetPasswordReusePrevention().parse(valid_payload) + assert params["account_id"] == "account_id" + + def test_remediate_success(self): + client = Mock() + action = SetPasswordReusePrevention() + assert action.remediate(client, "account_id") == 0 + assert client.update_account_password_policy.call_count == 1 + call_args = client.update_account_password_policy.call_args + password_reuse_policy = call_args[1]["PasswordReusePrevention"] + assert password_reuse_policy == 24 + + def test_remediate_with_exception(self): + client = Mock() + action = SetPasswordReusePrevention() + with pytest.raises(Exception): + assert action.remediate(client, "account_id") diff --git a/test/unit/test_aws_iam_server_certificate_expired.py b/test/unit/test_aws_iam_server_certificate_expired.py new file mode 100644 index 0000000..18e83a2 --- /dev/null +++ b/test/unit/test_aws_iam_server_certificate_expired.py @@ -0,0 +1,55 @@ +# Copyright (c) 2020 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +from mock import Mock + +from remediation_worker.jobs.aws_iam_server_certificate_expired.aws_iam_server_certificate_expired import ( + DeleteExpiredServerCertificate, +) + + +@pytest.fixture +def valid_payload(): + return """ +{ + "notificationInfo": { + "RuleId": "5c6cc5e103dcc90f363146cd", + "Service": "IAM", + "FindingInfo": { + "FindingId": "d0431afd-b82e-4021-8aa6-ba3cf5c60ef7", + "ObjectId": "certificate_name", + "ObjectChain": "{\\"cloudAccountId\\":\\"cloud_account_id\\",\\"entityId\\":\\"AWS.IAM.159026902.us-west-2.Key.key_id\\",\\"entityName\\":\\"key_id\\",\\"entityType\\":\\"AWS.IAM.AccountPasswordPolicy\\",\\"lastUpdateTime\\":\\"2020-09-09T00:36:35.000Z\\",\\"partitionKey\\":\\"156898827089\\",\\"provider\\":\\"AWS\\",\\"region\\":\\"us-west-2\\",\\"service\\":\\"CloudTrail\\", \\"properties\\":[{\\"name\\":\\"KeyState\\",\\"stringV\\":\\"Enabled\\",\\"type\\":\\"string\\"}]}", + "Region": "region" + } + } +} +""" + + +class TestDeleteExpiredServerCertificate(object): + def test_parse_payload(self, valid_payload): + params = DeleteExpiredServerCertificate().parse(valid_payload) + assert params["certificate_name"] == "certificate_name" + + def test_remediate_success(self): + client = Mock() + action = DeleteExpiredServerCertificate() + assert action.remediate(client, "certificate_name") == 0 + + def test_remediate_with_exception(self): + client = Mock() + action = DeleteExpiredServerCertificate() + with pytest.raises(Exception): + assert action.remediate(client, "certificate_name") diff --git a/test/unit/test_aws_s3_cloudtrail_public_access.py b/test/unit/test_aws_s3_cloudtrail_public_access.py index c2b81df..3682a27 100644 --- a/test/unit/test_aws_s3_cloudtrail_public_access.py +++ b/test/unit/test_aws_s3_cloudtrail_public_access.py @@ -17,7 +17,7 @@ from botocore.exceptions import ClientError from remediation_worker.jobs.aws_s3_cloudtrail_public_access.aws_s3_cloudtrail_public_access import ( - CloudtrailS3RemovePublicAcces, + CloudtrailS3RemovePublicAccess, ) @@ -41,7 +41,7 @@ def valid_payload(): class TestCloudtrailS3PublicAccess(object): def test_parse_payload(self, valid_payload): - params = CloudtrailS3RemovePublicAcces().parse(valid_payload) + params = CloudtrailS3RemovePublicAccess().parse(valid_payload) assert params["region"] == "region" assert params["cloudtrail_name"] == "CloudTrail_name" assert params["cloud_account_id"] == "cloud_account_id" @@ -49,7 +49,7 @@ def test_parse_payload(self, valid_payload): def test_remediate_success_with_bucket_policy_public(self): client = Mock() cloudtrail_client = Mock() - action = CloudtrailS3RemovePublicAcces() + action = CloudtrailS3RemovePublicAccess() trail = { "Trail": { "Name": "CloudTrail_name", @@ -126,7 +126,7 @@ def test_remediate_success_with_bucket_policy_public(self): def test_remediate_success_without_bucket_policy_public(self): client = Mock() cloudtrail_client = Mock() - action = CloudtrailS3RemovePublicAcces() + action = CloudtrailS3RemovePublicAccess() trail = { "Trail": { "Name": "CloudTrail_name", @@ -188,6 +188,6 @@ def put_public_access_block(self, **kwargs): ) client = TestClient() - action = CloudtrailS3RemovePublicAcces() + action = CloudtrailS3RemovePublicAccess() with pytest.raises(Exception): assert action.remediate(client, "bucket_name", "cloud_account_id") diff --git a/test/unit/test_azure_key_vault_expiry_date_set_for_all_keys.py b/test/unit/test_azure_key_vault_expiry_date_set_for_all_keys.py index 0ea6c9e..938b7ec 100644 --- a/test/unit/test_azure_key_vault_expiry_date_set_for_all_keys.py +++ b/test/unit/test_azure_key_vault_expiry_date_set_for_all_keys.py @@ -29,7 +29,7 @@ def valid_payload(): "FindingInfo": { "FindingId": "d3bb1d9a-fe52-4458-9935-47183f140e6b", "ObjectId": "key_vault_name.key_name", - "ObjectChain": "{\\"cloudAccountId\\":\\"subscription_id\\",\\"entityId\\":\\"Azure.KeyVault.Key.d687b1a3-9b78-43b1-a17b-7de297fd1fce.integration-tests-postgresql-b3wx.Key.postgresqlwnuuobrlrwngs\\",\\"entityName\\":\\"key_vault_name.key_name\\",\\"entityType\\":\\"Azure.KeyVault.Key\\",\\"lastUpdateTime\\":\\"2020-09-09T00:36:35.000Z\\",\\"partitionKey\\":\\"d687b1a3-9b78-43b1-a17b-7de297fd1fce\\",\\"provider\\":\\"Azure\\",\\"region\\":\\"eastus\\",\\"service\\":\\"PostgreSQL\\", \\"properties\\":[{\\"name\\":\\"ResourceGroup\\",\\"stringV\\":\\"resource_group_name\\",\\"type\\":\\"string\\"}]}", + "ObjectChain": "{\\"cloudAccountId\\":\\"subscription_id\\",\\"entityId\\":\\"Azure.KeyVault.Key.d687b1a3-9b78-43b1-a17b-7de297fd1fce.integration-tests-postgresql-b3wx.Key.postgresqlwnuuobrlrwngs\\",\\"entityName\\":\\"key_vault_name.key_name\\",\\"entityType\\":\\"Azure.KeyVault.Key\\",\\"lastUpdateTime\\":\\"2020-09-09T00:36:35.000Z\\",\\"partitionKey\\":\\"d687b1a3-9b78-43b1-a17b-7de297fd1fce\\",\\"provider\\":\\"Azure\\",\\"region\\":\\"eastus\\",\\"service\\":\\"PostgreSQL\\", \\"properties\\":[{\\"name\\":\\"System:SourceEntityId\\",\\"stringV\\":\\"Azure.KeyVault.8aa70cc7-bf51-4e8c-baa0-368cb78b3c0c.resource_group_name.Vault.rem-testsjehb\\",\\"type\\":\\"string\\"}]}", "Region": "region" } } diff --git a/test/unit/test_azure_key_vault_expiry_date_set_for_all_secrets.py b/test/unit/test_azure_key_vault_expiry_date_set_for_all_secrets.py index c635dd4..5c88cf7 100644 --- a/test/unit/test_azure_key_vault_expiry_date_set_for_all_secrets.py +++ b/test/unit/test_azure_key_vault_expiry_date_set_for_all_secrets.py @@ -29,7 +29,7 @@ def valid_payload(): "FindingInfo": { "FindingId": "d3bb1d9a-fe52-4458-9935-47183f140e6b", "ObjectId": "key_vault_name.secret_name", - "ObjectChain": "{\\"cloudAccountId\\":\\"subscription_id\\",\\"entityId\\":\\"Azure.KeyVault.Secret.d687b1a3-9b78-43b1-a17b-7de297fd1fce.integration-tests-postgresql-b3wx.Key.postgresqlwnuuobrlrwngs\\",\\"entityName\\":\\"key_vault_name.key_name\\",\\"entityType\\":\\"Azure.KeyVault.Secret\\",\\"lastUpdateTime\\":\\"2020-09-09T00:36:35.000Z\\",\\"partitionKey\\":\\"d687b1a3-9b78-43b1-a17b-7de297fd1fce\\",\\"provider\\":\\"Azure\\",\\"region\\":\\"eastus\\",\\"service\\":\\"PostgreSQL\\", \\"properties\\":[{\\"name\\":\\"ResourceGroup\\",\\"stringV\\":\\"resource_group_name\\",\\"type\\":\\"string\\"}]}", + "ObjectChain": "{\\"cloudAccountId\\":\\"subscription_id\\",\\"entityId\\":\\"Azure.KeyVault.Secret.d687b1a3-9b78-43b1-a17b-7de297fd1fce.integration-tests-postgresql-b3wx.Key.postgresqlwnuuobrlrwngs\\",\\"entityName\\":\\"key_vault_name.key_name\\",\\"entityType\\":\\"Azure.KeyVault.Secret\\",\\"lastUpdateTime\\":\\"2020-09-09T00:36:35.000Z\\",\\"partitionKey\\":\\"d687b1a3-9b78-43b1-a17b-7de297fd1fce\\",\\"provider\\":\\"Azure\\",\\"region\\":\\"eastus\\",\\"service\\":\\"PostgreSQL\\", \\"properties\\":[{\\"name\\":\\"System:SourceEntityId\\",\\"stringV\\":\\"Azure.KeyVault.8aa70cc7-bf51-4e8c-baa0-368cb78b3c0c.resource_group_name.Vault.rem-testsjehb\\",\\"type\\":\\"string\\"}]}", "Region": "region" } } diff --git a/test/unit/test_azure_sql_auditing_on_server.py b/test/unit/test_azure_sql_auditing_on_server.py index 05c93f0..0bbe543 100644 --- a/test/unit/test_azure_sql_auditing_on_server.py +++ b/test/unit/test_azure_sql_auditing_on_server.py @@ -77,6 +77,7 @@ def test_remediate_without_stg_without_keyvault(self): action.ensure_identity_assigned = Mock() action.create_role_assignment = Mock() action.create_server_blob_auditing_policy = Mock() + action.check_role_assignment = Mock() identity = Identity( principal_id="139bcf82-e14e-4773-bcf4-1da136674792", @@ -106,6 +107,7 @@ def test_remediate_without_stg_without_keyvault(self): location="eastus", identity=identity, ) + action.check_role_assignment.return_value = True action.check_stg_account.return_value = None action.check_key_vault.return_value = None assert ( @@ -134,7 +136,6 @@ def test_remediate_without_stg_without_keyvault(self): assert action.create_storage_account.call_count == 1 assert action.create_key_vault.call_count == 1 assert action.create_diagnostic_setting.call_count == 1 - assert action.create_role_assignment.call_count == 1 assert action.create_server_blob_auditing_policy.call_count == 1 def test_remediate_without_stg_with_keyvault(self): @@ -159,6 +160,7 @@ def test_remediate_without_stg_with_keyvault(self): action.ensure_identity_assigned = Mock() action.create_role_assignment = Mock() action.create_server_blob_auditing_policy = Mock() + action.check_role_assignment = Mock() identity = Identity( principal_id="139bcf82-e14e-4773-bcf4-1da136674792", @@ -189,6 +191,7 @@ def test_remediate_without_stg_with_keyvault(self): vault_uri="https://stg-keyvault-rem.vault.azure.net", ), ) + action.check_role_assignment.return_value = False assert ( action.remediate( client_id, diff --git a/test/unit/test_ebs_private_snapshot.py b/test/unit/test_ebs_private_snapshot.py new file mode 100644 index 0000000..15d3337 --- /dev/null +++ b/test/unit/test_ebs_private_snapshot.py @@ -0,0 +1,94 @@ +# Copyright (c) 2020 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from remediation_worker.jobs.ebs_private_snapshot.ebs_private_snapshot import EBSPrivateSnapshot + +@pytest.fixture +def valid_payload(): + return """ { + "notificationInfo": { + "FindingInfo": { + "ObjectId": "i-00347a2be30cf1a15", + "Region": "us-east-1" + } + } + } +""" + + +class TestEBSPrivateSnapshot: + def test_parse_payload(self, valid_payload): + obj = EBSPrivateSnapshot() + param = obj.parse(valid_payload) + assert "snapshot_id" in param + assert param["snapshot_id"] == "i-00347a2be30cf1a15" + assert "region" in param + assert param["region"] == "us-east-1" + + def test_remediate_success(self): + class TestClient(object): + def modify_snapshot_attribute(self, **kwargs): + return 0 + + def describe_snapshot_attribute(self, **kwargs): + return ({ + "SnapshotId": "snap-066877671789bd71b", + "CreateVolumePermissions": [ + { + "UserId": "123456789012", + 'Group': 'all' + } + + ] + }) + + client = TestClient() + obj = EBSPrivateSnapshot() + assert obj.remediate(client, "snap-01a972f6209ba8d24", "us-west-2") == 0 + + def test_remediate_not_success(self): + class TestClient(object): + def modify_snapshot_attribute(self, **kwargs): + return 0 + + def describe_snapshot_attribute(self, **kwargs): + return ({}) + + + client = TestClient() + obj = EBSPrivateSnapshot() + assert obj.remediate(client, "snap-01a972f6209ba8d24", "us-west-2") == 1 + + def test_remediate_not_success_exception(self): + class TestClient(object): + + def describe_snapshot_attribute(self, **kwargs): + return ({}) + + def modify_db_instance(self, **kwargs): + raise ClientError( + { + "Error": { + "Code": "InvalidSnapshot", + "Message": "NotFound", + } + }, + "InvalidSnapshot", + ) + + client = TestClient() + obj = EBSPrivateSnapshot() + assert obj.remediate(client, "snap-01a972f6209ba8d24", "us-west-2") == 1 diff --git a/test/unit/test_ec2_close_port_1433.py b/test/unit/test_ec2_close_port_1433.py new file mode 100755 index 0000000..f9143e7 --- /dev/null +++ b/test/unit/test_ec2_close_port_1433.py @@ -0,0 +1,39 @@ +# Copyright (c) 2020 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from remediation_worker.jobs.ec2_close_port_1433.ec2_close_port_1433 import EC2ClosePort1433 + + +@pytest.fixture +def valid_payload(): + return """ +{ + "notificationInfo": { + "FindingInfo": { + "ObjectId": "i-00347a2be30cf1a15", + "Region": "us-east-1" + } + } +} +""" + + +class TestEC2ClosePort1433: + def test_parse_payload(self, valid_payload): + obj = EC2ClosePort1433() + param, region = obj.parse(valid_payload) + assert "instance_id" in param + assert param["instance_id"] == "i-00347a2be30cf1a15" diff --git a/test/unit/test_ec2_close_port_1521.py b/test/unit/test_ec2_close_port_1521.py new file mode 100755 index 0000000..df85458 --- /dev/null +++ b/test/unit/test_ec2_close_port_1521.py @@ -0,0 +1,39 @@ +# Copyright (c) 2020 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from remediation_worker.jobs.ec2_close_port_1521.ec2_close_port_1521 import EC2ClosePort1521 + + +@pytest.fixture +def valid_payload(): + return """ +{ + "notificationInfo": { + "FindingInfo": { + "ObjectId": "i-00347a2be30cf1a15", + "Region": "us-east-1" + } + } +} +""" + + +class TestEC2ClosePort1521: + def test_parse_payload(self, valid_payload): + obj = EC2ClosePort1521() + param, region = obj.parse(valid_payload) + assert "instance_id" in param + assert param["instance_id"] == "i-00347a2be30cf1a15" diff --git a/test/unit/test_ec2_close_port_20.py b/test/unit/test_ec2_close_port_20.py new file mode 100755 index 0000000..9d30462 --- /dev/null +++ b/test/unit/test_ec2_close_port_20.py @@ -0,0 +1,39 @@ +# Copyright (c) 2020 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from remediation_worker.jobs.ec2_close_port_20.ec2_close_port_20 import EC2ClosePort20 + + +@pytest.fixture +def valid_payload(): + return """ +{ + "notificationInfo": { + "FindingInfo": { + "ObjectId": "i-00347a2be30cf1a15", + "Region": "us-east-1" + } + } +} +""" + + +class TestEC2ClosePort20: + def test_parse_payload(self, valid_payload): + obj = EC2ClosePort20() + param, region = obj.parse(valid_payload) + assert "instance_id" in param + assert param["instance_id"] == "i-00347a2be30cf1a15" diff --git a/test/unit/test_ec2_close_port_21.py b/test/unit/test_ec2_close_port_21.py new file mode 100755 index 0000000..b83fb89 --- /dev/null +++ b/test/unit/test_ec2_close_port_21.py @@ -0,0 +1,39 @@ +# Copyright (c) 2020 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from remediation_worker.jobs.ec2_close_port_21.ec2_close_port_21 import EC2ClosePort21 + + +@pytest.fixture +def valid_payload(): + return """ +{ + "notificationInfo": { + "FindingInfo": { + "ObjectId": "i-00347a2be30cf1a15", + "Region": "us-east-1" + } + } +} +""" + + +class TestEC2ClosePort21: + def test_parse_payload(self, valid_payload): + obj = EC2ClosePort21() + param, region = obj.parse(valid_payload) + assert "instance_id" in param + assert param["instance_id"] == "i-00347a2be30cf1a15" diff --git a/test/unit/test_ec2_close_port_23.py b/test/unit/test_ec2_close_port_23.py new file mode 100755 index 0000000..3b86ffd --- /dev/null +++ b/test/unit/test_ec2_close_port_23.py @@ -0,0 +1,39 @@ +# Copyright (c) 2020 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from remediation_worker.jobs.ec2_close_port_23.ec2_close_port_23 import EC2ClosePort23 + + +@pytest.fixture +def valid_payload(): + return """ +{ + "notificationInfo": { + "FindingInfo": { + "ObjectId": "i-00347a2be30cf1a15", + "Region": "us-east-1" + } + } +} +""" + + +class TestEC2ClosePort23: + def test_parse_payload(self, valid_payload): + obj = EC2ClosePort23() + param, region = obj.parse(valid_payload) + assert "instance_id" in param + assert param["instance_id"] == "i-00347a2be30cf1a15" diff --git a/test/unit/test_ec2_close_port_27017.py b/test/unit/test_ec2_close_port_27017.py new file mode 100755 index 0000000..7fae40d --- /dev/null +++ b/test/unit/test_ec2_close_port_27017.py @@ -0,0 +1,39 @@ +# Copyright (c) 2020 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from remediation_worker.jobs.ec2_close_port_27017.ec2_close_port_27017 import EC2ClosePort27017 + + +@pytest.fixture +def valid_payload(): + return """ +{ + "notificationInfo": { + "FindingInfo": { + "ObjectId": "i-00347a2be30cf1a15", + "Region": "us-east-1" + } + } +} +""" + + +class TestEC2ClosePort27017: + def test_parse_payload(self, valid_payload): + obj = EC2ClosePort27017() + param, region = obj.parse(valid_payload) + assert "instance_id" in param + assert param["instance_id"] == "i-00347a2be30cf1a15" diff --git a/test/unit/test_ec2_close_port_3306.py b/test/unit/test_ec2_close_port_3306.py new file mode 100755 index 0000000..680ca02 --- /dev/null +++ b/test/unit/test_ec2_close_port_3306.py @@ -0,0 +1,39 @@ +# Copyright (c) 2020 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from remediation_worker.jobs.ec2_close_port_3306.ec2_close_port_3306 import EC2ClosePort3306 + + +@pytest.fixture +def valid_payload(): + return """ +{ + "notificationInfo": { + "FindingInfo": { + "ObjectId": "i-00347a2be30cf1a15", + "Region": "us-east-1" + } + } +} +""" + + +class TestEC2ClosePort3306: + def test_parse_payload(self, valid_payload): + obj = EC2ClosePort3306() + param, region = obj.parse(valid_payload) + assert "instance_id" in param + assert param["instance_id"] == "i-00347a2be30cf1a15" diff --git a/test/unit/test_ec2_close_port_5439.py b/test/unit/test_ec2_close_port_5439.py new file mode 100755 index 0000000..b531da3 --- /dev/null +++ b/test/unit/test_ec2_close_port_5439.py @@ -0,0 +1,39 @@ +# Copyright (c) 2020 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from remediation_worker.jobs.ec2_close_port_5439.ec2_close_port_5439 import EC2ClosePort5439 + + +@pytest.fixture +def valid_payload(): + return """ +{ + "notificationInfo": { + "FindingInfo": { + "ObjectId": "i-00347a2be30cf1a15", + "Region": "us-east-1" + } + } +} +""" + + +class TestEC2ClosePort5439: + def test_parse_payload(self, valid_payload): + obj = EC2ClosePort5439() + param, region = obj.parse(valid_payload) + assert "instance_id" in param + assert param["instance_id"] == "i-00347a2be30cf1a15" diff --git a/test/unit/test_ec2_close_port_5601.py b/test/unit/test_ec2_close_port_5601.py new file mode 100755 index 0000000..9567cd7 --- /dev/null +++ b/test/unit/test_ec2_close_port_5601.py @@ -0,0 +1,39 @@ +# Copyright (c) 2020 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from remediation_worker.jobs.ec2_close_port_5601.ec2_close_port_5601 import EC2ClosePort5601 + + +@pytest.fixture +def valid_payload(): + return """ +{ + "notificationInfo": { + "FindingInfo": { + "ObjectId": "i-00347a2be30cf1a15", + "Region": "us-east-1" + } + } +} +""" + + +class TestEC2ClosePort5601: + def test_parse_payload(self, valid_payload): + obj = EC2ClosePort5601() + param, region = obj.parse(valid_payload) + assert "instance_id" in param + assert param["instance_id"] == "i-00347a2be30cf1a15" diff --git a/test/unit/test_ec2_close_port_8080.py b/test/unit/test_ec2_close_port_8080.py new file mode 100755 index 0000000..1b75f9e --- /dev/null +++ b/test/unit/test_ec2_close_port_8080.py @@ -0,0 +1,39 @@ +# Copyright (c) 2020 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from remediation_worker.jobs.ec2_close_port_8080.ec2_close_port_8080 import EC2ClosePort8080 + + +@pytest.fixture +def valid_payload(): + return """ +{ + "notificationInfo": { + "FindingInfo": { + "ObjectId": "i-00347a2be30cf1a15", + "Region": "us-east-1" + } + } +} +""" + + +class TestEC2ClosePort8080: + def test_parse_payload(self, valid_payload): + obj = EC2ClosePort8080() + param, region = obj.parse(valid_payload) + assert "instance_id" in param + assert param["instance_id"] == "i-00347a2be30cf1a15" diff --git a/test/unit/test_ec2_close_port_9200_9300.py b/test/unit/test_ec2_close_port_9200_9300.py new file mode 100755 index 0000000..49b61b1 --- /dev/null +++ b/test/unit/test_ec2_close_port_9200_9300.py @@ -0,0 +1,39 @@ +# Copyright (c) 2020 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from remediation_worker.jobs.ec2_close_port_9200_9300.ec2_close_port_9200_9300 import EC2ClosePort9200_9300 + + +@pytest.fixture +def valid_payload(): + return """ +{ + "notificationInfo": { + "FindingInfo": { + "ObjectId": "i-00347a2be30cf1a15", + "Region": "us-east-1" + } + } +} +""" + + +class TestEC2ClosePort9200_9300: + def test_parse_payload(self, valid_payload): + obj = EC2ClosePort9200_9300() + param, region = obj.parse(valid_payload) + assert "instance_id" in param + assert param["instance_id"] == "i-00347a2be30cf1a15" diff --git a/test/unit/test_kinesis_encrypt_stream.py b/test/unit/test_kinesis_encrypt_stream.py new file mode 100644 index 0000000..b8659b7 --- /dev/null +++ b/test/unit/test_kinesis_encrypt_stream.py @@ -0,0 +1,148 @@ +# Copyright (c) 2020 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from remediation_worker.jobs.kinesis_encrypt_stream.kinesis_encrypt_stream import KinesisEncryptStream + +@pytest.fixture +def valid_payload(): + return """ { + "notificationInfo": { + "FindingInfo": { + "ObjectId": "kinesis-stream", + "Region": "us-east-1" + } + } + } +""" + + +class TestKinesisEncryptStream: + def test_parse_payload(self, valid_payload): + obj = KinesisEncryptStream() + param = obj.parse(valid_payload) + assert "stream_name" in param + assert param["stream_name"] == "kinesis-stream" + assert "region" in param + assert param["region"] == "us-east-1" + + def test_remediate_success(self): + class TestClient(object): + def start_stream_encryption(self, **kwargs): + return 0 + + def describe_stream(self, **kwargs): + stream = {'StreamDescription': {'EncryptionType':"NONE"}} + return stream + + client = TestClient() + obj = KinesisEncryptStream() + assert obj.remediate("stream_name", client, "us-west1") == 0 + + def test_remediate_not_success_resource_not_found(self): + class TestClient(object): + def start_stream_encryption(self, **kwargs): + return 0 + + def describe_stream(self, **kwargs): + raise ClientError( + { + "Error": { + "Code": "ResourceNotFoundException", + "Message": "ResourceNotFoundException for reason", + } + }, + "KinesisEncryptStream", + ) + + client = TestClient() + obj = KinesisEncryptStream() + assert obj.remediate("stream_name", client, "us-west1") == 1 + + def test_remediate_not_success_limit_exceed(self): + class TestClient(object): + def start_stream_encryption(self, **kwargs): + return 0 + + def describe_stream(self, **kwargs): + raise ClientError( + { + "Error": { + "Code": "LimitExceededException", + "Message": "LimitExceededException for reason", + } + }, + "KinesisEncryptStream", + ) + + client = TestClient() + obj = KinesisEncryptStream() + assert obj.remediate("stream_name", client, "us-west1") == 1 + + def test_remediate_not_success_resource_in_use(self): + class TestClient(object): + def describe_stream(self, **kwargs): + stream = {'StreamDescription': {'EncryptionType':"NONE"}} + return stream + + def start_stream_encryption(self, **kwargs): + raise ClientError( + { + "Error": { + "Code": "ResourceInUseException", + "Message": "ResourceInUseException for reason", + } + }, + "KinesisEncryptStream", + ) + + client = TestClient() + obj = KinesisEncryptStream() + assert obj.remediate("stream_name", client, "us-west1") == 1 + + def test_remediate_not_success_other_aws_exceptions(self): + class TestClient(object): + def describe_stream(self, **kwargs): + stream = {'StreamDescription': {'EncryptionType':"NONE"}} + return stream + + def start_stream_encryption(self, **kwargs): + raise ClientError( + { + "Error": { + "Code": "InvalidArgumentException", + "Message": "InvalidArgumentException for reason", + } + }, + "KinesisEncryptStream", + ) + + client = TestClient() + obj = KinesisEncryptStream() + assert obj.remediate("stream_name", client, "us-west1") == 1 + + def test_remediate_not_success_other_exceptions(self): + class TestClient(object): + def describe_stream(self, **kwargs): + stream = {'StreamDescription': {'EncryptionType':"NONE"}} + return stream + + def start_stream_encryption(self, **kwargs): + raise Exception('This is the exception you expect to handle') + + client = TestClient() + obj = KinesisEncryptStream() + assert obj.remediate("stream_name", client, "us-west1") == 1 + diff --git a/test/unit/test_rds_enable_version_update.py b/test/unit/test_rds_enable_version_update.py new file mode 100644 index 0000000..01be971 --- /dev/null +++ b/test/unit/test_rds_enable_version_update.py @@ -0,0 +1,76 @@ +# Copyright (c) 2020 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +from botocore.exceptions import ClientError + +from remediation_worker.jobs.rds_enable_version_update.rds_enable_version_update import RDSUpgradMinorVersion + +@pytest.fixture +def valid_payload(): + return """ { + "notificationInfo": { + "FindingInfo": { + "ObjectId": "i-00347a2be30cf1a15", + "Region": "us-east-1" + } + } + } +""" + + +class TestRDSUpgradMinorVersion: + def test_parse_payload(self, valid_payload): + obj = RDSUpgradMinorVersion() + param = obj.parse(valid_payload) + assert "database_id" in param + assert param["database_id"] == "i-00347a2be30cf1a15" + assert "region" in param + assert param["region"] == "us-east-1" + + def test_remediation_success(self, valid_payload): + class TestClient(object): + def modify_db_instance(self, **kwargs): + return 1 + + def describe_db_instances(self, **kwargs): + rds = {'DBInstances':[{'PubliclyAccessible':True}]} + return rds + + client = TestClient() + obj = RDSUpgradMinorVersion() + assert obj.remediate(client, "database-1") == 0 + + def test_remediation_not_success(self, valid_payload): + class TestClient(object): + def modify_db_instance(self, **kwargs): + raise ClientError( + { + "Error": { + "Code": "InvalidDBInstanceState", + "Message": "InvalidDBInstanceState msg", + } + }, + "InvalidDBInstance", + ) + + def describe_db_instances(self, **kwargs): + rds = {'DBInstances':[{'PubliclyAccessible':True}]} + return rds + + + client = TestClient() + obj = RDSUpgradMinorVersion() + assert obj.remediate(client, "database-1") == 1 + diff --git a/test/unit/test_rds_remove_public_endpoint.py b/test/unit/test_rds_remove_public_endpoint.py new file mode 100644 index 0000000..83cccd8 --- /dev/null +++ b/test/unit/test_rds_remove_public_endpoint.py @@ -0,0 +1,78 @@ +# Copyright (c) 2020 VMware Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +#from botocore.exceptions import ClientError + +from remediation_worker.jobs.rds_remove_public_endpoint.rds_remove_public_endpoint import RDSRemovePublicEndpoint + +@pytest.fixture +def valid_payload(): + return """ { + "notificationInfo": { + "FindingInfo": { + "ObjectId": "i-00347a2be30cf1a15", + "Region": "us-east-1" + } + } + } +""" + + +class TestRDSRemovePublicEndpoint: + def test_parse_payload(self, valid_payload): + obj = RDSRemovePublicEndpoint() + param = obj.parse(valid_payload) + assert "instance_id" in param + assert param["instance_id"] == "i-00347a2be30cf1a15" + assert "region" in param + assert param["region"] == "us-east-1" + + + def test_remediation_success(self, valid_payload): + class TestClient(object): + def modify_db_instance(self, **kwargs): + return 1 + + def describe_db_instances(self, **kwargs): + rds = {'DBInstances':[{'PubliclyAccessible':True}]} + return rds + + client = TestClient() + obj = RDSRemovePublicEndpoint() + assert obj.remediate(client, "database-1") == 0 + + def test_remediation_not_success(self, valid_payload): + class TestClient(object): + def modify_db_instance(self, **kwargs): + raise ClientError( + { + "Error": { + "Code": "InvalidDBInstanceState", + "Message": "InvalidDBInstanceState msg", + } + }, + "InvalidDBInstance", + ) + + def describe_db_instances(self, **kwargs): + rds = {'DBInstances':[{'PubliclyAccessible':True}]} + return rds + + + client = TestClient() + obj = RDSRemovePublicEndpoint() + assert obj.remediate(client, "database-1") == 1 + + \ No newline at end of file diff --git a/tox.ini b/tox.ini index 01fe23f..6b5443e 100644 --- a/tox.ini +++ b/tox.ini @@ -2,6 +2,21 @@ minversion = 3.6.0 skip_missing_interpreters = true envlist = + unit-aws-ec2-default-security-group-traffic + unit-aws-iam-password-policy-min-length + unit-aws-iam-password-reuse-prevention + unit-aws-iam-server-certificate-expired + unit-ec2-close-port-5601 + unit-ec2-close-port-5439 + unit-ec2-close-port-3306 + unit-ec2-close-port-27017 + unit-ec2-close-port-23 + unit-ec2-close-port-21 + unit-ec2-close-port-20 + unit-ec2-close-port-1521 + unit-ec2-close-port-1433 + unit-ec2-close-port-8080 + unit-ec2-close-port-8200-9300 unit-security-group-close-port-5432 unit-s3-remove-public-admin-acl unit-s3-enable-access-logging @@ -41,6 +56,11 @@ envlist = unit-azure-postgresql-allow-access-to-azure-service-disabled unit-aws-s3-bucket-policy-allow-https unit-aws-sqs-queue-publicly-accessible + unit-ebs-private-snapshot + unit-rds-remove-public-endpoint + unit-rds_enable_version_update + unit-kinesis-encrypt-stream + [testenv] passenv = @@ -292,4 +312,105 @@ deps = -r remediation_worker/jobs/aws_s3_bucket_policy_allow_https/requirements- description = Unit test the project changedir = test commands = pytest --capture=no --basetemp="{envtmpdir}" unit/test_aws_sqs_queue_publicly_accessible.py -deps = -r remediation_worker/jobs/aws_sqs_queue_publicly_accessible/requirements-dev.txt \ No newline at end of file +deps = -r remediation_worker/jobs/aws_sqs_queue_publicly_accessible/requirements-dev.txt + +[testenv:unit-ebs-private-snapshot] +description = Unit test the project +changedir = test +commands = pytest --capture=no --basetemp="{envtmpdir}" unit/test_ebs_private_snapshot.py +deps = -r remediation_worker/jobs/ebs_private_snapshot/requirements-dev.txt + +[testenv:unit-rds-remove-public-endpoint] +description = Unit test the project +changedir = test +commands = pytest --capture=no --basetemp="{envtmpdir}" unit/test_rds_remove_public_endpoint.py +deps = -r remediation_worker/jobs/rds_remove_public_endpoint/requirements-dev.txt + +[testenv:unit-rds_enable_version_update] +description = Unit test the project +changedir = test +commands = pytest --capture=no --basetemp="{envtmpdir}" unit/test_rds_enable_version_update.py +deps = -r remediation_worker/jobs/rds_enable_version_update/requirements-dev.txt + +[testenv:unit-kinesis-encrypt-stream] +description = Unit test the project +changedir = test +commands = pytest --capture=no --basetemp="{envtmpdir}" unit/test_kinesis_encrypt_stream.py +deps = -r remediation_worker/jobs/kinesis_encrypt_stream/requirements-dev.txt + + +[testenv:unit-ec2-close-port-5601] +description = Unit test the project +changedir = test +commands = pytest --capture=no --basetemp="{envtmpdir}" unit/test_ec2_close_port_5601.py +deps = -r remediation_worker/jobs/ec2_close_port_3389/requirements-dev.txt + +[testenv:unit-ec2-close-port-5439] +commands = pytest --capture=no --basetemp="{envtmpdir}" unit/test_ec2_close_port_5439.py +changedir = test +deps = -r remediation_worker/jobs/ec2_close_port_5439/requirements-dev.txt + +[testenv:unit-ec2-close-port-3306] +changedir = test +commands = pytest --capture=no --basetemp="{envtmpdir}" unit/test_ec2_close_port_3306.py +deps = -r remediation_worker/jobs/ec2_close_port_3306/requirements-dev.txt + +[testenv:unit-ec2-close-port-27017] +changedir = test +commands = pytest --capture=no --basetemp="{envtmpdir}" unit/test_ec2_close_port_27017.py +deps = -r remediation_worker/jobs/ec2_close_port_27017/requirements-dev.txt + +[testenv:unit-ec2-close-port-23] +changedir = test +commands = pytest --capture=no --basetemp="{envtmpdir}" unit/test_ec2_close_port_23.py +deps = -r remediation_worker/jobs/ec2_close_port_23/requirements-dev.txt + +[testenv:unit-ec2-close-port-21] +changedir = test +commands = pytest --capture=no --basetemp="{envtmpdir}" unit/test_ec2_close_port_21.py +deps = -r remediation_worker/jobs/ec2_close_port_21/requirements-dev.txt + +[testenv:unit-ec2-close-port-20] +changedir = test +commands = pytest --capture=no --basetemp="{envtmpdir}" unit/test_ec2_close_port_20.py +deps = -r remediation_worker/jobs/ec2_close_port_20/requirements-dev.txt + +[testenv:unit-ec2-close-port-1521] +changedir = test +commands = pytest --capture=no --basetemp="{envtmpdir}" unit/test_ec2_close_port_1521.py +deps = -r remediation_worker/jobs/ec2_close_port_1521/requirements-dev.txt + +[testenv:unit-ec2-close-port-1433] +changedir = test +commands = pytest --capture=no --basetemp="{envtmpdir}" unit/test_ec2_close_port_1433.py +deps = -r remediation_worker/jobs/ec2_close_port_1433/requirements-dev.txt + +[testenv:unit-ec2-close-port-9200-9300] +changedir = test +pytest --capture=no --basetemp="{envtmpdir}" unit/test_ec2_close_port_9200_9300.py +deps = -r remediation_worker/jobs/ec2_close_port_9200_9300/requirements-dev.txt + +[testenv:unit-ec2-close-port-8080] +changedir = test +pytest --capture=no --basetemp="{envtmpdir}" unit/test_ec2_close_port_8080.py +deps = -r remediation_worker/jobs/ec2_close_port_8080/requirements-dev.txt + +[testenv:unit-aws-ec2-default-security-group-traffic] +changedir = test +pytest --capture=no --basetemp="{envtmpdir}" unit/test_aws_ec2_default_security_group_traffic.py +deps = -r remediation_worker/jobs/aws_ec2_default_security_group_traffic/requirements-dev.txt + +[testenv:unit-aws-iam-password-policy-min-length] +changedir = test +pytest --capture=no --basetemp="{envtmpdir}" unit/test_aws_iam_password_policy_min_length.py +deps = -r remediation_worker/jobs/aws_iam_password_policy_min_length/requirements-dev.txt + +[testenv:unit-aws-iam-password-reuse-prevention] +changedir = test +pytest --capture=no --basetemp="{envtmpdir}" unit/test_aws_iam_password_reuse_prevention.py +deps = -r remediation_worker/jobs/aws_iam_password_reuse_prevention/requirements-dev.txt + +[testenv:unit-aws-iam-server-certificate-expired] +changedir = test +pytest --capture=no --basetemp="{envtmpdir}" unit/test_aws_iam_server_certificate_expired.py +deps = -r remediation_worker/jobs/aws_iam_server_certificate_expired/requirements-dev.txt