Rusty SecurityHub Reports

Rusty SecurityHub Reports

By Antonio Masotti

It’s 8 am on a Monday, and you need to ensure your AWS environment remains compliant with your security policies.

Are all S3 buckets blocked from public access?

Are there EC2 instances with excessive open ports?

Are your certificates expiring soon?

… and many … too many … other questions, even before your first coffee :-D

Automating these security checks and having a comfortable report in your emails whenever you want to is essential for managing cloud infrastructure, especially when regulatory requirements demand constant vigilance. In this article, I explore a robust solution to leverage AWS Security Hub to generate automated security reports on a scheduled basis using a Lambda function written in Rust.

The idea is based on a quite old AWS Solution that utilized two Lambda functions in Node.js (Node12, RIP) and Python. However, the original setup was cumbersome and outdated. With Rust, we can streamline this process, offering a more efficient and elegant solution. Let’s see what we can do ;)

How the solution works

The old AWS solution had also an architectural diagram, which is still valid in the current implementation:

Solution Diagram

Originally posted on the AWS Blog in Feb 2021

Let’s break it into smaller pieces:

  1. AWS Security Hub: Continuously scans your AWS accounts for security vulnerabilities and deviations from best practices. For this solution we can use both standard insights and custom insights (which I provisioned separately with Terraform, let me know if you’re interested in seeing how to do that).
  2. Event (Time-based): A CloudWatch event is scheduled to trigger the Lambda function periodically. How often? That depends on you and how much are you willing to pay for it.
  3. Lambda Function: This Rust-written function retrieves the findings from AWS Security Hub, parses them and builds the payload for the SNS topic, that can then be distributed via Mail or using other SNS destinations.
  4. Amazon SNS
  5. Email Notification: Subscribers to the SNS topic receive an email notification with the details of the findings, but as said above, you can also choose other forms of subscriptions to SNS.

The code

Let’s dive into the Rust Lambda function. Since November 2023, the AWS SDK for Rust has reached stability and is ready for production use. You can find the complete code and deployment instructions on Github.

Here’s a glimpse of the Lambda function. For the full implementation visti the repo and the AWS Docs to know more about how to work with Rust in AWS.

use lambda_runtime::{run, service_fn, Error};
use log::LevelFilter;
use crate::utils::init_logger;
// Other deps



/// The entry point for the lambda
/// Initializes the logger and then calls the handler with the main 
/// processing function
#[tokio::main]
async fn main() -> Result<(), Error> {
    init_logger(LevelFilter::Debug);
    run(service_fn(function_handler)).await
}

pub async fn function_handler(event: LambdaEvent<Request>) -> Result<Response, Error> {
    // Get the environment variables
    let env_vars = get_env_vars();

    let sns_topic_arn = env_vars.sns_topic_arn;
    let insight_arns = env_vars.insight_arns;
    let region = env_vars.region;

    // Init AWS clients
    let config = aws_config::load_defaults(BehaviorVersion::v2024_03_28()).await;
    let sec_hub_client = SecurityHubClient::new(&config);
    let sns_client = SnsClient::new(&config);

    // Initialize the SNS message body
    let mut sns_body = init_report_header().await;

    // Basically a for-loop that gets the insights from SecHub
    // and updates the sns_body string, which will then be passed to SNS
    process_insights(insight_arns, region, &sec_hub_client, &mut sns_body).await?;

    finalize_report(&mut sns_body).await;
    log::trace!("SNS Body: {}", sns_body);

    // Send sns_body to SNS, and enjoy :)
    let resp = push_report_to_sns(event, sns_topic_arn, sns_client, sns_body).await?;
    Ok(resp)
}

A note about Cargo Lambda

In this project I’ve used cargo lambda, an amazing and powerful tool in the Rust ecosystem that simplifies the process of writing, testing, and deploying AWS Lambda functions.

With Cargo Lambda, developers can: create, run and test Lambda functions locally, ensuring a great development and debugging experience.

# Initialize the project
cargo lambda new my-project && cd my-project

# Starts a local emulator
cargo lambda watch 

# Simulate invocation
cargo lambda invoke --data-ascii "{ \"command\": \"hi\" }" 

It alsostreamlines the build and deployment process, allowing you to package your Rust code into a deployable Lambda bundle with minimal effort.

Build and manually deploy:

# Outputs the binary already packaged in a zip file
cargo lambda build --output-format zip 

# Deploy with the AWS CLI
aws lambda create-function --function-name my-project \
     --runtime provided.al2023 \
     --role arn:aws:iam::111122223333:role/lambda-role \
     --handler rust.handler \
     --zip-file fileb://target/lambda/my-function/bootstrap.zip

Notice here the runtime, provided.al2023, which is a simple Amazon Linux (al, version 2023) runtime with minimal dependencies.

Or let cargo build and deploy for you ;)

cargo lambda deploy --tags organization=aws,team=devOps my-project

Isn’t it great? Have a look at the Cargo Lambda documentation for much more options that are available to us, including how to handle multiple profiles, environment variables, tags, IAM roles and much more!

Why Rust

Rust is rapidly gaining momentum, consistently ranking as one of the most loved languages in recent surveys. Its popularity spans frontend, MLOps, Data Engineers and system programming of course; Here’s why it’s an excellent choice:

  • Safety and Performance: The two main selling points for Rust. Its ownership and borrowing model eliminates the need for a garbage collector and prevents common bugs like null pointer dereference and buffer overflows while achieving C-like performance. This performance translates to quicker execution times, lower latency, and reduced costs for AWS Lambda functions.

  • Energy efficiency: Maybe a surprising argument, but I think important nonetheless. In a recent comprehensive study comparing the energy efficiency of programming languages, Rust was shown to outperforme many other languages. By improving energy efficiency, organizations can significantly reduce their ecological footprint, an increasingly critical consideration in our climate-aware world.

  • Much more compact runtimes: One of the lesser-discussed yet significant advantages of Rust in the serverless environment is its compact runtime needs. Rust compiles to a single, small binary (the one discussed here is ~15MB in size), reducing the complexity and overhead associated with managing Lambda runtimes. This eliminates the hassle of runtime maintenance such as updates and dependency management. Rust functions can be deployed with just the minimal Amazon Linux runtime, even supporting the AWS Graviton 2 architecture, which uses the arm64 processor.

Conclusions

By choosing Rust for AWS Lambda functions, we leverage the language’s robust safety features and performance. This setup but also simplifies the monitoring and notification process, making it a valuable asset for any AWS cloud infrastructure.

For those interested in implementing this solution, the complete code and deployment instructions are available on GitHub at this link.