Building Custom Scenarios with CNAPPgoat
You can now construct and import your own vulnerability scenarios into CNAPPgoat, enhancing your cloud security skills.
Recap: What is CNAPPgoat?
CNAPPgoat is Tenable's open-source contribution to the multicloud environment landscape. It is a vulnerable-by-design deployment tool tailored for defenders and pentesters. By allowing intentional vulnerabilities to be seeded across AWS, Azure and GCP, we aim to provide a realistic setting for practitioners to enhance their skills.
The process: Crafting your unique scenario
Tenable Cloud Security now offers the community the autonomy of crafting your own CNAPPgoat scenarios. With the new feature rollout, you can construct and import your own vulnerability scenarios into CNAPPgoat. Here's a guide to help you get started:
Crafting your scenario using Pulumi
CNAPPgoat is built with Pulumi, an open-source infrastructure-as-code (IaC) platform that lets developers use familiar programming languages to define and deploy cloud infrastructure. It works with multiple cloud providers and provides a simple programming model.
This means you can write scenarios in your preferred language - be it Go, Python or YAML. Later in the blog we’ll cover how to write a scenario; for now, we’ll just mention that an event scenario comprises a Pulumi program (written in the programming language of your choice) and a project file (written in YAML).
Importing your scenario
When your scenario is ready, use the following command:
cnappgoat maintenance import-scenarios --directory path
This command integrates your handcrafted scenarios into the CNAPPgoat environment. It does so by recursively scanning the directory provided and updating the local .cnappgoat folder with the scenarios.
Afterward, you can use it like any other scenario, with `list`, `provision`, `destroy`, and so on.
Understand the structure
For effective scenario creation, let's dive into the crucial components:
- Pulumi.yaml: This is your scenario's blueprint, which holds all the parameters, attributes and essential details.
- Pulumi Program: Here's where the action lies. This segment contains your scenario's executable components, shaped by your chosen language. While Go is currently prominent, you're free to harness any supported language.
Modifying an existing scenario
Before we build our fully-fledged scenario, we can take a dip in shallower waters by modifying an existing scenario. Let’s look at the scenario `cspm-aws-ec2-open-public`. This scenario creates an EC2 instance open to the internet.
Let’s say we want to create a scenario that instead creates an instance open to only our IP. Let’s look at main.go (the scenario is written in Go). It’s not the shortest file, so I’m going to highlight just the part that creates the security group:
// Create a new security group
securityGroup, err := ec2.NewSecurityGroup(ctx, "CNAPPgoat-ec2-open-public-securitygroup", &ec2.SecurityGroupArgs{
VpcId: vpc.ID(),
Ingress: ec2.SecurityGroupIngressArray{
ec2.SecurityGroupIngressArgs{
Protocol: pulumi.String("tcp"),
FromPort: pulumi.Int(80),
ToPort: pulumi.Int(80),
CidrBlocks: pulumi.StringArray{
pulumi.String("0.0.0.0/0"),
},
},
},
})
if err != nil {
return err
}
Instead of 0.0.0.0/0, we’d like to have it open to only our IP. We could receive that IP via a config parameter but for now let’s keep things simple and just hardcode it.
So the changes we’d like are:
- Add a constant with our IP
- Use that constant in the security group object
- Change resource names accordingly
const MY_IP_CIDR = "1.2.3.4/32"
func main() {
...
...
// Create a new security group
securityGroup, err := ec2.NewSecurityGroup(ctx, "CNAPPgoat-ec2-my-ip-securitygroup", &ec2.SecurityGroupArgs{
VpcId: vpc.ID(),
Ingress: ec2.SecurityGroupIngressArray{
ec2.SecurityGroupIngressArgs{
Protocol: pulumi.String("tcp"),
FromPort: pulumi.Int(80),
ToPort: pulumi.Int(80),
CidrBlocks: pulumi.StringArray{
pulumi.String(MY_IP_CIDR),
},
},
},
})
if err != nil {
return err
}
We’ll also change all the other resource names: the instance, VPC and subnet.
Next, we’d like to modify the Pulumi.yaml file.
Let’s look at the original file:
name: cspm-aws-ec2-open-public
runtime: go
description: This script establishes an AWS EC2 instance running a public web server
on port 80, It exposes the server to any incoming traffic. To fix this issue, restrict
CIDR range in the security group to known IPs, enhancing security.
cnappgoat-params:
description: The provided scenario establishes a new Amazon EC2 instance to host a public webserver on port 80.
It enables public access to port 80, which is a security risk. To fix this issue, revise the security group settings, limiting access
to known and trusted IP addresses.
friendlyName: EC2 Open Public
id: cspm-aws-ec2-open-public
module: cspm
scenarioType: native
platform: aws
We modify it to this:
name: cspm-aws-ec2-open-to-my-ip
runtime: go
description: This script establishes an AWS EC2 instance running a public web server
on port 80, exposed to incoming traffic from one hard-coded IP. This isn’t a security risk.
cnappgoat-params:
description: This script establishes an AWS EC2 instance running a public web server
on port 80, exposed to incoming traffic from one hard-coded IP. This isn’t a security risk.
friendlyName: EC2 Open to My IP
id: cspm-aws-ec2-open-to-my-ip
module: cspm
scenarioType: user
platform: aws
What did we change?
- Scenario name
- Scenario ID
- Friendly name
- scenarioType was changed to user, because we are not building it as a native scenario to be contributed to the main cnappgoat-scenarios repository
And that’s it! The scenario is ready to be deployed and used.
Building a new scenario from scratch
Now let’s try building a scenario from scratch. In this case, let’s use the YAML specification. This might be more convenient for those accustomed to declarative configuration languages such as HCL (Hashicorp Configuration Language).
The YAML specifications aren’t always easy to guess, so consult the Pulumi YAML reference and API documentation. Every Pulumi resource has documentation for each of Pulumi’s runtimes including YAML. For example, here’s the one for GCP Storage Buckets.
When using YAML for Pulumi, both the actual program and the project file are the same file: Pulumi.yaml
name: cspm-gcp-public-storage-bucket
runtime: yaml
description: The scenario deploys a script that creates a GCP storage bucket with public
read access. To remediate this, modify the bucket policy
to restrict public access.
cnappgoat-params:
description: This scenario involves deploying a public GCP storage bucket code creates a
bucket and sets it to public, making the contained secrets easily accessible, which is a significant security
risk.
friendlyName: Public Storage Bucket
id: cspm-gcp-public-storage-bucket
module: cspm
scenarioType: user
platform: gcp
resources:
cnappgoatpublicbucket:
type: gcp:storage:Bucket
properties:
location: US
# Now, set an ACL to make it publicly readable
publicrule:
type: gcp:storage:BucketAccessControl
properties:
bucket: ${cnappgoatpublicbucket.name} # Refers to the bucket defined above
role: READER # Grants read access
entity: allUsers # Makes it public
outputs:
bucketName: ${cnappgoatpublicbucket.name}
bucketUrl: gs://${cnappgoatpublicbucket.name}
We can see two new sections in this YAML file:
Resources:
The resources section tells Pulumi what infrastructure components to deploy and their configurations.
- CnappgoatPublicBucket:
- A Google Cloud Storage bucket.
- The location: US specifies it's stored in the US region.
- publicrule:
- type: Defines the Access Control List (ACL) rule for the bucket.
- Properties:
- bucket: ${cnappgoatpublicbucket.name}: This refers to the bucket we defined previously,cnappgoatpublicbucket.
- role: READER: This grants read-only access.
- entity: allUsers: This makes the bucket publicly readable, allowing any user on the internet to access its contents.
Outputs:
The outputs section defines the information to be returned post-deployment.
- bucketName: Returns the created bucket's name.
- bucketUrl: Provides the direct URL to access the bucket, in the gs://[BUCKET_NAME] format.
The only thing left to do is import these into cnappgoat with the command `cnappgoat maintenance import-scenarios --directory path`, and we’re good to go.
Contributing to the CNAPPgoat scenarios repository
Have you written a scenario which could be useful to others as well? We’d love to add it to the CNAPPgoat scenarios repository. We only ask that you adhere to a few best practices.
General best practices
- Pulumi Programs: Our scenario files align with standard Pulumi programs. For details, refer to the Pulumi documentation.
- Resource Naming: Begin resource names with CNAPPgoat followed by, for clarity, a description.
- Tags: Try to tag resources with {"Cnappgoat": "true"}. Note: tags can be case-sensitive.
- Output Values: Output value that a user or scenario using the scenario would need to know.
- Region Independence: Make scenarios deployable across regions. Check scenarios/cwpp/aws/end-of-life-ec2 for an example.
Testing your contributions
Import and test your scenario:
cnappgoat maintenance import-scenarios --directory path
cnappgoat provision --debug <module>-<platform>-<scenario-name>
Containers and images guidelines
We prioritize security, using only trusted or vetted images/containers. If you want to deploy a custom container, contact the project team, and we’ll discuss how to upload it to the trusted repository.
Contact us
Have questions or need clarification? Contact us here.
Contribute and play a key role in CNAPPgoat's growth!
Related Articles
- Cloud
- Cloud