From f77d44668240591c29cd35b7ce5b369382734cf7 Mon Sep 17 00:00:00 2001
From: Gabriel Soltz <8935378+gabrielsoltz@users.noreply.github.com>
Date: Sun, 17 Dec 2023 16:22:15 +0100
Subject: [PATCH] Documentation and Code Improvements (#78)
* readme
* readme
* readme
* readme
* readme
* readme
* readme
* readme
* readme
* readme
* readme
* readme
* readme
* readme
* readme
* readme
* readme
* readme
* README
* readme
* readme
* readme
* readme
* readme
* readme
* readme
* readme
* readme
* readme
* readme
* readme
* readme
* readme
* readme
* readme
* readme
* readme
* readme
* readme
* readme
* readme
* readme
* docs
---
README.md | 1026 +++++++++++++++++++++++++++++-----------
docs/context.md | 225 ++++-----
docs/imgs/impact.png | Bin 0 -> 67019 bytes
lib/actions.py | 97 ++++
lib/config/impact.yaml | 21 +-
lib/findings.py | 86 +---
lib/helpers.py | 213 ++++++---
lib/impact/impact.py | 26 +-
lib/main.py | 361 +++-----------
lib/outputs.py | 592 +++++++++++++----------
lib/securityhub.py | 12 +
11 files changed, 1546 insertions(+), 1113 deletions(-)
create mode 100644 docs/imgs/impact.png
create mode 100644 lib/actions.py
diff --git a/README.md b/README.md
index b0652a3..467069d 100644
--- a/README.md
+++ b/README.md
@@ -13,11 +13,11 @@
# Table of Contents
- [Description](#description)
+- [Quick Run](#quick-run)
- [Context](#context)
- [Impact](#impact)
-- [Architecture](#architecture)
+- [High Level Architecture](#high-level-architecture)
- [Use Cases](#use-cases)
-- [Features](#features)
- [Configuration](#customizing-configuration)
- [Run with Python](#run-with-python)
- [Run with Docker](#run-with-docker)
@@ -27,67 +27,383 @@
- [Configuring Security Hub](#configuring-security-hub)
- [Configuring Context](#configuring-context)
- [Inputs](#Inputs)
-- [Output Modes](#output-modes)
+- [Output](#outputs)
- [Filters](#filters)
-- [Updating Workflow Status](#updating-workflow-status)
-- [Enriching Findings](#enriching-findings)
-- [Findings Aggregation](#findings-aggregation)
+- [Security Hub Actions](#security-hub-actions)
+- [Contributing](#contributing)
# Description
-**MetaHub** is an open-source security tool for **impact-contextual vulnerability management**. It can automate the process of **contextualizing** security findings based on your environment and your needs: YOUR **context**, identifying **ownership**, and calculate an **impact scoring** based on it that you can use for defining prioritization and automation. You can use it with [AWS Security Hub](https://aws.amazon.com/security-hub) or any [ASFF](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-findings-format.html) security scanners (like [Prowler](https://github.com/prowler-cloud/prowler)).
+**MetaHub** is an open-source security tool for **impact-contextual vulnerability management**. It can automate the process of **contextualizing** security findings based on your environment and your needs, YOUR **context**, identifying **ownership**, and calculate an **impact scoring** based on it that you can use for defining prioritization (where should you start?) and automations like remediations, alerts or tickets. The tool is for AWS environments and you can use it with [AWS Security Hub](https://aws.amazon.com/security-hub) or any [ASFF](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-findings-format.html) compatible scanners (like [Prowler](https://github.com/prowler-cloud/prowler)).
-**MetaHub** describe your context by connecting to your affected resources in your affected accounts. It can describe information about your AWS account and organization, the affected resources tags, the affected CloudTrail events, your affected resource configurations, and all their associations: if you are contextualizing a security finding affecting an EC2 Instance, MetaHub will not only connect to that instance itself but also its IAM Roles; from there, it will connect to the IAM Policies associated with those roles. It will connect to the Security Groups and analyze all their rules, the VPC and the Subnets where the instance is running, the Volumes, the Auto Scaling Groups, and more.
+**MetaHub** describes your [**context**](#context) by connecting to your affected resources in your affected accounts. It can describe information about your AWS account and organization, the affected resources tags, related CloudTrail events, your affected resource configurations **and all their associations**: if you are contextualizing a security finding affecting an EC2 Instance, **MetaHub** will not only connect to that instance itself but also its IAM Roles; from there, it will connect to the IAM Policies associated with those roles. It will connect to the Security Groups and analyze all their rules, the VPC and the Subnets where the instance is running, the Volumes, the Auto Scaling Groups, and more. You can apply [**filters**](#filters) to automate detecting other resources with the same properties and do in-depth investigations.
-After fetching all the information from your context, **MetaHub** will evaluate certain important conditions for all your resources: `exposure`, `access`, `encryption`, `status`, `environment`, `application` and `owner`. Based on those calculations and in addition to the information from the security findings affecting the resource all together, MetaHub will generate a **Scoring** for each finding.
+After fetching all the information from your **context**, **MetaHub** will evaluate the [**impact**](#impact) conditions for all your resources: [**exposure**](#exposure), [**access**](#access), [**encryption**](#encryption), [**status**](#status), [**environment**](#environment), [**application**](#application), and [**owner**](#owner) and based on those calculations and in addition to the information about the security [**findings**](#findings) affecting the resource all **together**, **MetaHub** will generate a **score** for each finding and affected resource.
-Check the following dashboard generated by MetaHub. You have the affected resources, grouping all the security findings affecting them together and the original severity of the finding. After that, you have the **Impact Score** and all the criteria MetaHub evaluated to generate that score. All this information is filterable, sortable, groupable, downloadable, and customizable.
+Check the following dashboard generated by **MetaHub**. You have the affected resources, grouping all the security findings affecting them together and the original severity of each finding. After that, you have the **score** and all the criteria **MetaHub** evaluated to generate that **score**. All this information is filterable, sortable, groupable, downloadable, and customizable.
-You can rely on this **Impact Score** for prioritizing findings (where should you start?), directing attention to critical issues, and automating alerts and escalations.
-
-**MetaHub** can also filter, deduplicate, group, report, suppress, or update your security findings in automated workflows. It is designed for use as a CLI tool or within automated workflows, such as AWS Security Hub custom actions or AWS Lambda functions.
-
-The following is the JSON output for a an EC2 instance; see how MetaHub organizes all the information about its context together, under `associations`, `config`, `tags`, `account` `cloudtrail`, and `impact`
+The following is the JSON [output](#outputs) for an EC2 instance; see how **MetaHub** organizes all the information about its **context** together, under [**associations**](#associations), [**config**](#config), [**tags**](#tags), [**account**](#account) and [**cloudtrail**](#cloudtrail), and finally the [**impact**](#impact) key with the **score** and all the criteria evaluated to generate that **score**.
-# Context
+**MetaHub** provides a range of ways to list, manage, and output your security findings for investigation, suppression, updating, and integration with other tools. It is designed for use as a [CLI](#run-with-python) tool or within automated workflows, such as [AWS Lambda functions](#run-with-lambda). It supports different **[outputs](#outputs)**, some of them [**programatic json**](#json), but also powerful [**html**](#html), [**xlsx**](#xlsx) and [**csv**](#csv) that you can [customize](#customize-html-csv-or-xlsx-outputs).
+
+If you use **AWS Security Hub**, MetaHub integrates smoothly and extends its functionalities. It can be used as a **[Security Hub Custom Action](#run-with-security-hub-custom-action)**, it supports **[AWS Security Hub filters](security-hub-filtering)**, you can manage the **[workflow status of your findings](#updating-workflow-status)**, and you can even **[enrich your findings directly in AWS Security Hub](#enriching-findings)**.
+
+**MetaHub** is designed to be used with AWS and supports **multi-account setups**. You can run the tool from any environment by assuming roles in your AWS Security Hub `master` and your `child/service` accounts. This allows you to fetch aggregated data from multiple accounts using your AWS Security Hub multi-account implementation while also fetching and enriching those findings with data from the accounts where your affected resources are running. Refer to [Configuring Security Hub](#configuring-security-hub) for more information.
+
+# Quick Run
+
+Read your security findings from AWS Security Hub with the default filters and executes the default context options:
+
+```bash
+./metahub
+```
-In **MetaHub**, context refers to information about the affected resources like their configuration, associations, logs, tags, account, and more.
+Read your security findings from Prowler as an input file and executes the default context options:
-MetaHub doesn't stop at the affected resource but analyzes any associated or attached resources. For instance, if there is a security finding on an EC2 instance, MetaHub will not only analyze the instance but also the security groups attached to it, including their rules. MetaHub will examine the IAM roles that the affected resource is using and the policies attached to those roles for any issues. It will analyze the EBS attached to the instance and determine if they are encrypted. It will also analyze the Auto Scaling Groups that the instance is associated with and how. MetaHub will also analyze the VPC, Subnets, and other resources associated with the instance.
+```bash
+python3 prowler.py aws -M json-asff -q
+./metahub --inputs file-asff --input-asff /path/to/prowler-findings.json.asff
+```
+
+Read a specific (filtered by Id) security finding from AWS Security Hub and executes the default context options:
-The **Context** module has the capability to retrieve information from the affected resources, affected accounts, and every associated resources. The context module has five main parts: `config` (which includes `associations` as well), `tags`, `cloudtrail`, and `account`. By default `config`, `tags` and `account` are enabled, but you can change this behavior using the option `--context` (for enabling all the context modules you can use `--context config tags cloudtrail account`). The output of each enabled key will be added under the affected resource.
+```bash
+./metahub --sh-filters Id=arn:aws:securityhub:us-east-1:123456789012:security-control/CloudFront.1/finding/8bd4d049-dcbc-445b-a5d1-595d8274b4c1
+```
-- [Config](#config)
-- [Associations](#associations)
-- [Tags](#tags)
-- [CloudTrail](#cloudtrail)
-- [Account](#account)
+Read all the security findings affecting one resource which are ACTIVE (filtered by ResourceId and RecordState) from AWS Security Hub and executes the default context options:
+
+```bash
+./metahub --sh-filters RecordState=ACTIVE ResourceId=arn:aws:ec2:eu-west-1:123456789012:subnet/subnet-0b7d243ff90ebc03e
+```
+
+Read all the security findings affecting an AWS Account which are ACTIVE (filtered by AwsAccountId and RecordState) for resources with a tag `Environment` and the value `stg` and executes the context options `config` and `tags`:
+
+```bash
+./metahub --sh-filters RecordState=ACTIVE AwsAccountId=123456789012 --mh-filters-tags Environment=stg --context config tags
+```
+
+# Context
+
+In **MetaHub**, **context** refers to information about the affected resources like their **configuration**, **associations**, **logs**, **tags** and **account**.
+
+MetaHub doesn't stop at the affected resource but also analyzes any associated or attached resources. For instance, if a security finding exists on a Security Group, **MetaHub** will analyze the Security Group and everything else associated with it, like the EC2 instances using it. For each associated resource, **MetaHub** will fetch its context. If the Security Group is attached to an EC2 Instance, **MetaHub** will analyze the instance and all its associations, like the IAM Roles and policies. From a single security finding on a Security Group, **MetaHub** will fetch the context of the Security Group, the EC2 Instance, the IAM Roles, and the IAM Policies. This is critical for understanding the impact of your security findings.
+
+The **context** module has five main parts: [**config**](#config) (which includes [**associations**](#associations)), [**tags**](#tags), [**cloudtrail**](#cloudtrail), and [**account**](#account). By default only **config**, **tags** and **account** are enabled, but you can change this behavior using the option `--context` (e.g. use `--context config tags cloudtrail account` for enabling all context keys, or `--context config` for enabling only the config and associations key.):
## Config
-Under the `config` key, you can find anyting related to the configuration of the affected resource. For example, if the affected resource is an EC2 Instance, you will see keys like `private_ip`, `public_ip`, or `instance_profile`.
+Under the `config` key, you can find important configuration from the affected resource. For example, if the affected resource is an S3 Bucket, you will find information about its bucket policy, its ACLs, its encryption configuration, and more. If the affected resource is an EC2 Instance, you will find information about its key, its public and private IP, its metadata and more. The configuration information that **MetaHub** fetches is defined by resource type. If you want to add more configuration information see [contributing](#contributing).
-You can filter your findings based on Config outputs using the option: `--mh-filters-config {True/False}`. See [Config Filters](#config-filters).
+You can filter your findings based on config outputs using the option: `--mh-filters-config {True/False}` (see [config filters](#config-filters)).
+
+
+ Example for an S3 bucket config key
+
+```json
+"config": {
+ "resource_policy": {
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Sid": "Test",
+ "Effect": "Allow",
+ "Principal": {
+ "Service": "config.amazonaws.com"
+ },
+ "Action": "s3:GetBucketAcl",
+ "Resource": "arn:aws:s3:::metahub-bucket",
+ "Condition": {
+ "StringEquals": {
+ "AWS:SourceAccount": "123456789012"
+ }
+ }
+ },
+ ]
+ },
+ "website_enabled": false,
+ "bucket_acl": [
+ {
+ "Grantee": {
+ "DisplayName": "gabriel.soltz",
+ "ID": "1234564bd76c6c64080717b68eafaa588b41706daaf22d3d0705b398bd7cbd57",
+ "Type": "CanonicalUser"
+ },
+ "Permission": "FULL_CONTROL"
+ }
+ ],
+ "cannonical_user_id": "1234564bd76c6c64080717b68eafaa588b41706daaf22d3d0705b398bd7cbd57",
+ "public_access_block_enabled": {
+ "BlockPublicAcls": true,
+ "IgnorePublicAcls": true,
+ "BlockPublicPolicy": true,
+ "RestrictPublicBuckets": true
+ },
+ "account_public_access_block_enabled": false,
+ "public": false,
+ "bucket_encryption": [
+ {
+ "ApplyServerSideEncryptionByDefault": {
+ "SSEAlgorithm": "AES256"
+ },
+ "BucketKeyEnabled": false
+ }
+ ]
+},
+```
+
+
## Associations
-Under the `associations` key, you will find all the associated resources of the affected resource. For example, if the affected resource is an EC2 Instance, you will find resources like: Security Groups, IAM Roles, Volumes, VPC, Subnets, Auto Scaling Groups, etc. Each time MetaHub finds an association, it will connect to the associated resource again and fetch its own context.
+Under the `associations` key, you will find all the associated resources of the affected resource. For example, if the affected resource is an EC2 Instance, you will find resources like: Security Groups, IAM Roles, Volumes, VPC, Subnets, Auto Scaling Groups. If the affected resource is a IAM Role, you will find resources like IAM Policies, IAM Users, IAM Groups, and more. Each time MetaHub finds an association, it will connect to the associated resource again and fetch its own context. Associations are key to understanding the context and impact of your security findings.
-Associations are key to understanding the context and impact of your security findings as their exposure.
+You can filter your findings based on associations outputs using the option: `--mh-filters-config {True/False}` (see [config filters](#config-filters)).
-You can filter your findings based on Associations outputs using the option: `--mh-filters-config {True/False}`. See [Config Filters](#config-filters).
+
+ Example for an EC2 instance assocations key
+
+```json
+"associations": {
+ "security_groups": {
+ "arn:aws:ec2:eu-west-1:123456789012:security-group/sg-020cc749a58678e05": {
+ "associations": {
+ "vpcs": {
+ "arn:aws:ec2:eu-west-1:123456789012:vpc/vpc-03cc56a1c2afb5760": {
+ "associations": {
+ "subnets": {
+ "arn:aws:ec2:eu-west-1:123456789012:subnet/subnet-03d86f1ccd7729d85": {},
+ "arn:aws:ec2:eu-west-1:123456789012:subnet/subnet-0ccfb8dea658f49ec": {},
+ "arn:aws:ec2:eu-west-1:123456789012:subnet/subnet-05e85a7b0ec9e404c": {},
+ "arn:aws:ec2:eu-west-1:123456789012:subnet/subnet-0e177ea95bcc76256": {}
+ }
+ },
+ "config": {
+ "cidr": "172.10.0.0/16",
+ "default": false,
+ "public": null
+ }
+ }
+ },
+ "network_interfaces": {
+ "arn:aws:ec2:eu-west-1:123456789012:network-interface/eni-041a6e5bb59c336ee": {}
+ },
+ "instances": {
+ "arn:aws:ec2:eu-west-1:123456789012:instance/i-018daeedcf06398c0": {}
+ }
+ },
+ "config": {
+ "public_ips": [
+ "100.100.100.100"
+ ],
+ "managed_services": [],
+ "its_referenced_by_a_security_group": false,
+ "security_group_rules": [
+ {
+ "SecurityGroupRuleId": "sgr-08cdc9fdac8fd1a5b",
+ "GroupId": "sg-020cc749a58678e05",
+ "GroupOwnerId": "123456789012",
+ "IsEgress": true,
+ "IpProtocol": "-1",
+ "FromPort": -1,
+ "ToPort": -1,
+ "CidrIpv4": "0.0.0.0/0",
+ "Tags": []
+ },
+ {
+ "SecurityGroupRuleId": "sgr-0e6cd39169dc137ab",
+ "GroupId": "sg-020cc749a58678e05",
+ "GroupOwnerId": "123456789012",
+ "IsEgress": false,
+ "IpProtocol": "tcp",
+ "FromPort": 22,
+ "ToPort": 22,
+ "CidrIpv4": "0.0.0.0/0",
+ "Tags": []
+ }
+ ],
+ "public": true,
+ "default": false,
+ "attached": true,
+ "resource_policy": null
+ }
+ },
+ },
+ "iam_roles": {
+ "arn:aws:iam::123456789012:role/eu-west-1-stg-backend-iam-role": {
+ "associations": {
+ "iam_policies": {
+ "arn:aws:iam::123456789012:policy/eu-west-1-stg-backend-iam-policy-cw": {
+ "associations": {
+ "iam_roles": {
+ "arn:aws:iam::123456789012:role/eu-west-1-stg-backend-iam-role": {}
+ },
+ "iam_groups": {},
+ "iam_users": {}
+ },
+ "config": {
+ "name": "eu-west-1-stg-backend-iam-policy-cw",
+ "description": false,
+ "customer_managed": true,
+ "attached": true,
+ "public": null,
+ "resource_policy": {
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": [
+ "logs:CreateLogGroup",
+ "logs:CreateLogStream",
+ "logs:PutLogEvents",
+ "logs:DescribeLogStreams"
+ ],
+ "Resource": [
+ "arn:aws:logs:*:*:*"
+ ]
+ }
+ ]
+ }
+ }
+ },
+ }
+ },
+ "config": {
+ "iam_inline_policies": {},
+ "instance_profile": "arn:aws:iam::123456789012:instance-profile/eu-west-1-stg-backend-iam-profile",
+ "trust_policy": {
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Sid": "",
+ "Effect": "Allow",
+ "Principal": {
+ "Service": "ec2.amazonaws.com"
+ },
+ "Action": "sts:AssumeRole"
+ }
+ ]
+ },
+ "permissions_boundary": false,
+ "public": null,
+ "resource_policy": null
+ }
+ }
+ },
+ "volumes": {
+ "arn:aws:ec2:eu-west-1:123456789012:volume/vol-0371a09e338f582da": {
+ "associations": {
+ "instances": {
+ "arn:aws:ec2:eu-west-1:123456789012:instance/i-018daeedcf06398c0": {}
+ }
+ },
+ "config": {
+ "encrypted": true,
+ "attached": true,
+ "public": null,
+ "resource_policy": null
+ }
+ }
+ },
+ "autoscaling_groups": {
+ "arn:aws:autoscaling:eu-west-1:123456789012:autoScalingGroup/stg-backend-20201205160228428400000002": {
+ "associations": {
+ "instances": {
+ "arn:aws:ec2:eu-west-1:123456789012:instance/i-018daeedcf06398c0": {}
+ },
+ "launch_templates": {
+ "arn:aws:ec2:eu-west-1:123456789012:launch-template/lt-06b73d2e77f10446f": {}
+ },
+ "launch_configurations": {}
+ },
+ "config": {
+ "name": "stg-backend-20201205160228428400000002",
+ "public": null,
+ "resource_policy": null
+ }
+ }
+ },
+ "vpcs": {
+ "arn:aws:ec2:eu-west-1:123456789012:vpc/vpc-03cc56a1c2afb5760": {
+ "associations": {
+ "subnets": {
+ "arn:aws:ec2:eu-west-1:123456789012:subnet/subnet-03d86f1ccd7729d85": {},
+ "arn:aws:ec2:eu-west-1:123456789012:subnet/subnet-0ccfb8dea658f49ec": {},
+ "arn:aws:ec2:eu-west-1:123456789012:subnet/subnet-05e85a7b0ec9e404c": {},
+ "arn:aws:ec2:eu-west-1:123456789012:subnet/subnet-0e177ea95bcc76256": {}
+ }
+ },
+ "config": {
+ "cidr": "172.10.0.0/16",
+ "default": false,
+ "public": null
+ }
+ }
+ },
+ "subnets": {
+ "arn:aws:ec2:eu-west-1:123456789012:subnet/subnet-03d86f1ccd7729d85": {
+ "associations": {
+ "route_tables": {
+ "arn:aws:ec2:eu-west-1:123456789012:route-table/rtb-0ebae6462f919943d": {
+ "associations": {},
+ "config": {
+ "default": false,
+ "route_to_internet_gateway": [
+ {
+ "DestinationCidrBlock": "0.0.0.0/0",
+ "GatewayId": "igw-0790540d8d726f9d4",
+ "Origin": "CreateRoute",
+ "State": "active"
+ }
+ ],
+ "route_to_nat_gateway": [],
+ "route_to_transit_gateway": [],
+ "route_to_vpc_peering": [],
+ "public": null
+ }
+ }
+ },
+ "network_interfaces": {
+ "arn:aws:ec2:eu-west-1:123456789012:network-interface/eni-0e8918fa31d2acd55": {},
+ "arn:aws:ec2:eu-west-1:123456789012:network-interface/eni-0f6c936934fd9d6a6": {},
+ "arn:aws:ec2:eu-west-1:123456789012:network-interface/eni-041a6e5bb59c336ee": {},
+ "arn:aws:ec2:eu-west-1:123456789012:network-interface/eni-0e5f6cfdc7c286224": {}
+ },
+ "instances": {
+ "arn:aws:ec2:eu-west-1:123456789012:instance/i-018daeedcf06398c0": {}
+ }
+ },
+ "config": {
+ "cidr": "172.11.11.0/24",
+ "map_public_ip_on_launch_enabled": true,
+ "default": false,
+ "public": true,
+ "resource_policy": null,
+ "public_ips": [
+ "100.100.100.100",
+ ],
+ "managed_services": [
+ "ELB app/stg-alb-backend/3567d780bd062d75",
+ "Interface for NAT Gateway nat-0805d9808347bba69"
+ ],
+ "attached": true
+ }
+ }
+ },
+}
+```
-## Tags
+
-**MetaHub** relies on [AWS Resource Groups Tagging API](https://docs.aws.amazon.com/resourcegroupstagging/latest/APIReference/overview.html) to query the tags associated with your resources.
+## Tags
-Note that not all AWS resource type supports this API. You can check [supported services](https://docs.aws.amazon.com/resourcegroupstagging/latest/APIReference/supported-services.html).
+Under the `tags` key, you will find all the tags associated with the affected resource. **MetaHub** relies on [AWS Resource Groups Tagging API](https://docs.aws.amazon.com/resourcegroupstagging/latest/APIReference/overview.html) to query the tags associated with your resources. Note that not all AWS resource type supports this API. You can check [supported services](https://docs.aws.amazon.com/resourcegroupstagging/latest/APIReference/supported-services.html).
Tags are a crucial part of understanding your context. Tagging strategies often include:
@@ -98,37 +414,111 @@ Tags are a crucial part of understanding your context. Tagging strategies often
If you follow a proper tagging strategy, you can filter and generate interesting outputs. For example, you could list all findings related to a specific team and provide that data directly to that team.
-You can filter your findings based on Tags outputs using the option: `--mh-filters-tags TAG=VALUE`. See [Tags Filtering](#tags-filtering)
+You can filter your findings based on tags outputs using the option: `--mh-filters-tags TAG=VALUE` (see [tags filters](#tags-filters)).
+
+
+ Example for an EC2 instance tag key
+
+```json
+"tags": {
+ "aws:autoscaling:groupName": "stg-backend-20201205160228428400000002",
+ "environment": "stg",
+ "terraform": "true",
+ "aws:ec2launchtemplate:version": "8",
+ "aws:ec2launchtemplate:id": "lt-06b73d2e77f10446f",
+ "Name": "stg-backend"
+}
+```
+
+
## CloudTrail
-Under the key `cloudtrail`, you will find critical Cloudtrail events related to the affected resource, such as creating events.
+Under the key `cloudtrail`, you will find critical Cloudtrail events related to the affected resource, such as creation events. The Cloudtrail events we look for are defined by resource type, and you can add, remove, or change them by editing the configuration file [resources.py](lib/config/resources.py). For example, for an affected resource of type Security Group, MetaHub will look for the following events: `CreateSecurityGroup` (Security Group Creation event) and `AuthorizeSecurityGroupIngress` (Security Group Rule Authorization event).
+
+
+ Example for an EC2 instance cloudtrail key
+
+```json
+"cloudtrail": {
+ "RunInstances": {
+ "Username": "root",
+ "EventTime": "2023-11-15 06:10:07+01:00",
+ "EventId": "4f122d76-812d-4438-bc33-3585a9e863cf"
+ }
+}
+```
-The Cloudtrail events that we look for are defined by resource type, and you can add, remove or change them by editing the configuration file [resources.py](lib/config/resources.py).
+
-For example for an affected resource of type Security Group, MetaHub will look for the following events:
+
+ Example for a DynamoDB table cloudtrail key
+
+```json
+"cloudtrail": {
+ "CreateTable": {
+ "Username": "gabriel.soltz",
+ "EventTime": "2023-12-05 14:34:25+01:00",
+ "EventId": "7110e3ae-09a3-44b9-929a-1775e0fbedcf"
+ }
+}
+```
-- `CreateSecurityGroup`: Security Group Creation event
-- `AuthorizeSecurityGroupIngress`: Security Group Rule Authorization event.
+
## Account
-Under the key `account`, you will find information about the account where the affected resource is runnning, like if it's part of an AWS Organizations, information about their contacts, etc.
+Under the key `account`, you will find information about the account where the affected resource is runnning, like if it's part of an AWS Organizations, information about their contacts, and more.
-# Impact
+
+
+
+ Example for account key
+
+```json
+"account": {
+ "Alias": "metahub-demo",
+ "AlternateContact": {
+ "AlternateContactType": "SECURITY",
+ "EmailAddress": "gabriel@domain.com",
+ "Name": "Gabriel",
+ "PhoneNumber": "+1234567890",
+ "Title": "Security"
+ },
+ "Organizations": {
+ "Arn": "arn:aws:organizations::123456789012:organization/o-12349772jb",
+ "Id": "o-12349772jb",
+ "MasterAccountId": "123456789012",
+ "MasterAccountEmail": "gabriel.soltz@domain.com",
+ "FeatureSet": "ALL",
+ "DelegatedAdministrators": {},
+ "Details": {
+ "ParentId": "r-k123",
+ "ParentType": "ROOT",
+ "OU": "ROOT",
+ "Policies": {
+ "p-FullAWSAccess": {
+ "Name": "FullAWSAccess",
+ "Arn": "arn:aws:organizations::aws:policy/service_control_policy/p-FullAWSAccess",
+ "Type": "SERVICE_CONTROL_POLICY",
+ "Description": "Allows access to every operation",
+ "AwsManaged": true,
+ "Targets": []
+ }
+ }
+ }
+ }
+},
+```
-The impact module in MetaHub focuses on generating a score for each finding based on the context of the affected resource and all the security findings affecting them and their severities together. The impact score is a number between 0 and 100, where 100 is the highest impact.
+
+
+# Impact
-The formula for getting the impact score include the following criteria:
+
-- [Exposure](#exposure)
-- [Access](#access)
-- [Encryption](#encryption)
-- [Status](#status)
-- [Environment](#environment)
-- [Application](#application)
-- [Owner](#owner)
-- [Findings](#findings)
+The impact module in MetaHub focuses on understanding the 7 key properties of the affected resource: [**exposure**](#exposure), [**access**](#access), [**encryption**](#encryption), [**status**](#status), [**environment**](#environment), [**application**](#application), and [**owner**](#owner) and combining their values with the values of all the security [findings](#findings) affecting the same resource and their severities to generate a **score**. The impact score is a number between 0 and 100, where 100 is the highest impact.
+
## Exposure
@@ -143,6 +533,46 @@ The formula for getting the impact score include the following criteria:
| 🟢 restricted | 0% | The resource is restricted. |
| 🔵 unknown | - | The resource couldn't be checked |
+
+ Example for an effectively-public resource
+
+```json
+"exposure": { --> The exposure key
+ "effectively-public": { --> The exposure value, effectively-public
+ "entrypoint": "66.66.66.66", --> The entrypoint to the resource from the Internet (Ip, Domain, etc.)
+ "unrestricted_ingress_rules": [ --> The unrestricted ingress rules, if any
+ {
+ "SecurityGroupRuleId": "sgr-0553206714e321b87",
+ "GroupId": "sg-0a15a46e47f07d139",
+ "GroupOwnerId": "123456789012",
+ "IsEgress": false,
+ "IpProtocol": "tcp",
+ "FromPort": 22,
+ "ToPort": 22,
+ "CidrIpv4": "0.0.0.0/0",
+ "Tags": []
+ }
+ ],
+ "unrestricted_egress_rules": [ --> The unrestricted egress rules, if any
+ {
+ "SecurityGroupRuleId": "sgr-007b509667896ebe3",
+ "GroupId": "sg-0a15a46e47f07d139",
+ "GroupOwnerId": "123456789012",
+ "IsEgress": true,
+ "IpProtocol": "-1",
+ "FromPort": -1,
+ "ToPort": -1,
+ "CidrIpv4": "0.0.0.0/0",
+ "Tags": []
+ },
+ ],
+ "resource_public_config": true --> The public configuration of the resource
+ }
+}
+```
+
+
+
## Access
**Access** evaluates the resource policy layer. MetaHub checks every available policy including: IAM Managed policies, IAM Inline policies, Resource Policies, Bucket ACLS, and any association to other resources like IAM Roles which its policies are also analyzed . An unrestricted policy is not only an itsue itself of that policy, it afected any other resource which is using it.
@@ -159,6 +589,30 @@ The formula for getting the impact score include the following criteria:
| 🟢 restricted | 0% | The policy is restricted. |
| 🔵 unknown | - | The policy couldn't be checked. |
+
+ Example for an unrestricted-actions resource
+
+```json
+"access": { --> The access key
+ "unrestricted-actions": { --> The access value, unrestricted-actions
+ "wildcard_actions": { --> The wildcard policies, if any
+ "arn:aws:iam::123456789012:policy/eu-west-1-stg-iam-policy-dynamodb-cache": [
+ {
+ "Action": [
+ "dynamodb:*" --> The wildcard action
+ ],
+ "Effect": "Allow",
+ "Resource": [
+ "arn:aws:dynamodb:eu-west-1:123456789012:table/table",
+ ]
+ }
+ ],
+ }
+}
+```
+
+
+
## Encryption
**Encryption** evaluate the different encryption layers based on each resource type. For example, for some resources it evaluates if `at_rest` and `in_transit` encryption configuration are both enabled.
@@ -169,6 +623,22 @@ The formula for getting the impact score include the following criteria:
| 🟢 encrypted | 0% | The resource is fully encrypted including any of it's associations. |
| 🔵 unknown | - | The resource encryption couldn't be checked. |
+
+ Example for an unencrypted resource
+
+```json
+"encryption": { --> The encryption key
+ "unencrypted": { --> The encryption value, unencrypted
+ "unencrypted_resources": [ --> the unencrypted resources associated with the affected resource, if any
+ "arn:aws:ec2:eu-west-1:012345678901:volume/vol-0ac713ec808a8d8bd"
+ ],
+ "resource_encryption_config": null --> The encryption configuration of the resource, if it has any
+ }
+}
+```
+
+
+
## Status
**Status** evaluate the status of the affected resource in terms of attachment or functioning. For example, for an EC2 Instance we evaluate if the resource is running, stopped, or terminated, but for resources like EBS Volumes and Security Groups, we evaluate if those resources are attached to any other resource.
@@ -183,6 +653,20 @@ The formula for getting the impact score include the following criteria:
| 🟢 not-enabled | 0% | The resource supports enabled and it is not enabled. |
| 🔵 unknown | - | The resource couldn't be checked for status. |
+
+ Example for a running resource
+
+```json
+"status": { --> The status key
+ "running": { --> The status value, running
+ "status": "running", --> The status configuration of the resource, if it has any
+ "attached": null --> The attachment configuration of the resource, if it has any
+ }
+}
+```
+
+
+
## Environment
**Environment** evaluates the environment where the affected resource is running. By default, MetaHub defines 3 environments: `production`, `staging`, and `development`, but you can add, remove, or modify these environments based on your needs. MetaHub evaluates the environment based on the tags of the affected resource, the account id or the account alias. You can define your own environemnts definitions and strategy in the configuration file (See [Customizing Configuration](#customizing-configuration)).
@@ -194,6 +678,21 @@ The formula for getting the impact score include the following criteria:
| 🟢 development | 0% | It is a development resource. |
| 🔵 unknown | - | The resource couldn't be checked for enviroment. |
+
+ Example for a production resource matched by Tags
+
+```json
+"environment": { --> The environment key
+ "production": { --> The environment value, production
+ "tags": { --> The parameters used for evaluating the environment, in this case tags
+ "Env": "prod" --> The tag and key found used for evaluating the environment
+ }
+ }
+}
+```
+
+
+
## Application
**Application** evaluates the application that the affected resource is part of. MetaHub relies on the AWS [myApplications](https://docs.aws.amazon.com/awsconsolehelpdocs/latest/gsg/aws-myApplications.html) feature, which relies on the Tag `awsApplication`, but you can extend this functionality based on your context for example by defining other tags you use for defining applications or services (like `Service` or any other), or by relying on account id or alias. You can define your application definitions and strategy in the configuration file (See [Customizing Configuration](#customizing-configuration)).
@@ -202,6 +701,21 @@ The formula for getting the impact score include the following criteria:
| --------------------- | :-------: | ------------------------------------------------- |
| 🔵 unknown | - | The resource couldn't be checked for application. |
+
+ Example for a resource matched by myApplication Tag
+
+```json
+"application": { --> The application key
+ "payments-app": { --> The application value, payments-app
+ "tags": { --> The parameters used for evaluating the environment, in this case the tag awsApplication
+ "awsApplication": "arn:aws:resource-groups:eu-west-1:123456789012:group/app1/0c8vpbjkzeeffsz2cqgxpae7b2" --> The tag and key found used for evaluating the environment
+ }
+ }
+}
+```
+
+
+
## Owner
**Owner** focuses on ownership detection. It can determine the owner of the affected resource in various ways. This information can be used to automatically assign a security finding to the correct owner, escalate it, or make decisions based on this information. An automated way to determine the owner of a resource is critical for security teams. It allows them to focus on the most critical issues and assign them as fast as possible to the right people in automated workflows. You can define your owner definitions and strategy in the configuration file (See [Customizing Configuration](#customizing-configuration)).
@@ -210,6 +724,22 @@ The formula for getting the impact score include the following criteria:
| --------------------- | :-------: | ------------------------------------------- |
| 🔵 unknown | - | The resource couldn't be checked for owner. |
+
+ Example for a resource matched by Account Id
+
+```json
+"owner": { --> The owner key
+ "payments-team": { --> The owner value, payments-app
+ "account": { --> The parameters used for evaluating the environment, in this case the account
+ "account_ids": ["123456789012"], --> The account ids found used for evaluating the environment
+ "account_aliases": [],
+ },
+ }
+}
+```
+
+
+
## Findings
As part of the impact score calculation, we also evaluate the total ammount of security findings and their severities affecting the resource. We use the following formula to calculate this metric:
@@ -242,30 +772,6 @@ Some use cases for MetaHub include:
- Created enriched HTML reports for your findings that you can filter, sort, group, and download
- Create Security Hub Insights based on MetaHub context
-# Features
-
-**MetaHub** provides a range of ways to list and manage security findings for investigation, suppression, updating, and integration with other tools or alerting systems. To avoid _Shadowing_ and _Duplication_, MetaHub organizes related findings together when they pertain to the same resource. For more information, refer to [Findings Aggregation](#findings-aggregation)
-
-**MetaHub** queries the affected resources directly in the affected account to provide additional **context** using the following options:
-
-- **[Config](#Config)**: Fetches the most important configuration values from the affected resource.
-- **[Associations](#Associations)**: Fetches all the associations of the affected resource, such as IAM roles, security groups, and more.
-- **[Tags](#Tags)**: Queries tagging from affected resources
-- **[CloudTrail](#CloudTrail)**: Queries CloudTrail in the affected account to identify who created the resource and when, as well as any other related critical events
-- **[Account](#Account)**: Fetches extra information from the account where the affected resource is running, such as the account name, security contacts, and other information.
-
-**MetaHub** supports filters on top of these context\* outputs to automate the detection of other resources with the same issues. You can filter security findings affecting resources tagged in a certain way (e.g., `Environment=production`) and combine this with filters based on Config or Associations, like, for example, if the resource is public, if it is encrypted, only if they are part of a VPC, if they are using a specific IAM role, and more. For more information, refer to **[Config filters](#config-filters)** and **[Tags filters](#tags-filters)** for more information.
-
-But that's not all. If you are using **MetaHub** with Security Hub, you can even combine the previous filters with the Security Hub native filters (**[AWS Security Hub filtering](security-hub-filtering)**). You can filter the same way you would with the AWS CLI utility using the option `--sh-filters`, but in addition, you can save and re-use your filters as YAML files using the option `--sh-template`.
-
-If you prefer, With **MetaHub**, you can back **[enrich your findings directly in AWS Security Hub](#enriching-findings)** using the option `--enrich-findings`. This action will update your AWS Security Hub findings using the field `UserDefinedFields`. You can then create filters or [Insights](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-insights.html) directly in AWS Security Hub and take advantage of the contextualization added by MetaHub.
-
-When investigating findings, you may need to update security findings altogether. **MetaHub** also allows you to execute **[bulk updates](#updating-workflow-status)** to AWS Security Hub findings, such as changing Workflow Status using the option `--update-findings`. As an example, you identified that you have hundreds of security findings about public resources. Still, based on the MetaHub context, you know those resources are not effectively public as they are protected by routing and firewalls. You can update all the findings for the output of your MetaHub query with one command. When updating findings using MetaHub, you also update the field `Note` of your finding with a custom text for future reference.
-
-**MetaHub** supports different **[Output Modes](#output-modes)**, some of them **json based** like **json-inventory**, **json-statistics**, **json-short**, **json-full**, but also powerfull **html**, **xlsx** and **csv**. These outputs are customizable; you can choose which columns to show. For example, you may need a report about your affected resources, adding the tag Owner, Service, and Environment and nothing else. Check the configuration file and define the columns you need.
-
-**MetaHub** supports **multi-account setups**. You can run the tool from any environment by assuming roles in your AWS Security Hub `master` account and your `child/service` accounts where your resources live. This allows you to fetch aggregated data from multiple accounts using your AWS Security Hub multi-account implementation while also fetching and enriching those findings with data from the accounts where your affected resources live based on your needs. Refer to [Configuring Security Hub](#configuring-security-hub) for more information.
-
# Customizing Configuration
**MetaHub** uses configuration files that let you customize some checks behaviors, default filters, and more. The configuration files are located in [lib/config/](lib/config).
@@ -410,7 +916,7 @@ The Security Hub custom action is deployed as part of the Terraform code. See [D
# AWS Authentication
-- Ensure you have AWS credentials set up on your local machine (or from where you will run MetaHub).
+Ensure you have AWS credentials set up on your local machine (or from where you will run MetaHub).
For example, you can use `aws configure` option.
@@ -495,13 +1001,9 @@ You also can combine AWS Security Hub findings with input ASFF files specifying
When using a file as input, you can't use the option `--sh-filters` for filter findings, as this option relies on AWS API for filtering. You can't use the options `--update-findings` or `--enrich-findings` as those findings are not in the AWS Security Hub. If you are reading from both sources at the same time, only the findings from AWS Security Hub will be updated.
-# Output Modes
+# Outputs
-**MetaHub** can generate different programmatic and visual outputs. By default, all output modes are enabled: `json-short`, `json-full`, `json-statistics`, `json-inventory`, `html`, `csv`, and `xlsx`.
-
-The outputs will be saved in the `outputs/` folder with the execution date.
-
-If you want only to generate a specific output mode, you can use the option `--output-modes` with the desired output mode.
+**MetaHub** can generate different programmatic and visual outputs. By default, all output modes are enabled: `json-short`, `json-full`, `json-statistics`, `json-inventory`, `html`, `csv`, and `xlsx`. If you want only to generate a specific output mode, you can use the option `--output-modes` with the desired output mode. The outputs will be saved in the `outputs/` folder with the execution date.
For example, if you only want to generate the output `json-short`, you can use:
@@ -515,6 +1017,8 @@ If you want to generate `json-short`, `json-full` and `html` outputs, you can us
./metahub.py --output-modes json-short json-full html
```
+**MetaHub** organizes the security findings affecting the same resource all together under the `findings` key for in an attempt to avoid Shadowing (when two checks refer to the same issue, but one in a more generic way than the other one) and Duplication (when you use more than one scanner and get the same problem from more than one.). You can see this behaviour clear in the outputs `json-short`, `json-full` and `html`.
+
- [JSON](#json)
- [HTML](#html)
- [CSV](#csv)
@@ -522,96 +1026,186 @@ If you want to generate `json-short`, `json-full` and `html` outputs, you can us
## JSON
-### JSON-Short
-
-Show all findings titles together under each affected resource and the `AwsAccountId`, `Region`, and `ResourceType`:
-
-```
-"arn:aws:sagemaker:us-east-1:ofuscated:notebook-instance/obfuscated": {
- "findings": [
- "SageMaker.2 SageMaker notebook instances should be launched in a custom VPC",
- "SageMaker.3 Users should not have root access to SageMaker notebook instances",
- "SageMaker.1 Amazon SageMaker notebook instances should not have direct internet access"
- ],
- "AwsAccountId": "obfuscated",
- "Region": "us-east-1",
- "ResourceType": "AwsSageMakerNotebookInstance",
- "config: {},
- "associations: {},
- "tags: {},
- "cloudtrail: {},
- "account: {}
- "impact: {}
-},
-```
+> :info: For exploring a JSON output interactively, you can use the tool [fx](https://github.com/antonmedv/fx).
### JSON-Full
-Show all findings with all data. Findings are organized by ResourceId (ARN). For each finding, you will also get: `SeverityLabel,` `Workflow,` `RecordState,` `Compliance,` `Id`, and `ProductArn`:
+Shows the affected resource using it's ARN as the key and the findings affecting it as a list under the key `findings`. In adittion to the findings, you will also get the following keys: `ResourceType`, `Region`, `AwsAccountId`, `associations`, `config`, `tags`, `account`, `cloudtrail`, and `impact`.
-```
-"arn:aws:sagemaker:eu-west-1:ofuscated:notebook-instance/obfuscated": {
- "findings": [
+```json
+"arn:aws:ec2:eu-west-1:1234567890:instance/i-0a40b2be25dbac0ac": { --> The affected resource ARN
+ "findings": [ --> The findings affecting the resource
{
- "SageMaker.3 Users should not have root access to SageMaker notebook instances": {
- "SeverityLabel": "HIGH",
- "Workflow": {
- "Status": "NEW"
+ "EC2 instances should use Instance Metadata Service Version 2 (IMDSv2)": { --> The finding title
+ "SeverityLabel": "HIGH", --> The finding severity label
+ "Workflow":{ --> The finding workflow
+ "Status": "NEW",
},
- "RecordState": "ACTIVE",
- "Compliance": {
- "Status": "FAILED"
+ "RecordState": "ACTIVE", --> The finding record state
+ "Compliance":{ --> The finding compliance
+ "Status": "FAILED",
},
- "Id": "arn:aws:security hub:eu-west-1:ofuscated:subscription/aws-foundational-security-best-practices/v/1.0.0/SageMaker.3/finding/12345-0193-4a97-9ad7-bc7c1730eec6",
- "ProductArn": "arn:aws:security hub:eu-west-1::product/aws/security hub"
+ "Id": "arn:aws:securityhub:eu-west-1:123456789012:security-control/EC2.8/finding/a1d4f19f-453e-4c3c-b486-8443c73e84f1",
+ "ProductArn": "arn:aws:securityhub:eu-west-1::product/aws/securityhub",
}
},
- {
- "SageMaker.2 SageMaker notebook instances should be launched in a custom VPC": {
- "SeverityLabel": "HIGH",
- "Workflow": {
- "Status": "NEW"
- },
- "RecordState": "ACTIVE",
- "Compliance": {
- "Status": "FAILED"
- },
- "Id": "arn:aws:security hub:eu-west-1:ofuscated:subscription/aws-foundational-security-best-practices/v/1.0.0/SageMaker.2/finding/12345-e8e1-4915-9881-965104b0aabf",
- "ProductArn": "arn:aws:security hub:eu-west-1::product/aws/security hub"
- }
+ {"EC2 instances should be managed by AWS Systems Manager": {...}}, --> Another finding title
+ {"EC2 instances should not have a public IPv4 address": {...}} --> Another finding title
+ ],
+ "ResourceType": "AwsEc2Instance", --> The affected resource type
+ "Region": "eu-west-1", --> The affected resource region
+ "AwsAccountId": "1234567890", --> The affected resource account id
+ "associations": { --> The associations of the affected resource
+ "security_groups": {}, --> The security groups associated with the affected resource
+ "iam_roles": {}, --> The IAM roles associated with the affected resource
+ "volumes": {}, --> The volumes associated with the affected resource
+ "autoscaling_groups": {}, --> The autoscaling groups associated with the affected resource
+ "vpcs": {}, --> The VPCs associated with the affected resource
+ "subnets": {}, --> The subnets associated with the affected resource
+ },
+ "config": { --> The configuration of the affected resource (based on it's type)
+ "public_ip": "200.200.200.200", --> The public IP of the affected resource, if any
+ "private_ip": "10.10.10.10", --> The private IP of the affected resource, if any
+ "key": "ssh-key", --> The key used for the affected resource, if any
+ "metadata_options": { --> The metadata options of the affected resource, if any
+ "State": "applied",
+ "HttpTokens": "required",
+ "HttpPutResponseHopLimit": 1,
+ "HttpEndpoint": "enabled",
+ "HttpProtocolIpv6": "disabled",
+ "InstanceMetadataTags": "disabled"
},
- {
- "SageMaker.1 Amazon SageMaker notebook instances should not have direct internet access": {
- "SeverityLabel": "HIGH",
- "Workflow": {
- "Status": "NEW"
- },
- "RecordState": "ACTIVE",
- "Compliance": {
- "Status": "FAILED"
- },
- "Id": "arn:aws:security hub:eu-west-1:ofuscated:subscription/aws-foundational-security-best-practices/v/1.0.0/SageMaker.1/finding/12345-3a21-4016-a8e5-f5173b44e90a",
- "ProductArn": "arn:aws:security hub:eu-west-1::product/aws/security hub"
+ },
+ "tags": { --> The tags of the affected resource
+ "Name": "test", --> The tag key and value
+ "Env": "prod",
+ "awsApplication": "arn:aws:resource-groups:eu-west-1:1234567890:group/app1/0c8vpbjkzeeffsz2cqgxpae7b2"
+ },
+ "account": { --> The account of the affected resource
+ "Alias": "prod", --> The account alias
+ "AlternateContact": {}, --> The alternate contact of the account, if any
+ "Organizations": { --> The organization of the account, if any
+ "Id": "o-1234567890",
+ "Arn": "arn:aws:organizations::1234567890:organization/o-1234567890/o-1234567890",
+ "MasterAccountId": "1234567890",
+ "MasterAccountArn": "arn:aws:organizations::1234567890:account/o-1234567890/1234567890",
+ "MasterAccountEmail": "",
+ "Details": {
+ "ParentId": "p-k1234567890",
+ "ParentType": "ROOT",
+ "OU": "ROOT",
+ "Policies": { --> The policies of the account, if any
+ "p-FullAWSAccess": {...} --> The policy name and policy
+ }
}
+ },
+ },
+ "cloudtrail": { --> The CloudTrail events affecting the affected resource
+ "RunInstances": { --> The CloudTrail event name
+ "Username": "test", --> The username of the event, if any
+ "EventTime": "2021-01-01T00:00:00Z",
+ "EventId": "12345678-1234-1234-1234-123456789012",
}
+ },
+ "impact": { --> The impact of the affected resource
+ "exposure": {...}, --> The exposure impact
+ "access": {...}, --> The access impact
+ "encryption": {...}, --> The encryption impact
+ "status": {...}, --> The status impact
+ "environment": {...}, --> The environment impact
+ "application": {...}, --> The application impact
+ "owner": {...}, --> The owner impact
+ "findings": {...}, --> The findings impact
+ "score": {...} --> The total impact score
+ }
+}
+```
+
+### JSON-Short
+
+Shows the affected resource using it's ARN as the key and the findings affecting it as a list under the key `findings`. In adittion to the findings, you will also get the following keys: `ResourceType`, `Region`, `AwsAccountId`, `associations`, `config`, `tags`, `account`, `cloudtrail`, and `impact`.
+
+```json
+"arn:aws:ec2:eu-west-1:1234567890:instance/i-0a40b2be25dbac0ac": { --> The affected resource ARN
+ "findings": [ --> The findings affecting the resource
+ "EC2 instances should use Instance Metadata Service Version 2 (IMDSv2)", --> The finding title
+ "EC2 instances should be managed by AWS Systems Manager", --> The finding title
+ "EC2 instances should not have a public IPv4 address" --> The finding title
],
- "AwsAccountId": "obfuscated",
- "Region": "eu-west-1",
- "ResourceType": "AwsSageMakerNotebookInstance",
- "config: {},
- "associations: {},
- "tags: {},
- "cloudtrail: {},
- "account: {}
- "impact: {}
-},
+ "ResourceType": "AwsEc2Instance", --> The affected resource type
+ "Region": "eu-west-1", --> The affected resource region
+ "AwsAccountId": "1234567890", --> The affected resource account id
+ "associations": { --> The associations of the affected resource
+ "security_groups": {}, --> The security groups associated with the affected resource
+ "iam_roles": {}, --> The IAM roles associated with the affected resource
+ "volumes": {}, --> The volumes associated with the affected resource
+ "autoscaling_groups": {}, --> The autoscaling groups associated with the affected resource
+ "vpcs": {}, --> The VPCs associated with the affected resource
+ "subnets": {}, --> The subnets associated with the affected resource
+ },
+ "config": { --> The configuration of the affected resource (based on it's type)
+ "public_ip": "200.200.200.200", --> The public IP of the affected resource, if any
+ "private_ip": "10.10.10.10", --> The private IP of the affected resource, if any
+ "key": "ssh-key", --> The key used for the affected resource, if any
+ "metadata_options": { --> The metadata options of the affected resource, if any
+ "State": "applied",
+ "HttpTokens": "required",
+ "HttpPutResponseHopLimit": 1,
+ "HttpEndpoint": "enabled",
+ "HttpProtocolIpv6": "disabled",
+ "InstanceMetadataTags": "disabled"
+ },
+ },
+ "tags": { --> The tags of the affected resource
+ "Name": "test", --> The tag key and value
+ "Env": "prod",
+ "awsApplication": "arn:aws:resource-groups:eu-west-1:1234567890:group/app1/0c8vpbjkzeeffsz2cqgxpae7b2"
+ },
+ "account": { --> The account of the affected resource
+ "Alias": "prod", --> The account alias
+ "AlternateContact": {}, --> The alternate contact of the account, if any
+ "Organizations": { --> The organization of the account, if any
+ "Id": "o-1234567890",
+ "Arn": "arn:aws:organizations::1234567890:organization/o-1234567890/o-1234567890",
+ "MasterAccountId": "1234567890",
+ "MasterAccountArn": "arn:aws:organizations::1234567890:account/o-1234567890/1234567890",
+ "MasterAccountEmail": "",
+ "Details": {
+ "ParentId": "p-k1234567890",
+ "ParentType": "ROOT",
+ "OU": "ROOT",
+ "Policies": { --> The policies of the account, if any
+ "p-FullAWSAccess": {...} --> The policy name and policy
+ }
+ }
+ },
+ },
+ "cloudtrail": { --> The CloudTrail events affecting the affected resource
+ "RunInstances": { --> The CloudTrail event name
+ "Username": "test", --> The username of the event, if any
+ "EventTime": "2021-01-01T00:00:00Z",
+ "EventId": "12345678-1234-1234-1234-123456789012",
+ }
+ },
+ "impact": { --> The impact of the affected resource
+ "exposure": {...}, --> The exposure impact
+ "access": {...}, --> The access impact
+ "encryption": {...}, --> The encryption impact
+ "status": {...}, --> The status impact
+ "environment": {...}, --> The environment impact
+ "application": {...}, --> The application impact
+ "owner": {...}, --> The owner impact
+ "findings": {...}, --> The findings impact
+ "score": {...} --> The total impact score
+ }
+}
```
### JSON-Inventory
Show a list of all resources with their ARN.
-```
+```json
[
"arn:aws:sagemaker:us-east-1:ofuscated:notebook-instance/obfuscated",
"arn:aws:sagemaker:eu-west-1:ofuscated:notebook-instance/obfuscated"
@@ -622,12 +1216,12 @@ Show a list of all resources with their ARN.
Show statistics for each field/value. In the output, you will see each field/value and the number of occurrences; for example, the following output shows statistics for six findings.
-```
+```json
{
"Title": {
"SageMaker.1 Amazon SageMaker notebook instances should not have direct internet access": 2,
"SageMaker.2 SageMaker notebook instances should be launched in a custom VPC": 2,
- "SageMaker.3 Users should not have root access to SageMaker notebook instances": 2,
+ "SageMaker.3 Users should not have root access to SageMaker notebook instances": 2
},
"SeverityLabel": {
"HIGH": 6
@@ -844,7 +1438,9 @@ Examples:
./metahub --sh-filters RecordState=ACTIVE WorkflowStatus=NEW ResourceType=AwsEc2SecurityGroup --mh-filters-tags Environment=Production
```
-# Updating Workflow Status
+# Security Hub Actions
+
+## Updating Workflow Status
You can use **MetaHub** to update your AWS Security Hub Findings workflow status (`NOTIFIED,` `NEW,` `RESOLVED,` `SUPPRESSED`) with a single command. You will use the `--update-findings` option to update all the findings from your MetaHub query. This means you can update one, ten, or thousands of findings using only one command. AWS Security Hub API is limited to 100 findings per update. Metahub will split your results into 100 items chucks to avoid this limitation and update your findings beside the amount.
@@ -866,7 +1462,7 @@ Running the following update command will update those six findings' workflow st
The `--update-findings` will ask you for confirmation before updating your findings. You can skip this confirmation by using the option `--no-actions-confirmation`.
-# Enriching Findings
+## Enriching Findings
You can use **MetaHub** to enrich back your AWS Security Hub Findings with Context outputs using the option `--enrich-findings`. Enriching your findings means updating them directly in AWS Security Hub. **MetaHub** uses the `UserDefinedFields` field for this.
@@ -884,124 +1480,6 @@ For example, you want to enrich all AWS Security Hub findings with `WorkflowStat
The `--enrich-findings` will ask you for confirmation before enriching your findings. You can skip this confirmation by using the option `--no-actions-confirmation`.
-# Findings Aggregation
-
-Working with Security Findings sometimes introduces the problem of Shadowing and Duplication.
-
-Shadowing is when two checks refer to the same issue, but one in a more generic way than the other one.
-
-Duplication is when you use more than one scanner and get the same problem from more than one.
-
-Think of a Security Group with port 3389/TCP open to 0.0.0.0/0. Let's use Security Hub findings as an example.
-
-If you are using one of the default Security Standards like `AWS-Foundational-Security-Best-Practices,` you will get two findings for the same issue:
-
-- `EC2.18 Security groups should only allow unrestricted incoming traffic for authorized ports`
-- `EC2.19 Security groups should not allow unrestricted access to ports with high risk`
-
-If you are also using the standard CIS AWS Foundations Benchmark, you will also get an extra finding:
-
-- `4.2 Ensure no security groups allow ingress from 0.0.0.0/0 to port 3389`
-
-Now, imagine that SG is not in use. In that case, Security Hub will show an additional fourth finding for your resource!
-
-- `EC2.22 Unused EC2 security groups should be removed`
-
-So now you have in your dashboard four findings for one resource!
-
-Suppose you are working with multi-account setups and many resources. In that case, this could result in many findings that refer to the same thing without adding any extra value to your analysis.
-
-**MetaHub** aggregates security findings under the affected resource.
-
-This is how MetaHub shows the previous example with output-mode json-short:
-
-```sh
-"arn:aws:ec2:eu-west-1:01234567890:security-group/sg-01234567890": {
- "findings": [
- "EC2.19 Security groups should not allow unrestricted access to ports with high risk",
- "EC2.18 Security groups should only allow unrestricted incoming traffic for authorized ports",
- "4.2 Ensure no security groups allow ingress from 0.0.0.0/0 to port 3389",
- "EC2.22 Unused EC2 security groups should be removed"
- ],
- "AwsAccountId": "01234567890",
- "Region": "eu-west-1",
- "ResourceType": "AwsEc2SecurityGroup"
-}
-```
-
-This is how MetaHub shows the previous example with output-mode json-full:
-
-```sh
-"arn:aws:ec2:eu-west-1:01234567890:security-group/sg-01234567890": {
- "findings": [
- {
- "EC2.19 Security groups should not allow unrestricted access to ports with high risk": {
- "SeverityLabel": "CRITICAL",
- "Workflow": {
- "Status": "NEW"
- },
- "RecordState": "ACTIVE",
- "Compliance": {
- "Status": "FAILED"
- },
- "Id": "arn:aws:security hub:eu-west-1:01234567890:subscription/aws-foundational-security-best-practices/v/1.0.0/EC2.22/finding/01234567890-1234-1234-1234-01234567890",
- "ProductArn": "arn:aws:security hub:eu-west-1::product/aws/security hub"
- }
- },
- {
- "EC2.18 Security groups should only allow unrestricted incoming traffic for authorized ports": {
- "SeverityLabel": "HIGH",
- "Workflow": {
- "Status": "NEW"
- },
- "RecordState": "ACTIVE",
- "Compliance": {
- "Status": "FAILED"
- },
- "Id": "arn:aws:security hub:eu-west-1:01234567890:subscription/aws-foundational-security-best-practices/v/1.0.0/EC2.22/finding/01234567890-1234-1234-1234-01234567890",
- "ProductArn": "arn:aws:security hub:eu-west-1::product/aws/security hub"
- }
- },
- {
- "4.2 Ensure no security groups allow ingress from 0.0.0.0/0 to port 3389": {
- "SeverityLabel": "HIGH",
- "Workflow": {
- "Status": "NEW"
- },
- "RecordState": "ACTIVE",
- "Compliance": {
- "Status": "FAILED"
- },
- "Id": "arn:aws:security hub:eu-west-1:01234567890:subscription/aws-foundational-security-best-practices/v/1.0.0/EC2.22/finding/01234567890-1234-1234-1234-01234567890",
- "ProductArn": "arn:aws:security hub:eu-west-1::product/aws/security hub"
- }
- },
- {
- "EC2.22 Unused EC2 security groups should be removed": {
- "SeverityLabel": "MEDIUM",
- "Workflow": {
- "Status": "NEW"
- },
- "RecordState": "ACTIVE",
- "Compliance": {
- "Status": "FAILED"
- },
- "Id": "arn:aws:security hub:eu-west-1:01234567890:subscription/aws-foundational-security-best-practices/v/1.0.0/EC2.22/finding/01234567890-1234-1234-1234-01234567890",
- "ProductArn": "arn:aws:security hub:eu-west-1::product/aws/security hub"
- }
- }
- ],
- "AwsAccountId": "01234567890",
- "AwsAccountAlias": "obfuscated",
- "Region": "eu-west-1",
- "ResourceType": "AwsEc2SecurityGroup"
-}
-```
-
-Your findings are combined under the ARN of the resource affected, ending in only one result or one non-compliant resource.
-
-You can now work in MetaHub with all these four findings together as if they were only one. For example, you can update these four Workflow Status findings using only one command: See [Updating Workflow Status](#updating-workflow-status)
-
# Contributing
You can follow this guide if you want to contribute to the Context module [guide](docs/context.md).
diff --git a/docs/context.md b/docs/context.md
index e0f3fc3..8478c7b 100644
--- a/docs/context.md
+++ b/docs/context.md
@@ -1,71 +1,108 @@
-# Context Development
+# Context Module
-The ResourceType defines the MetaChecks to be executed. When there is an AWS Security Hub finding for an S3 Bucket (ResourceType: AwsS3Bucket), all the MetaChecks available for that resource will execute and be added as extra information under the ARNs resource.
+The context module has 4 main components: config, tags, account and cloudtrail.
-- [How it works](#how-it-works)
-- [Adding a new AWS ResourceType for MetaChecks](#adding-a-new-aws-resourcetype-for-metachecks)
-- [Creating MetaChecks](#creating-metachecks)
+The config component is responsible for fetching the configuration and the associated resources. This configuration and associated resources are defined by resource type, under `lib/context/resources` you will find a file for each resource type.
-## How it works
+## Adding a new AWS ResourceType
-Context works this way:
+If you want to add context for a ResourceType that has not yet been defined in MetaHub, you will first need to add the ResourceType as a Class:
-1. Connect to the account where the resource lives assuming the provided role (`--mh-assume-role`)
-2. Describe the resource using describe functions
-3. Executes MetaChecks on top of the described resource
-4. Add the MetaChecks output to your affected resources
-5. Apply filters if provided (`--mh-filters-checks`)
-6. Output the list of affected resources with MetaChecks outputs that matchs your filters
+1. Create a new file under `lib/context/resources` with the ResourceType as name, for example `AwsS3Bucket.py`
-## Adding a new AWS ResourceType for MetaChecks
+2. Start with this template as a base. We are using a base Class MetaChecksBase for every ResourceType.
-If you want to add MetaChecks for a ResourceType that has not yet been defined in MetaHub, you will first need to add the ResourceType as a Class:
+```python
+"""ResourceType: Name of the ResourceType"""
-1. Create a new file under `metachecks/checks` with the ResourceType as name, for example `AwsS3Bucket.py`
-
-2. Start with this template as a base. We are using a base Class (MetaChecksBase) to provide the filtering functionality.
-
-```
-'''MetaCheck: '''
+from botocore.exceptions import ClientError
from lib.AwsHelpers import get_boto3_client
-from lib.metachecks.checks.Base import MetaChecksBase
-from lib.context.resources.MetaChecksHelpers import IamHelper
+from lib.context.resources.Base import ContextBase
-class Metacheck(MetaChecksBase):
+class Metacheck(ContextBase):
def __init__(
self,
logger,
finding,
- metachecks,
- mh_filters_checks,
+ mh_filters_config,
sess,
drilled=False,
):
self.logger = logger
- if metachecks:
- self.region = finding["Region"]
- self.account = finding["AwsAccountId"]
- self.partition = finding["Resources"][0]["Id"].split(":")[1]
- self.finding = finding
- self.sess = sess
- self.resource_arn = finding["Resources"][0]["Id"]
- self.resource_id = finding["Resources"][0]["Id"].split("/")[1]
- self.mh_filters_checks = mh_filters_checks
- self.client = get_boto3_client(self.logger, "ec2", self.region, self.sess)
+ self.sess = sess
+ self.mh_filters_config = mh_filters_config
+ self.parse_finding(finding, drilled)
+ self.client = get_boto3_client(self.logger, "SERVICE", self.region, self.sess) --> YOUR BOTO3 CLIENT
+ # Describe Resource
+ self.RESOURCE_TYPE = self.describe_RESOURCE_TYPE() --> You will need a describe function for your resource type
+ if not self.RESOURCE_TYPE: --> Handling if the resource does not exist
+ return False
+ # Drilled Associations
+ self.iam_roles = self._describe_instance_iam_roles() --> Add your associations, needs to be a dictionary {"arn": {}}
+
+ # Parse --> How to parse the resource id from the ARN
+ def parse_finding(self, finding, drilled):
+ self.finding = finding
+ self.region = finding["Region"]
+ self.account = finding["AwsAccountId"]
+ self.partition = finding["Resources"][0]["Id"].split(":")[1]
+ self.resource_type = finding["Resources"][0]["Type"]
+ self.resource_id = ( --> When the resource is drilled, it get's the arn as drilled
+ finding["Resources"][0]["Id"].split("/")[-1]
+ if not drilled
+ else drilled.split("/")[-1]
+ )
+ self.resource_arn = finding["Resources"][0]["Id"] if not drilled else drilled --> When the resource is drilled, it get's the arn as drilled
+
+ # Describe Functions
+
+ def describe_RESOURCE_TYPE(self): --> Describe function for your resource type
+ try:
+ response = self.client.describe_instances(
+ InstanceIds=[
+ self.resource_id,
+ ],
+ Filters=[
+ {
+ "Name": "instance-state-name",
+ "Values": [
+ "pending",
+ "running",
+ "shutting-down",
+ "stopping",
+ "stopped",
+ ],
+ }
+ ],
)
+ if response["Reservations"]:
+ return response["Reservations"][0]["Instances"][0]
+ except ClientError as err:
+ if not err.response["Error"]["Code"] == "InvalidInstanceID.NotFound":
+ self.logger.error(
+ "Failed to describe_instance: {}, {}".format(self.resource_id, err)
+ )
+ return False
+
+ # Context Config
+
+
+ def associations(self):
+ associations = {} --> The associations
+ return associations
def checks(self):
- checks = [
- ]
+ checks = {} --> The config checks
return checks
+
```
-3. Define _describe functions_ for the ResourceType. These functions will fetch the information you need to then create checks on top of it. For example, if you want to check if an S3 bucket has a public ACL, you first describe the ACLS and then create a function to check if those ACLS are public. This way, you can re-use the describe output for any necessary check. Describe functions in MetaHub are named starting with a `_` as a naming convention. These describe functions will be then be class attributes.
+3. Define as many describe functions for the ResourceType you need. These functions will fetch the information you need to then create config checks on top of it.
-```
-def _get_bucket_acl(self):
+```python
+def get_bucket_acl(self):
try:
response = self.client.get_bucket_acl(Bucket=self.resource_id)
except ClientError as err:
@@ -74,96 +111,30 @@ def _get_bucket_acl(self):
return response["Grants"]
```
-4. Define an attribute for your describe function, in the previous example, we created a function to describe the ACLs (`_get_bucket_acl`) so we will call this attribute `bucket_acl`
-
-```
-'''MetaCheck: '''
-
-from lib.AwsHelpers import get_boto3_client
-from lib.metachecks.checks.Base import MetaChecksBase
-from lib.context.resources.MetaChecksHelpers import IamHelper
+4. Define config check functions to add keys to the config key, and add those functions to the checks function.
+```python
+def public_dns(self):
+ public_dns = False
+ if self.instance:
+ public_dns = self.instance.get("PublicDnsName")
+ return public_dns
-class Metacheck(MetaChecksBase):
- def __init__(
- self,
- logger,
- finding,
- metachecks,
- mh_filters_checks,
- sess,
- drilled=False,
- ):
- self.logger = logger
- if metachecks:
- self.region = finding["Region"]
- self.account = finding["AwsAccountId"]
- self.partition = finding["Resources"][0]["Id"].split(":")[1]
- self.finding = finding
- self.sess = sess
- self.resource_arn = finding["Resources"][0]["Id"]
- self.resource_id = finding["Resources"][0]["Id"].split("/")[1]
- self.mh_filters_checks = mh_filters_checks
- self.client = get_boto3_client(self.logger, "ec2", self.region, self.sess)
- )
- # Describe functions
- self.bucket_acl = self._get_bucket_acl() --> YOUR DESCRIBE FUNCTION AS AN ATTRIBUTE
-
- def checks(self):
- checks = [
- ]
- return checks
+def checks(self):
+ checks = {
+ "public_dns": self.public_dns(),
+ }
+ return checks
```
-5. Import Metacheck in metachecks/checks/**init**.py file
-
-## Creating MetaChecks
-
-You can code any check you need on top of the data fetched by the _describe functions_.
-
-A MetaCheck should be defined as a yes/no question; when the answer is yes, we can add extra information. When it is no, we can return False or empty data ("", [], {}). For example, if we check if an S3 ACL is public, we can return the permissions that make that ACL public, like READ or FULL_CONTROL.
-When filtering using Meta Checks, we evaluate True as True and True if we return data. So you can output extra information for your resources this way and then integrate it with other tools. As another example, if you are checking a Security Group for unrestrictive open ports, you can output which ports are open and then use that to integrate with Nmap for scanning.
+5. Add the associated resources to the associations function.
+```python
+def associations(self):
+ associations = {
+ "iam_roles": self.iam_roles,
+ }
+ return associations
```
-def is_bucket_acl_public(self):
- public_acls = []
- if self.bucket_acl:
- for grant in self.bucket_acl:
- if grant["Grantee"]["Type"] == "Group":
- who = grant["Grantee"]["URI"].split("/")[-1]
- if who == "AllUsers" or who == "AuthenticatedUsers":
- perm = grant["Permission"]
- public_acls.append(perm)
- if public_acls:
- return public_acls
- return False
-```
-
-This function will return the permissions allowed to public (like FULL_CONTROL or READ) or will return False if it's not public.
-```
-def it_has_bucket_acl_with_cross_account(self):
- acl_with_cross_account = []
- if self.bucket_acl:
- for grant in self.bucket_acl:
- if grant["Grantee"]["Type"] == "CanonicalUser":
- if grant["Grantee"]["ID"] != self.cannonical_user_id:
- perm = grant["Permission"]
- acl_with_cross_account.append(perm)
- if acl_with_cross_account:
- return acl_with_cross_account
- return False
-```
-
-This function will return the permissions that were granted to other accounts (like FULL_CONTROL or READ) or will return False if it was not granted to other accounts.
-
-To enable the check, add it to the list in the fuction `checks` of your `ResourceType`.
-
-```
- def checks(self):
- checks = [
- "it_has_bucket_acl_with_cross_account",
- "is_bucket_acl_public"
- ]
- return checks
-```
+4. Import Metacheck in lib/resources/**init**.py file
diff --git a/docs/imgs/impact.png b/docs/imgs/impact.png
new file mode 100644
index 0000000000000000000000000000000000000000..7bfa62be69e5974a24712cc45f7ce199a0683e04
GIT binary patch
literal 67019
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