Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add code and tests files #1

Merged
merged 3 commits into from
Sep 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[flake8]
max_line_length = 88
46 changes: 46 additions & 0 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
name: Docker Registry Scanner Main CI/CD Pipelines

on:
push:
branches: [main]
paths-ignore: [README.md]
pull_request:
branches: [main]
paths-ignore: [README.md]

defaults:
run:
shell: bash

permissions: read-all

jobs:
syntax:
name: Syntax
uses: ./.github/workflows/syntax.yaml

tests:
name: Tests
needs: [syntax]
uses: ./.github/workflows/tests.yaml
with:
coverage_min: '95'

tag_release:
name: Create Tag & Release
needs: [tests]
permissions:
contents: write
uses: ./.github/workflows/tag_release.yaml
with:
dry_run: |-
${{
github.event_name == 'pull_request' ||
(
github.event_name == 'push' &&
github.ref != 'refs/heads/main'
)
}}
secrets:
gh_token: ${{ secrets.GITHUB_TOKEN }}
36 changes: 36 additions & 0 deletions .github/workflows/syntax.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
name: Docker Registry Scanner Syntax Checks

on:
workflow_call:

permissions: read-all

jobs:
syntax:
name: Syntax
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0

- name: Commitlint
uses: wagoid/commitlint-github-action@v5
with:
failOnWarnings: true

- name: Install YAMLlint, PyLint, Flake8 and Black
run: python -m pip install --user pylint flake8 black yamllint

- name: Run YAMLlint
run: yamllint --strict .

- name: Run PyLint
run: python -m pylint --recursive yes .

- name: Run Flake8
run: python -m flake8 .

- name: Run Black
run: python -m black --diff --check --color .
51 changes: 51 additions & 0 deletions .github/workflows/tag_release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
name: Docker Registry Scanner Release And Tag

on:
workflow_call:
inputs:
dry_run:
description: Dry-run for creating tag and release
required: true
type: boolean
secrets:
gh_token:
description: GitHub token
required: true
outputs:
new_version:
description: New tag version generated using conventional commit
value: ${{ jobs.tag.outputs.new_version }}

permissions:
contents: write

jobs:
tag:
name: Tag
runs-on: ubuntu-latest
outputs:
changelog: ${{ steps.tag_version.outputs.changelog }}
new_tag: ${{ steps.tag_version.outputs.new_tag }}
new_version: ${{ steps.tag_version.outputs.new_version }}
steps:
- name: Bump Version & Tag
id: tag_version
uses: mathieudutour/[email protected]
with:
default_bump: false
dry_run: ${{ inputs.dry_run }}
github_token: ${{ secrets.gh_token }}

release:
if: ${{ !inputs.dry_run && needs.tag.outputs.new_tag != '' }}
name: Release
runs-on: ubuntu-latest
needs: [tag]
steps:
- name: GitHub Release
uses: ncipollo/release-action@v1
with:
tag: ${{ needs.tag.outputs.new_tag }}
name: Release ${{ needs.tag.outputs.new_tag }}
body: ${{ needs.tag.outputs.changelog }}
49 changes: 49 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
name: Docker Registry Scanner Tests

on:
workflow_call:
inputs:
coverage_min:
description: Minimum code coverage
required: true
type: string

permissions: read-all

jobs:
tests:
name: Tests
runs-on: ubuntu-latest

steps:
- name: Checkout Code
uses: actions/checkout@v3

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.11

- name: Install Pytest And Coverage
run: python -m pip install pytest coverage

- name: Run Unit Testing
env:
PYTHONPATH: src/
run: python -m coverage run -m pytest -v tests/

- name: Check Coverage
id: coverage
continue-on-error: true
run: coverage report -m --fail-under ${{ inputs.coverage_min }} --omit 'tests/*'

- name: Coverage Result
run: |
if [ "${{ steps.coverage.outcome }}" == "failure" ]
then
echo "::warning::BNPPF code coverage NOK 💀 (<${{ inputs.coverage_min }}%)"
exit 1
else
echo "::notice::BNPPF code coverage OK 🎉 (>${{ inputs.coverage_min }}%)"
fi
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
certs/
!tests/certs/
.coverage
__pycache__
.pytest_cache
scan_results_report.json
2 changes: 2 additions & 0 deletions .pylintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[pylint]
disable = import-error
7 changes: 7 additions & 0 deletions .yamllint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
extends: default
rules:
line-length:
max: 100
truthy:
check-keys: false
46 changes: 46 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
ARG PYTHON_VERSION="3.11"

FROM debian:12-slim AS gpg-dearmor

COPY pubkeys/trivy.key /tmp

# DL3008 warning: Pin versions in apt get install
# hadolint ignore=DL3008
RUN apt-get update \
&& apt-get install --no-install-recommends -y gnupg \
&& apt-get upgrade -y \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* \
&& gpg --dearmor > /tmp/trivy.gpg < /tmp/trivy.key

FROM python:"${PYTHON_VERSION}-slim"

