Scenario: Fixing Vulnerabilities and Secrets with Artifact Scanning¶
Challenge ahead!
This scenario gets a bit challenging after scanning images, but you should be able to get through it.
Prerequisites¶
- Vision One Container Security Artifact Scanner API-Key with the following permissions:
- Cloud Security Operations
- Container Protection
- Run artifact scan
- Container Protection
- Cloud Security Operations
Ensure to have the latest tmas
deployed:
Scan Images for Vulnerabilities, Malware, and Secrets¶
First, set the Artifact Scanner API-Key as an environment variable:
Note: tmas defaults to the Vision One service region
us-east-1
. If your Vision One is serviced from any other region you need to add the--region
flag to the scan request.Valid regions:
[ap-southeast-2 eu-central-1 ap-south-1 ap-northeast-1 ap-southeast-1 us-east-1]
If you followed the other scenarios on Artifact Scanning and Shift Left Security you know how to use tmas
already.
In this scenario we're going to fix findings.
Vulnerabilities - How to Fix Findings (Python Example)¶
So, let's go through fixing Vulnerabilities in a container image. For this we play with one of my apps, UpTonight. For the curious ones out there, it calculates the best astrophotography targets for the night at a given location and date. These are deep sky objects, solar system bodies and coments.
We start with version 2.2 from beginning October and scan it for vulnerabilities:
Heading over to the Cloud Security -> Shift Left Security
we click on the 66
below Vulnerabilities
for the Artifact mawinkler/uptonight
with the Tag 2.2
.
We can now filter on Fix Available
and get six findings that we can address.
Alternatively, we could do this in our shell by running:
tmas scan -V registry:mawinkler/uptonight:2.2 | \
jq -r '.vulnerabilities.findings.Medium[] | select(.fix!="not-fixed") | [.id, .name, .version, .fix] | @tsv'
GHSA-8495-4g3g-x7pr aiohttp 3.10.5 3.10.11
CVE-2024-9287 libpython3.10-minimal 3.10.12-1~22.04.6 3.10.12-1~22.04.7
CVE-2024-9287 libpython3.10-stdlib 3.10.12-1~22.04.6 3.10.12-1~22.04.7
CVE-2024-9287 python3.10 3.10.12-1~22.04.6 3.10.12-1~22.04.7
CVE-2024-9287 python3.10-minimal 3.10.12-1~22.04.6 3.10.12-1~22.04.7
GHSA-9wx4-h78v-vm56 requests 2.31.0 2.32.0
What this tells us is that there seem to be three areas where we need to take a closer look:
- Four findings are introduced by a vulnerable python version 3.10.12 we could update.
- Similar for the
requests
library which is used to create HTTP(s) requests. - And
aoihttp
which we should update as well.
It is of course important that we test any version upgrades.
Setting Up a Python Virtual Environment¶
We need to make sure we're using the same version of Python as in the image. To do this safely without breaking the system, we'll use pyenv
to do this in an isolated environment. If you don't have it, install it by running:
Linux
Append the following to your .bashrc
:
# pyenv
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"
Ensure some dependencies:
sudo apt update
sudo apt-get install build-essential zlib1g-dev libffi-dev libssl-dev libbz2-dev libreadline-dev libsqlite3-dev liblzma-dev libncurses-dev tk-dev
And restart the terminal.
MacOS
Append the following to your .zshrc
:
And restart the terminal.
Now, let's downlod the sources into a temporary directory:
mkdir uptonight && cd uptonight
wget https://github.com/mawinkler/uptonight/archive/refs/tags/2.2.tar.gz
tar xfvz 2.2.tar.gz
cd uptonight-2.2
Since the image in this release uses Python 3.10.12, we install and activate it.
Downloading Python-3.10.12.tar.xz...
-> https://www.python.org/ftp/python/3.10.12/Python-3.10.12.tar.xz
Installing Python-3.10.12...
Installed Python-3.10.12 to /home/markus/.pyenv/versions/3.10.12
Now, we create the virtual environment:
Note that your prompt has changed (venv) ...
. This indicates that you're now in a python virtual environment.
Run UpTonight¶
Before running UpTonight we need to install it's dependencies, which are defined in the requirements.txt
.
Now run UpTonight for a quick test:
# Center of Munich
export LONGITUDE="11d34m51.50s"
export LATITUDE="48d08m10.77s"
export ELEVATION=519
export TIMEZONE="Europe/Berlin"
python3 main.py
This should produce some output in your shell and create some files within the out
directory. If interested, check them out ;-).
Fix the requests
Package¶
This is pretty easy. We only need to change one line within the requirements.txt
, update the dependencies in our environment and check if it still works.
Change requests==2.31.0
to requests==2.32.0
and rerun:
and retest:
Fix the aiohttp
Package¶
So this is a little more tricky since this is a nested dependency (not listed in requirements.txt
)
To get an idea where it sits in the dependency tree we can utilize pipdeptree
.
We can see that aiohttp
is introduced by s3fs
, the AWS S3 python library.
...
s3fs==2024.10.0
├── aiobotocore [required: >=2.5.4,<3.0.0, installed: 2.15.2]
│ ├── aiohttp [required: >=3.9.2,<4.0.0, installed: 3.11.7]
│ │ ├── aiohappyeyeballs [required: >=2.3.0, installed: 2.4.3]
│ │ ├── aiosignal [required: >=1.1.2, installed: 1.3.1]
│ │ │ └── frozenlist [required: >=1.1.0, installed: 1.5.0]
│ │ ├── async-timeout [required: >=4.0,<6.0, installed: 5.0.1]
│ │ ├── attrs [required: >=17.3.0, installed: 24.2.0]
│ │ ├── frozenlist [required: >=1.1.1, installed: 1.5.0]
│ │ ├── multidict [required: >=4.5,<7.0, installed: 6.1.0]
│ │ │ └── typing_extensions [required: >=4.1.0, installed: 4.12.2]
│ │ ├── propcache [required: >=0.2.0, installed: 0.2.0]
│ │ └── yarl [required: >=1.17.0,<2.0, installed: 1.18.0]
│ │ ├── idna [required: >=2.0, installed: 3.10]
│ │ ├── multidict [required: >=4.0, installed: 6.1.0]
│ │ │ └── typing_extensions [required: >=4.1.0, installed: 4.12.2]
│ │ └── propcache [required: >=0.2.0, installed: 0.2.0]
│ ├── aioitertools [required: >=0.5.1,<1.0.0, installed: 0.12.0]
│ ├── botocore [required: >=1.35.16,<1.35.37, installed: 1.35.36]
│ │ ├── jmespath [required: >=0.7.1,<2.0.0, installed: 1.0.1]
│ │ ├── python-dateutil [required: >=2.1,<3.0.0, installed: 2.9.0.post0]
│ │ │ └── six [required: >=1.5, installed: 1.16.0]
│ │ └── urllib3 [required: >=1.25.4,<3,!=2.2.0, installed: 2.2.3]
│ └── wrapt [required: >=1.10.10,<2.0.0, installed: 1.17.0]
├── aiohttp [required: !=4.0.0a1,!=4.0.0a0, installed: 3.11.7]
│ ├── aiohappyeyeballs [required: >=2.3.0, installed: 2.4.3]
│ ├── aiosignal [required: >=1.1.2, installed: 1.3.1]
│ │ └── frozenlist [required: >=1.1.0, installed: 1.5.0]
│ ├── async-timeout [required: >=4.0,<6.0, installed: 5.0.1]
│ ├── attrs [required: >=17.3.0, installed: 24.2.0]
│ ├── frozenlist [required: >=1.1.1, installed: 1.5.0]
│ ├── multidict [required: >=4.5,<7.0, installed: 6.1.0]
│ │ └── typing_extensions [required: >=4.1.0, installed: 4.12.2]
│ ├── propcache [required: >=0.2.0, installed: 0.2.0]
│ └── yarl [required: >=1.17.0,<2.0, installed: 1.18.0]
│ ├── idna [required: >=2.0, installed: 3.10]
│ ├── multidict [required: >=4.0, installed: 6.1.0]
│ │ └── typing_extensions [required: >=4.1.0, installed: 4.12.2]
│ └── propcache [required: >=0.2.0, installed: 0.2.0]
└── fsspec [required: ==2024.10.0.*, installed: 2024.10.0]
...
s3fs
is introduced by astropy[all]
which we can verify here https://github.com/astropy/astropy/blob/main/pyproject.toml
. UpTonight does not use AWS so we can try the astropy[recommended]
variant.
Change astropy[all]==6.0.1
to astropy[recommended]==6.0.1
and run:
# Clean up installed packages
pip install pip-tools
pip-sync requirements.txt
# Ensure requirements are still met
pip install -r requirements.txt
and retest:
Good, still seems to to work.
Verify Our Changes¶
Let's build the container, scan it and check if we successfully fixed the two vulnerabilities:
docker build -t uptonight-patched --load .
tmas scan -V docker:uptonight-patched | \
jq -r '.vulnerabilities.findings.Medium[] | select(.fix!="not-fixed") | [.id, .name, .version, .fix] | @tsv'
CVE-2024-9287 libpython3.10-minimal 3.10.12-1~22.04.6 3.10.12-1~22.04.7
CVE-2024-9287 libpython3.10-stdlib 3.10.12-1~22.04.6 3.10.12-1~22.04.7
CVE-2024-9287 python3.10 3.10.12-1~22.04.6 3.10.12-1~22.04.7
CVE-2024-9287 python3.10-minimal 3.10.12-1~22.04.6 3.10.12-1~22.04.7
Wahoo, we did it :-) Two are gone.
Fix the python3.10
Package¶
Python3 is installed within the Dockerfile:
# Compile image
FROM ubuntu:jammy AS compile-image
ENV DEBIAN_FRONTEND=noninteractive
WORKDIR /app
RUN apt-get update \
&& apt-get install -y python3-pip python3-dev pkg-config libhdf5-dev \
&& cd /usr/local/bin \
&& ln -s /usr/bin/python3 python \
&& pip3 install --upgrade pip
COPY requirements.txt requirements.txt
RUN pip3 install --upgrade pip setuptools && \
pip install --no-cache-dir -r requirements.txt --user && \
pip list
# Run image
FROM ubuntu:jammy AS runtime-image
RUN apt-get update \
&& apt-get install -y python3 \
&& cd /usr/local/bin \
&& ln -s /usr/bin/python3 python
COPY --from=compile-image /root/.local /root/.local
COPY --from=compile-image /etc/ssl /etc/ssl
WORKDIR /app
COPY uptonight uptonight
COPY targets targets
COPY main.py .
ENV PATH=/root/local/bin:$PATH
ENTRYPOINT ["python3", "/app/main.py"]
This Dockerfile tells us, that it is based on ubuntu:jammy
and therefore depends on the the packages made available by Ubuntu. It is actually a multi-stage build, the compile-image
uses python3-dev
whereby the runtime-image
uses only python3
.
Apparently Ubuntu doesn't ship the required Python minor version we want to use. We can now install python from the official sources (which is a bit more difficult) or we can use the newer version of Jammy
, which is Noble
. This will also upgrade Python3 to 3.12, which is much more restrictive in terms of pip installation systemwide.
So we need to change the Dockerfile
a little (FROM
s and add --break-system-packages
):
FROM ubuntu:noble AS compile-image
ENV DEBIAN_FRONTEND=noninteractive
WORKDIR /app
RUN apt-get update \
&& apt-get install -y python3-pip python3-dev pkg-config libhdf5-dev \
&& cd /usr/local/bin \
&& ln -s /usr/bin/python3 python
COPY requirements.txt requirements.txt
RUN pip install --no-cache-dir -r requirements.txt --user --break-system-packages && \
pip list
# Run image
FROM ubuntu:noble AS runtime-image
...
Now rebuild/retest the image.
docker build -t uptonight-patched --load .
tmas scan -V docker:uptonight-patched | \
jq -r '.vulnerabilities.findings.Medium[] | select(.fix!="not-fixed") | [.id, .name, .version, .fix] | @tsv'
Nothing to fix in Medium anymore and if we check for all vulnerabilities we now have only 18 left, which are not fixable currently. Good job!
But is UpTonight still working?
docker run -e LONGITUDE="11d34m51.50s" -e LATITUDE="48d08m10.77s" -e ELEVATION=519 -e TIMEZONE="Europe/Berlin" -v ./out:/app/out uptonight-patched
Wahoo! We made it.
Lastly, deactivate the python virtual environment:
Secrets - How to Deal with False Positives (Python Example)¶
Here, we're going to tackle false positives in the scan results.
Depending on the programming language used in a containerized app and how it got containerized tmas
will likely discover lots of generic secrets. These false positives are usually found within unit tests or examples included in the language runtime distribution.
Let's play again with UpTonight.
First, let's run a scan on the 2.2
image:
So, there are apparently 306 secrets...
If we want to inspect a specific finding we need to dissect the container image into it's layers. Make sure to use the correct image digest reported by Vision One.
docker pull docker.io/mawinkler/uptonight@sha256:2dbb4a927b796b9384cc9deb6a51690407d1e661caeae163f4db4ecd53167701
docker save -o uptonight.tar docker.io/mawinkler/uptonight@sha256:2dbb4a927b796b9384cc9deb6a51690407d1e661caeae163f4db4ecd53167701
The above command saves the image including all of its layers into a tar archive. Extracting this archive we get the following structure:
.
├── blobs
│ └── sha256
│ ├── 0289e2ec8bd41c714a9c9cb966178936fbee43d105fe01228248dfb8f7e2e65f
│ ├── 14400b4a9b67b699da1139d477d547ec7641c0ed426154e44dc8801049b194d0
│ ├── 1e2970d0bac38bc6f58c786a4afd5edd618f2234ac4544d252c076d1d5ba8bcb
│ ├── 2573e0d8158209ed54ab25c87bcdcb00bd3d2539246960a3d592a1c599d70465
│ ├── 3af7b477f81821ea399f9e0ec15c01da98760385de5215b48305818ffe387eb0
│ ├── 4d4d9b9d193f30fc49e7a4436c528b23ac727344f5d1a2750be3fb306b49fc2b
│ ├── 699776f4e4fc26d1405c79779ac8884dace05694168924be559f87f938b1a2e4
│ ├── 6b6e322dc8865457a6eb8d5855f40c6a83c5719d30dfe557d0e2fa841fe4178e
│ ├── 7c51e6dae7e6949b891fee75e2035795f5cddf244b86bc05bc96b1917b542c9a
│ ├── 8498a0b71ea370be3ea0f1939319046bc8b71df07d5f7c35396d410e4b93ed90
│ ├── 895e113eb7ccd19522d9b7ca0279bc79579363c1d690d60d4d572dddbea3ef00
│ ├── 906f0ecb429801119a0bba0298aa2eaa26ba7f852ec44c7cd43d9779d4ba0cd4
│ ├── abef58d990b11bc9a7da4190cdcb05f03b6edaa3f63802d2949e4a3dd2501bba
│ ├── ac12c35f8650c0ec8b7f3bae9050a2e4ea9a30cf573a46dddb76958dababb7ff
│ ├── c72dfe6bf14c642d21076c0d9d1c1917894f8dbcc60b95d4b01ff3b71d196bf7
│ ├── ccc40309c2e9d4552ee8d86e9bb0e88be2d82bf2527755f95608076b6ac0a730
│ ├── dbe0eae7d2c32f7c87d0f6020a390f7d97b699d26c6021e81871a06a862ddab9
│ └── e72b88a7237022c675a9909b1125f50a4ad49120af2cbf52f27b9017f6b520ff
├── index.json
├── manifest.json
├── oci-layout
└── uptonight.tar
2 directories, 22 files
Just to be clear, the files in the blobs/sha256
directory contain the file system of each layer that makes up the image.
Now, let us inspect some files with potential secret findings.
JSON Web Token
Secret Type: jwt
Description: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.
Artifact ID: docker.io/mawinkler/uptonight@sha256:2dbb4a927b796b9384cc9deb6a51690407d1e661caeae163f4db4ecd53167701
Path: /root/.local/lib/python3.10/site-packages/astroquery/eso/tests/data/oidc_token.json
Layer ID: sha256:c72dfe6bf14c642d21076c0d9d1c1917894f8dbcc60b95d4b01ff3b71d196bf7
Start Line: 3
End Line: 3
Start Column: 17
End Column: 122
Secret: eyJhbGciOiJIU******EPqgpup30c6Mg
Scan ID: 3148a308-6a5b-43c8-bca5-3e361e182912
Extract the file:
tar xf blobs/sha256/c72dfe6bf14c642d21076c0d9d1c1917894f8dbcc60b95d4b01ff3b71d196bf7 root/.local/lib/python3.10/site-packages/astroquery/eso/tests/data/oidc_token.json
When we open the file and search for the discovered secret, we can see that it does look like a JWT token, but since it's in a tests directory from an external library, we can be pretty sure that this is a false positive.
{
"access_token": "some-access-token",
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2Nzg2Mjg5NTl9.qqKrC1MesQQmLtqsFOm2kxe4f_Nqo4EPqgpup30c6Mg",
"token_type": "bearer",
"expires_in": 28800,
"scope": ""
}
Very similar, there are AWS Access Tokens and a lot of generic secrets detected in the botocore
which we will exclude.
It is a fairly common result to have lots of findings about generic API keys, JWT tokens, etc. when the container image uses a programming language like Python and is not stripped down to what is really needed to run the application. We will later retest the same application with the container image properly stripped down.
For now, we can create an overrides file for a tmas rescan:
tmas_overrides.yaml
secrets:
paths:
- patterns:
- ".*/tests/.*"
reason: Unit tests
- patterns:
- "./botocore/data/.*/examples-1.json"
reason: Botocore examples
- patterns:
- ".*/site-packages/cryptography/hazmat/.*"
- ".*/site-packages/cryptography/x509/.*"
- ".*/site-packages/numpy/core/include/numpy/old_defines.h"
reason: "False positive in external library"
When now rescan the same image by running
We have only 11 findings left, 295 are overridden.
{
"secrets": {
"totalFilesScanned": 16531,
"unmitigatedFindingsCount": 11,
"overriddenFindingsCount": 295,
"findings": {
Not that bad! Feel free to inspect the remaining secrets discovered.
Finally, a Properly Stripped Down Image¶
As promised, let's see the difference with a properly stripped down image of the same application, now in version 2.3:
{
"vulnerabilities": {
"totalVulnCount": 15,
"criticalCount": 0,
"highCount": 0,
"mediumCount": 9,
"lowCount": 4,
"negligibleCount": 2,
"unknownCount": 0,
"overriddenCount": 0,
...
},
"secrets": {
"totalFilesScanned": 1686,
"unmitigatedFindingsCount": 0,
"overriddenFindingsCount": 0,
"findings": {}
}
}
The number of vulnerabilities decreased from 66 to 15, with a maximum severity rating of medium. The image size decreased from 291MB to 137MB.
🎉 Success 🎉