Adding application and image scans to your CI/CD pipeline

Developer Advocate, CircleCI

Security is a shared responsibility. DevSecOps integrates security into the software development lifecycle (SDLC) to detect and resolve vulnerabilities early. Traditional security checks at the end of the development cycle often result in delays, rework, and inefficiencies. Embedding security into CI/CD pipelines mitigates these issues by automating scans and alerts, ensuring vulnerabilities are addressed promptly.
Adding security to CI/CD with Snyk
Snyk helps developers identify and fix vulnerabilities in applications and containers. CircleCI integrates with Snyk via the Snyk Orb, enabling seamless security scanning within CI/CD.
Setting up the project
To demonstrate the DevSecOps practices, we will create a simple Python(Flask) project with basic functionality, write unit tests using pytest
, and integrate security scanning with Snyk.
Prerequisites
Ensure you have:
- CircleCI account
- Snyk account. The free tier is sufficient for this tutorial.
- Docker Hub account
- Python and
pip
installed on your local machine
Creating the Flask project
Create a new directory and set up a basic Flask application:
mkdir circleci-flask-app && cd circleci-flask-app
python -m venv venv
source venv/bin/activate
pip install flask==0.12.4 Jinja2==2.10.1 pytest
This initializes a new project, creates a virtual environment, and installs Flask along with other required dependencies.
Next, create app.py
with a simple route:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def home():
return "Hello, Secure World!"
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
This is a minimal Flask application that serves a simple message when accessed via a browser.
Now create requirements.txt
with the dependencies:
Flask==0.12.4
Jinja2==2.10.1
pytest
This defines the dependencies required for the application to run.
Deploying the project to CircleCI (without security)
To automate the build process and streamline deployments, we will integrate our Flask application with CircleCI. This initial setup focuses on ensuring that our application builds successfully without introducing any security checks. Later, we will enhance the pipeline with security scanning to detect vulnerabilities.
Create .circleci/config.yml
and paste the following in it:
version: 2.1
jobs:
build:
docker:
- image: cimg/python:3.10
steps:
- checkout
- run:
name: Install Dependencies
command: pip install --no-cache-dir -r requirements.txt
workflows:
build_test_deploy:
jobs:
- build
At this stage, the pipeline will execute without running security checks, allowing the application to build successfully but leaving vulnerabilities undetected.
Commit and push your local changes to GitHub.
Next, login to CircleCI account, search for the project circleci-flask-app
and click Set Up Project
Select fastest
then click Set Up Project.
This will trigger the pipeline to run and build successfully.
Again, nothing much to check here as the pipeline is running without security checks. Before we add security scanning, let’s add the Snyk API token to CircleCI as an environment variable. Click on the project settings -> Environment variables and add a new variable SNYK_TOKEN
with the value of your Snyk API token.
You can find the Snyk API token in your Snyk account settings.
Adding Snyk security scan
Now modify the .circleci/config.yml
file to include the Snyk Orb for security scanning:
version: 2.1
orbs:
snyk: snyk/snyk@2.2.0
jobs:
build_test:
docker:
- image: cimg/python:3.12.2
steps:
- checkout
- run:
name: Install Dependencies
command: pip install --no-cache-dir -r requirements.txt
- snyk/scan
workflows:
build_test_deploy:
jobs:
- build_test
This introduces Snyk scanning to the pipeline. Since the project still uses outdated dependencies, the scan will fail. This is expected behavior as the goal is to identify and fix vulnerabilities. Push the changes to GitHub to trigger the pipeline.
Fixing vulnerabilities and adding unit tests
At this stage, we will update our dependencies and introduce unit tests to verify that our application behaves as excpected. Updating dependencies ensures that we mitigate security risks, while unit tests provide a layer of validation.
Update the requirements.txt
file to fix the vulnerabilities:
Flask
Jinja2
pytest
Now create test_app.py
to verify the endpoint:
import pytest
from app import app
def test_home():
client = app.test_client()
response = client.get('/')
assert response.status_code == 200
assert response.data == b"Hello, Secure World!"
This test ensures that the home route is working correctly.
Modify .circleci/config.yml
to re-run Snyk with updated dependencies and unit tests:
version: 2.1
orbs:
snyk: snyk/snyk@2.2.0
jobs:
build_test:
docker:
- image: cimg/python:3.12.2
steps:
- checkout
- run:
name: Install Fixed Dependencies
command: pip install --no-cache-dir -r requirements.txt
- snyk/scan
- run:
name: Run Unit Tests
command: pytest
workflows:
build_test_deploy:
jobs:
- build_test
This configuration now includes unit tests and ensures vulnerabilities are mitigated before deployment. Push the changes to GitHub to trigger the pipeline.
Similarly, you can check the Snyk dashboard to confirm the vulnerabilities have been resolved.
Security enabled pipeline - the Snyk Docker image scan
After discussing Snyk’s application scanning capabilities, we now move on to Docker image scanning, which is seamlessly integrated into CI/CD pipelines. This step ensures that vulnerabilities within containerized applications are detected early, preventing insecure images from being deployed.
We will use the same Flask application and extend our pipeline by adding a Docker image build and scan step. If vulnerabilities are found, the image will not be pushed to Docker Hub, ensuring only secure images are deployed.
Create a Dockerfile in the root of the project:
FROM python:3.7.4
WORKDIR /app
COPY . /app
RUN pip install --no-cache-dir Flask==0.12.4 Jinja2==2.10.1 pytest
EXPOSE 5000
CMD ["python", "app.py"]
Next, update the .circleci/config.yml
file to include the Docker image build and scan job:
version: 2.1
orbs:
snyk: snyk/snyk@2.2.0
jobs:
build_test:
docker:
- image: cimg/python:3.12.2
steps:
- checkout
- run:
name: Install Fixed Dependencies
command: pip install --no-cache-dir -r requirements.txt
- snyk/scan
- run:
name: Run Unit Tests
command: pytest
build_push_image:
docker:
- image: cimg/python:3.12.2
steps:
- checkout
- setup_remote_docker
- run:
name: Build and Scan Docker Image
command: |
docker build -t $DOCKERHUB_USERNAME/app:latest .
- snyk/scan:
docker-image-name: "$DOCKERHUB_USERNAME/app:latest"
monitor-on-build: true
- run:
name: Push Docker Image
command: |
docker build -t $DOCKERHUB_USERNAME/app:latest .
echo $DOCKERHUB_PASS | docker login -u $DOCKERHUB_USERNAME --password-stdin
docker push $DOCKERHUB_USERNAME/app:latest
workflows:
build_test_deploy:
jobs:
- build_test
- build_push_image:
requires:
- build_test
Before you push the changes to GitHub, go back to CircleCI and add the following environment variables:
$DOCKERHUB_USERNAME
(Docker Hub username)$DOCKERHUB_PASS
(Docker Hub password)
Now commit your changes and push to GitHub to trigger the pipeline.
As expected the pipeline will fail due to the known vulnerabilities in the base image. The Snyk Docker image scan will detect the vulnerabilities and fail the pipeline.
You can fix the vulnerabilities by updating the base image in the Dockerfile to a non-vulnerable version.
FROM python:3.12-alpine
WORKDIR /app
COPY . /app
RUN pip install --no-cache-dir Flask Jinja2 pytest
EXPOSE 5000
CMD ["python", "app.py"]
Once you push the changes to GitHub, the pipeline will run successfully and the Docker image will be pushed to the Docker Hub.
Conclusion
By integrating security scans into the CI/CD pipeline, teams can proactively identify vulnerabilities in both application dependencies and Docker images. Using the Snyk Orb simplifies security integration, ensuring your applications and infrastructure remain protected.
The complete code for this tutorial is available on GitHub.