LABEL org.opencontainers.image.authors="Jonathan Sabbe <[email protected]>"

WORKDIR /app

COPY src/ .
COPY apt/trivy.sources /etc/apt/sources.list.d/
COPY --from=gpg-dearmor /tmp/trivy.gpg /etc/apt/trusted.gpg.d/trivy.gpg

ARG TRIVY_VERSION="0.45.1"

# DL3008 warning: Pin versions in apt get install
# hadolint ignore=DL3008
RUN apt-get update \
&& apt-get install --no-install-recommends -y git trivy=$TRIVY_VERSION \
&& apt-get upgrade -y \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* \
&& adduser --disabled-password --comment "Docker Scanner" scanner \
&& chown -R scanner:scanner . \
&& pip uninstall -y setuptools

USER scanner

RUN git config --global --add safe.directory "${PWD}"

ENV TZ="Europe/Brussels" \
PYTHONUNBUFFERED="1"

ENTRYPOINT [ "python", "main.py" ]
76 changes: 76 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Docker Registry Scanner

## Description

The goal of this application is to scan Docker images stored on a [registry](https://github.com/docker) (for now only registry made by [Docker Inc.](https://github.com/docker)).

Scan is done using Trivy security scanner.

Docker Registry Scanner application writes status of images as standard output and scan results report JSON file (defaults: `./scan_results_report.json`) is created at the end of the run.

**Example**

```shell
Scanning Docker image 'localhost:443/my-poc:latest'...
🔴 NOK localhost:443/my-poc:latest ({'HIGH': 1, 'CRITICAL': 0})
Scanning Docker image 'localhost:443/semver-bumper:08dc6233'...
Scanning Docker image 'localhost:443/semver-bumper:latest'...
🔴 NOK localhost:443/semver-bumper:08dc6233 ({'HIGH': 1, 'CRITICAL': 0})
🟢 OK localhost:443/semver-bumper:latest
Scanning Docker image 'localhost:443/ubuntu:latest'...
🟢 OK localhost:443/ubuntu:latest
```

## Configuration

### Environment Variables

- `LOGGING_LEVEL`: (Optional) Logging level needed. Can be `DEBUG`, `INFO`, `WARNING` or `CRITICAL`. (Default: `INFO`)
- `DOCKER_REGISTRY_URL`: **(Required)** Docker registry **HTTPS** URL that needs to be scanned. (e.g. `https://docker-registry.example.com:12345/path/to/repository/`)
- `DOCKER_REGISTRY_CA_FILE`: (Optional) PEM format file of CA.
- `DOCKER_IMAGES_FILTER`: (Optional) REGEX pattern used to filter Docker images. (Default: `.*`)
- `DOCKER_TAGS_FILTER`: (Optional) REGEX pattern used to filter Docker image tags. (Default: `.*`)
- `IMAGE_LIST_NBR_MAX`: (Optional) Maximum number of Docker images that needs to be fetch from Docker registry. (Default: `1000`)
- `HTTPS_CONNECTION_TIMEOUT`: (Optional) Docker registry client HTTPS connection timeout. (Default: `3`)
- `SCAN_SEVERITY`: (DEPRECATED) Scanner severity configuration. Deprecated in favor of `SCAN_MIN_SEVERITY`. (Default: `HIGH,CRITICAL`)
- `SCAN_MIN_SEVERITY`: (Optional) Scanner minimum severity threshold. Can be `UNKNOWN`, `LOW`, `MEDIUM`, `HIGH` or `CRITICAL`. (Defaut: `HIGH`)
- `SCAN_RESULTS_REPORT_FILE`: (Optional) Scanner results report file. (Default: `./scan_results_report.json`)
- `SCAN_SCANNERS`: (Optional) Scanner scan types to do. (Default: `vuln,secret`)
- `MULTIPROCESSING_PROCESSES`: (Optional): Process in parallel used to scan Docker images. (Default: `5`)


## Docker

### Build

```shell
docker build -t docker-registry-scanner .
```

### Run

**Examples**

```shell
# Minimum required
docker run \
--rm \
-e DOCKER_REGISTRY_URL="https://docker-registry.example.com:12345" \
docker-registry-scanner

# Filter Docker images, minimum scan severity 'LOW' and logging level 'DEBUG'
docker run \
--rm \
-e DOCKER_REGISTRY_URL="https://docker-registry.example.com:12345" \
-e DOCKER_IMAGES_FILTER='^release/docker/internal/speos/$' \
-e SCAN_MIN_SEVERITY="LOW" \
-e LOGGING_LEVEL="DEBUG" \
docker-registry-scanner

# HTTPS Proxy to reach Trivy databases URLs
docker run \
--rm \
-e DOCKER_REGISTRY_URL="https://docker-registry.example.com:12345" \
-e https_proxy="http://proxy.example.com:7890" \
docker-registry-scanner
```
5 changes: 5 additions & 0 deletions apt/trivy.sources
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Types: deb
URIs: https://aquasecurity.github.io/trivy-repo/deb
Suites: bookworm
Components: main
Signed-By: /etc/apt/trusted.gpg.d/trivy.gpg
23 changes: 23 additions & 0 deletions pubkeys/trivy.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----

mQGNBFzP66QBDAC+sYojv+2L2wU7ZOjekt741t/Hz9mAR6rZfsFE8eOdK6Ozav2t
l13QKpqSTUwQ33mQEwudEIUsZOov2tsBaG4r9Kxasu7wmeXoenvLWt92K6ZfcFwq
THti2Xcku6T1X+JXu4RAHf2wEreVY+Os8BnXMuIp4KWbFhVcLloeiMMZXUgSDHGM
pPXfvkF0D4AewSOlnlf8we7+AVX6ZA9tHyb2KVO40f3mgfwMA110B5xGRY5JU+hI
zouwwRb8xPX7cRf4BKaLKcMGzCEL+Ks7MUGCN7pKtIPkxDC1EyHk0dVOKKm2dl0W
kaZNTD+UoJjUTgkT/y+EJC2/nXMCNwdloGo+L9Ue8baRMku6PQEakmdh6j+y5Hwo
KafrJDK+Li+pnibN8TruEjIiJKbZWRWIiQ9Z3zY3O1KmrWilfFywICn9kyP9d1Rb
SCGx495cGzXl1RNB4k1j3oUOINL2A/5jIyh/e27/aNoqGad3xTMMdr2VmmxenTBz
xsJMXJViVD4CPacAEQEAAbQFdHJpdnmJAdQEEwEIAD4WIQQuLTVnRhYyyEu2zW/p
0KNhYnb6bAUCXM/rpAIbAwUJEswDAAULCQgHAgYVCAkKCwIEFgIDAQIeAQIXgAAK
CRDp0KNhYnb6bFBmDACtd1no/JcuBKDuntn0DCLw+j0A+v64GIXHJhyt2fxYhIG/
WwTkkZ27glIbOW8EvsDoa13Ec7slPCTQI9WYn9hXJcMOXSnHyIWaVguIM2ouSYZK
H6N2g1lA+Fh3vjpat63Lysp1hpOYQckM37x6N2M/H/kfeLnMMo9e7tcjHsO07/2t
RJorOzMZR7zZmNR+bhh58caes7IRbhihp6dMEZBacQ8qcHlbFFEmyVt38nXreSgi
b4BuXgtOfFpZftUZX5eF7xhTYdIKOKKAn3pTpVdCP6UAseVtGwMUFUpfz6sXrPnc
Vvm6edFMOX+LkZ0SH6GT4XGqcFZNleuODL+kzTkqqmZ+M0tEeauprlBw364CgL5I
FVs2p2Wr7Gjn7TW7zOmzT8Cl6Lw5if5i+kH0ROh0JLfDhUa+bIrVKiXO2QF0tLl2
p4GqyKKfp3BbHROF7bKqpypzB50tYpMMTCnIYQGyTtLTCv2SbO6PHepvXXKsODII
bfbBTsY/XQ0WYaD/HVs=
=HQ8K
-----END PGP PUBLIC KEY BLOCK-----
23 changes: 23 additions & 0 deletions src/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Configuration"""

from typing import Final, Optional
from os import getenv

LOGGING_LEVEL: Final[str] = getenv(key="LOGGING_LEVEL", default="INFO")
DOCKER_REGISTRY_URL: Final[Optional[str]] = getenv(key="DOCKER_REGISTRY_URL")
DOCKER_REGISTRY_CA_FILE: Final[Optional[str]] = getenv(key="DOCKER_REGISTRY_CA_FILE")
DOCKER_IMAGES_FILTER: Final[str] = getenv(key="DOCKER_IMAGES_FILTER", default=r".*")
DOCKER_TAGS_FILTER: Final[str] = getenv(key="DOCKER_TAGS_FILTER", default=r".*")
IMAGE_LIST_NBR_MAX: Final[int] = int(getenv(key="IMAGE_LIST_NBR_MAX", default="1000"))
HTTPS_CONNECTION_TIMEOUT: Final[int] = int(
getenv(key="HTTP_CONNECTION_TIMEOUT", default="3")
)
SCAN_SEVERITY: Final[str] = getenv(key="SCAN_SEVERITY", default="HIGH,CRITICAL")
SCAN_MIN_SEVERITY: Final[Optional[str]] = getenv(key="SCAN_MIN_SEVERITY")
SCAN_RESULTS_REPORT_FILE: Final[str] = getenv(
key="SCAN_RESULTS_REPORT_FILE", default="./scan_results_report.json"
)
SCAN_SCANNERS: Final[str] = getenv(key="SCAN_SCANNERS", default="vuln,secret")
MULTIPROCESSING_PROCESSES: Final[int] = int(
getenv(key="MULTIPROCESSING_PROCESSES", default="5")
)
Loading
Loading