Skip to content

Commit

Permalink
chore: initial 0.0.1 release and workflows
Browse files Browse the repository at this point in the history
  • Loading branch information
js-timbirkett committed Jan 12, 2022
1 parent 7f39f95 commit 14afafd
Show file tree
Hide file tree
Showing 15 changed files with 539 additions and 0 deletions.
44 changes: 44 additions & 0 deletions .github/workflows/build-and-push.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Publish Docker image
on:
release:
types: [published]
jobs:
push_to_registry:
name: Push Docker image to GitHub Packages
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- name: Check out the repo
uses: actions/checkout@v2
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
# list of Docker images to use as base name for tags
images: |
ghcr.io/${{ github.repository_owner }}/prometheus-inspector-exporter
# generate Docker tags based on the following events/attributes
tags: |
type=schedule
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
- name: Log in to GitHub Container Registry
uses: docker/login-action@v1
if: github.event_name != 'pull_request'
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
71 changes: 71 additions & 0 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"

on:
push:
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '29 2 * * 6'

jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write

strategy:
fail-fast: false
matrix:
language: [ 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed

steps:
- name: Checkout repository
uses: actions/checkout@v2

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main

# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1

# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl

# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language

#- run: |
# make bootstrap
# make release

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
34 changes: 34 additions & 0 deletions .github/workflows/test-and-lint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Test and Lint
on:
push:
branches:
- main
pull_request:
branches:
- main
schedule:
- cron: '30 11 * * *'
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.5, 3.6, 3.7, 3.8, 3.9]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install tox tox-gh-actions
- name: Test with Tox
run: |
tox
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: psf/black@stable
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @pysysops @js-timbirkett
24 changes: 24 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
FROM alpine:3

# Installing required packages
RUN apk add --update --no-cache \
python3~=3.9 py-pip

# Install app code
RUN mkdir /app
ADD ./setup.* README.md /app/
ADD inspector_exporter /app/inspector_exporter
ADD tests /app/tests

# Install app deps
RUN cd /app && pip install -e .

# Run as non-root
RUN adduser app -S -u 1000
USER app

# Switch the cwd to /app so that running app and tests is easier
WORKDIR /app

