It’s a new beginning! Ermetic is now Tenable Cloud Security.

Building Custom Scenarios with CNAPPgoat

You can now construct and import your own vulnerability scenarios into CNAPPgoat, enhancing your cloud security skills

Noam Dahan By Noam Dahan
Building Custom Scenarios with CNAPPgoat

Recap: What is CNAPPgoat?

CNAPPgoat is Ermetic’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

Ermetic 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 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) 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`, etc.

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? Email us at: [email protected].

Contribute and play a key role in CNAPPgoat's growth!

Skip to content