Extending unSkript Actions: Quick Customizations of ‘Off the Shelf’ Code
unSkript has hundreds of built-in Actions that allow you to quickly build integrated Runbooks with popular Cloud providers, databases, and more. But sometimes, the supplied Actions are not exactly what you are looking for. Luckily, since each Action is written in Python, it is easy to extend and customize any existing Action to exactly suit your needs. In this post, we’ll walk through an example — with a couple of different approaches to modify the Action.
Public EC2 instances
AWS EC2 virtual machines can be used for a variety of purposes inside an organization. There may be some EC2 instances that are public (hosting your website, for example), but others may be running business critical functions that should be firewalled away from the public.
Accidentally misconfiguring a private EC2 instance as public facing exposes your company to hacking and potential security breaches, so from a security perspective it is imperative to constantly monitor and ensure that your code is not exposed on the public internet. The unSkript Action “ List Public EC2 Instances “ can help you identify any public EC2 instances.
The Action is easy to configure — requiring just AWS Credentials and an AWS region where your EC2 instances are running. Running this Action gives a result with the public DNS and IP of all the public EC2 instances in that region:
This is great. With a little work, you may discover that some of these need to be made private, and you can update the Security Group as needed. But, there may be a few instances that need to remain public (for your website, or a specific SAAS offering). How do you identify and remove those from the report to prevent false positives?
Let’s look at the code
Here is the code in the Action that gets all of the EC2 instances, and decides whether they are public or not:
ec2Client = handle.client('ec2', region_name=region) res = aws_get_paginator(ec2Client, "describe_instances", "Reservations") result={} # Iterate through the list of instances for reservation in res: for instance in reservation['Instances']: #print("instance",instance) instance_id = instance['InstanceId'] public_DNS = instance['PublicDnsName'] if len(public_DNS)>0 and instance_id != "i-0684a4244445eb47c": public_ip = instance['PublicIpAddress'] result[instance_id] = {"public DNS": public_DNS,"public IP":public_ip} return(result)
The first 2 lines define the query to AWS, specifying that the query is related to EC2 and identifying the region. We’ll iterate through all the of the “instances” in the response to determine which of the instances are public. These will be placed into the result JSON which is returned by the Action.
To identify a public EC2 instance, we look for the presence of the parameter instance[‘PublicDnsName’]. If there is no PublicDNS Name, we’re good — the instance is private. If the length of the PublicDNSName is >0, then the instance is public, and we can append the result JSON Dictionary with the information about the instance.
So how could we extend this? If we are aware of public instances (that are supposed to be public), we wouldn’t want this Action reporting those values. How might we hide these from the results?
Hardcoding
What if there are only one or 2 EC2 instances that should be public? You might decide to just hardcode these out of your listing:
if len(public_DNS)>0 and instance_id != "i-0684a4244445eb47c": public_ip = instance['PublicIpAddress'] result[instance_id] = {"public DNS": public_DNS,"public IP":public_ip}
In this example, we’ve explicitly added one EC2 instance to ignore. Of there is more than one, we could build a List of the instance_ids to ignore:
publicList = ["i-0684a4244445eb47c",'i-0b21f192adb417251'] <snip> if len(public_DNS)>0 and instance_id not in publicList: public_ip = instance['PublicIpAddress'] result[instance_id] = {"public DNS": public_DNS,"public IP":public_ip}
So, with just a few minor edits to the pre-built Action, we can refine the list by exempting known public EC2 instances from the report.
But, hard coding values has its own disadvantages — changes require dipping into the code whenever a new public instance needs to be added. What if we could make the list of public instances to ignore a parameter to the xRunBook?
Soft-coding
With a small configuration change, we could define a xRunBook or Action input parameter to provide the publicList of instances for our Action. By specifying the list as an input, we can configure this list at runtime (or if there are no changes, we can rely on the default value of the parameter).
By Clicking Parameters at the top of our xRunBook, we can add a parameter (Click + Add Parameter) called publicList:
Clicking the edit button on publicList allows a bit better visibility:
We’ve now built a default list of public EC2 instances to ignore, but an updated list can be easily provided at runtime (or even programmatically). We no longer need to define the publicList in the code, so our changes now appear as:
#publicLIst is now a parameter, so this line is no longer required #publicList = ["i-0684a4244445eb47c",'i-0b21f192adb417251'] <snip> if len(public_DNS)>0 and instance_id not in publicList: public_ip = instance['PublicIpAddress'] result[instance_id] = {"public DNS": public_DNS,"public IP":public_ip}
Filter by longevity
Perhaps we are only concerned about new EC2 instances. We can assume that the legacy public instances are ok, and we just want to identify any that have been recently deployed and set to public.
The API response from AWS for each instance includes the LaunchTime timestamp for each EC2 instance. We can filter the response to show only NEW public EC2 instances — in the example below — in the last 24 hours:
yesterday = datetime.now(pytz.utc) - timedelta(hours=24) <snip> if len(public_DNS)>0 and instance['LaunchTime'] > yesterday: public_ip = instance['PublicIpAddress'] result[instance_id] = {"public DNS": public_DNS,"public IP":public_ip}
All of the public EC2 instances in the region queried are over a day old, so the result is {}.
Prebuilt, but modular
An advantage of using unSkript is that you can begin building RunBooks immediately by utilizing our library of pre-built Actions. But, if the available Action does not 100% fit your needs, it can be easily modified with Python to exactly match the outcome that you require.
The ability to quickly build, modify and configure with “off the shelf” Actions is one of the reasons unSkript’s users find such value in our xRunBook engine. Want to give it a spin? There’s a Docker install ( instructions on GitHub), or give the free trial of our SAAS product a spin. We also love contributions, so if you have modified an Action (or created a new one!) we’d love a Pull request. If you have any questions — join the Slack Community and we’ll be happy to help.
Originally published at https://unskript.com on January 5, 2023.