ENV AWS_DEFAULT_REGION eu-west-1
CMD [ "inspector_exporter" ]
2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
include *.py
include README.md
62 changes: 62 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,64 @@
[![CodeQL](https://github.com/aws-exporters/inspector/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/aws-exporters/inspector/actions/workflows/codeql-analysis.yml)
[![Test and Lint](https://github.com/aws-exporters/inspector/actions/workflows/test-and-lint.yaml/badge.svg)](https://github.com/aws-exporters/inspector/actions/workflows/test-and-lint.yaml)
![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/aws-exporters/inspector)
![GitHub](https://img.shields.io/github/license/aws-exporters/inspector)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org)

# inspector
A Prometheus exporter for AWS Inspector

Current Status:
- [x] AWS_ECR_CONTAINER
- [ ] AMI
- [ ] AWS_EC2_INSTANCE
- [ ] PACKAGE_VULNERABILITY
- [ ] NETWORK_REACHABILITY

## Motivation
Inspector has a lot of useful information: scan results, EC2 instance
vulnerabilities, networking issues and AMI scan results.

Information that might be useful to display on team based dashboards alongside
Kubernetes workload availability and Istio traffic metrics.

## Technical Design
This exporter makes use of `boto3` to query Inspector2 for finding aggregations.

To be kind to the AWS APIs, results are cached and refreshed in the background every
30 minutes (by default).

### Configuration
Configuration with environment variables:

| Variable | Description | Default | Example |
| -------- | ----------- | ------- | ------- |
| `APP_PORT` | The port to expose the exporter on | `9000` | `8080` |
| `APP_HOST` | The host to bind the application to | `0.0.0.0` | `localhost` |
| `CACHE_REFRESH_INTERVAL` | How many seconds to wait before refreshing caches in the background | `1800` | `3600` |
| `AWS_ACCOUNT_ID` | The ID of the AWS account to export metrics for | `current AWS account` | `112233445566` |
| `LOG_LEVEL` | How much or little logging do you want | `INFO` | `DEBUG` |

### Exported Metrics
The metrics currently exported are:

#### `aws_inspector_container_image_severity_count`
- **Type:** Gauge
- **Description:** Scan result counts per image/tag/by severity. The labels are
almost identical to those exposed by the ECR exporter making moving from one to
the other as simple as possible.
- **Example:**
```
# HELP aws_inspector_container_image_severity_count ECR image scan summary results
# TYPE aws_inspector_container_image_severity_count gauge
....
aws_inspector_container_image_severity_count{digest="sha256:0eb66119edb5484e846acb68ce60b02fb69aed204fd3dedb5277f8add881fcdb",image="112233445566.dkr.ecr.eu-west-1.amazonaws.com/robopig:e34c1c8f",name="robopig",registry_id="112233445566",severity="CRITICAL",tag="e34c1c8f"} 0.0
aws_inspector_container_image_severity_count{digest="sha256:0eb66119edb5484e846acb68ce60b02fb69aed204fd3dedb5277f8add881fcdb",image="112233445566.dkr.ecr.eu-west-1.amazonaws.com/robopig:e34c1c8f",name="robopig",registry_id="112233445566",severity="HIGH",tag="e34c1c8f"} 4.0
aws_inspector_container_image_severity_count{digest="sha256:0eb66119edb5484e846acb68ce60b02fb69aed204fd3dedb5277f8add881fcdb",image="112233445566.dkr.ecr.eu-west-1.amazonaws.com/robopig:e34c1c8f",name="robopig",registry_id="112233445566",severity="MEDIUM",tag="e34c1c8f"} 3.0
aws_inspector_container_image_severity_count{digest="sha256:0eb66119edb5484e846acb68ce60b02fb69aed204fd3dedb5277f8add881fcdb",image="112233445566.dkr.ecr.eu-west-1.amazonaws.com/robopig:e34c1c8f",name="robopig",registry_id="112233445566",severity="LOW",tag="e34c1c8f"} 3.0
aws_inspector_container_image_severity_count{digest="sha256:81de8eb8dfcb38c28d6ca0a8e4c9ad27bedc72e523f96d93c7cc365e62be5147",image="112233445566.dkr.ecr.eu-west-1.amazonaws.com/monkeytail:2b4692ee",name="monkeytail",registry_id="112233445566",severity="CRITICAL",tag="2b4692ee"} 3.0
aws_inspector_container_image_severity_count{digest="sha256:81de8eb8dfcb38c28d6ca0a8e4c9ad27bedc72e523f96d93c7cc365e62be5147",image="112233445566.dkr.ecr.eu-west-1.amazonaws.com/monkeytail:2b4692ee",name="monkeytail",registry_id="112233445566",severity="HIGH",tag="2b4692ee"} 6.0
aws_inspector_container_image_severity_count{digest="sha256:81de8eb8dfcb38c28d6ca0a8e4c9ad27bedc72e523f96d93c7cc365e62be5147",image="112233445566.dkr.ecr.eu-west-1.amazonaws.com/monkeytail:2b4692ee",name="monkeytail",registry_id="112233445566",severity="MEDIUM",tag="2b4692ee"} 15.0
aws_inspector_container_image_severity_count{digest="sha256:81de8eb8dfcb38c28d6ca0a8e4c9ad27bedc72e523f96d93c7cc365e62be5147",image="112233445566.dkr.ecr.eu-west-1.amazonaws.com/monkeytail:2b4692ee",name="monkeytail",registry_id="112233445566",severity="LOW",tag="2b4692ee"} 4.0
....
```
Empty file added inspector_exporter/__init__.py
Empty file.
126 changes: 126 additions & 0 deletions inspector_exporter/collector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import boto3
import botocore
import logging
import json

from prometheus_client.core import InfoMetricFamily, GaugeMetricFamily
from cachetools import TTLCache
from datetime import timezone


def _inspector_client():
boto_config = botocore.client.Config(
connect_timeout=2, read_timeout=10, retries={"max_attempts": 2}
)
session = boto3.session.Session()
return session.client("inspector2", config=boto_config)


class InspectorMetricsCollector:
def __init__(self, account_id):
self.logger = logging.getLogger()
self.account_id = account_id or boto3.client("sts").get_caller_identity().get(
"Account"
)
self.imagecache = TTLCache(10000, 86400)

def collect(self):

image_common_label_keys = ["name", "tag", "digest", "registry_id", "image"]

container_image_findings = GaugeMetricFamily(
"aws_inspector_container_image_severity_count",
"ECR image scan summary results",
labels=image_common_label_keys + ["severity"],
)

for repo in self.imagecache:
images = self.imagecache.get(repo, [])

for image in images:
tags = image.get("imageTags")
severity_counts = image["severityCounts"]
if tags:
for tag in tags:
image_common_label_values = [
image["repositoryName"],
tag,
image["imageDigest"],
image["accountId"],
f'{image["repositoryUri"]}:{tag}',
]
for severity in severity_counts:
container_image_findings.add_metric(
image_common_label_values + [severity],
int(severity_counts[severity]),
)

return [
container_image_findings,
]

def refresh_image_cache(self):
inspector_client = _inspector_client()
self.logger.info("refreshing image cache")

paginator = inspector_client.get_paginator("list_finding_aggregations")

image_iterator = paginator.paginate(
accountIds=[
{
"comparison": "EQUALS",
"value": self.account_id,
}
],
aggregationType="AWS_ECR_CONTAINER",
PaginationConfig={"pageSize": 1000},
)

image_findings = [
image for x in list(image_iterator) for image in x["responses"]
]

for image_finding in image_findings:
repositoryName = image_finding["awsEcrContainerAggregation"]["repository"]

image_to_cache = {
"repositoryName": repositoryName,
"accountId": image_finding["awsEcrContainerAggregation"]["accountId"],
"imageTags": image_finding["awsEcrContainerAggregation"]["imageTags"],
"imageDigest": image_finding["awsEcrContainerAggregation"]["imageSha"],
"repositoryUri": self.get_repo_uri(
image_finding["awsEcrContainerAggregation"]["resourceId"],
repositoryName,
),
"severityCounts": self.format_severity_counts(
image_finding["awsEcrContainerAggregation"]["severityCounts"]
),
}

if self.imagecache.get(repositoryName, None) is None:
self.imagecache[repositoryName] = [image_to_cache.copy()]
else:
self.imagecache[repositoryName].append(image_to_cache.copy())

def refresh_caches(self):
self.refresh_image_cache()
self.logger.info("cache refresh complete")

@staticmethod
def get_repo_uri(resource_id, repo_name):
id_parts = resource_id.split(":")
account_id = id_parts[4]
region = id_parts[3]
return f"{account_id}.dkr.ecr.{region}.amazonaws.com/{repo_name}"

@staticmethod
def format_severity_counts(severity_counts):
return {
"CRITICAL": severity_counts["critical"],
"HIGH": severity_counts["high"],
"MEDIUM": severity_counts["medium"],
"LOW": severity_counts["all"]
- severity_counts["critical"]
- severity_counts["high"]
- severity_counts["medium"],
}
Loading

0 comments on commit 14afafd

Please sign in to comment.