Monday, January 2, 2017

Scanning docker image vulnerabilities using Clair

CoreOS has delivered a tool, Clair, which can be used to detect vulnerabilities  in containers images. Clair can be integrated in online or on premise registry services.

Some work has started to integrate Clair in Harbor, which is a very promising docker native open source registry server.

Controlling images when they enter a registry is not sufficient enough; one has to check the vulnerabilities of already deployed images.

The Clair server comes also with a contributed command line interface, analyze-local-images, which aims at checking the vulnerabilities of the images of a Docker host.

This (Golang) command requires a Clair Server running somewhere on the network.

It works as follow:

- analyse-local-images gathers the layers of the image to check in a temp directory
- it opens a web server on the port 9279, ready to serve the layers to the Clair server
- it then asks the Clair server to check the layers for vulnerabilities
- the Clair server download each layer from the web server started above
- the Clair server gives a vulnerability feedback to the analyze-local-images CLI
- finally, analyze-local-images displays the result and clean the temp directory

I plan to use this CLI on each docker-host I am using, so I wrote a Python3 script to check each local images of the local host, beginning by installing the analyze-local-images CLI (using Docker itself) in the $HOME/bin directory if it is not yet available.

The fun part of this script is the utilization of the amazing docker-py library (1.10.6), particularly to build the golang executable of the  analyze-local-images CLI.


# -*- coding: utf-8 -*-
# checkimages.py
'''
Use Clair to check the vulnerabilities of the Docker images of the host.
'''

import docker
import argparse
import os
import subprocess
import io
import tarfile

_CLAIR_CLIENT_DIR_PATH = os.getenv('HOME') + '/bin'
_CLAIR_CLIENT_NAME = 'analyze-local-images'
_CLAIR_CLIENT_PATH = _CLAIR_CLIENT_DIR_PATH + '/' + _CLAIR_CLIENT_NAME


def create_clair_client(docker_client, dstdir):
    '''
    Use Docker to compile the Clair local client and copy it under dstdir.

    Creates a local clair client it is not available:

    - pull a golang image
    - create a container to build the client
    - start the container
    - get the compiled client and untar it at the right place
    - remove the container
    - remove the golang image

    Then, use the clair client to submit each images of the host to the clair server.

    :param docker_client:
    :param dstdir:
    :return:
    '''
    try:
        docker_client.pull(tag='1.6', repository='golang')
        result = docker_client.create_container(
            image='golang:1.6',
            command="go get -u github.com/coreos/clair/contrib/analyze-local-images",
        )
        cid = result['Id']
        docker_client.start(container=cid)
        docker_client.wait(container=cid)
        response, _ = docker_client.get_archive(container=cid, path='/go/bin/' + _CLAIR_CLIENT_NAME)
        file_content = io.BytesIO(response.read())
        tf = tarfile.open(fileobj=file_content)
        tf.extractall(path=dstdir)
        tf.close()
        docker_client.remove_container(container=cid)
        docker_client.remove_image(image='golang:1.6')
    except RuntimeError as err:
        print("error: {0}".format(err))


def main():
    """
    Check vulnerabilities for all available images.
    If the Clair client is not available, build it and put in $HOME/bin.

    :return:
    """
    parser = argparse.ArgumentParser(description='Check vulnerabilities on local images')

    parser.add_argument('-c', '--clair-host', metavar='HOST', required=True,
                        dest='clair_host', action='store',
                        help='the clair host')

    parser.add_argument('-C', '--client-host', metavar='HOST', required=True,
                        dest='client_host', action='store',
                        help='the docker host')

    args = parser.parse_args()

    docker_client = docker.from_env(version='auto')

    if not os.path.exists(_CLAIR_CLIENT_PATH):
        create_clair_client(docker_client, _CLAIR_CLIENT_DIR_PATH)

    cmd = _CLAIR_CLIENT_PATH + \
          ' -minimum-severity High' + \
          ' -color never' + \
          ' -endpoint http://' + args.clair_host + ':6060' + \
          ' -my-address ' + args.client_host

    for image in docker_client.images():
        tag = image['RepoTags'][0]
        try:
            out_bytes = subprocess.check_output(
                cmd + ' ' + tag,
                stderr=subprocess.STDOUT,
                shell=True)
            print(out_bytes.decode('utf-8'))
            print("-" * 80)
        except subprocess.CalledProcessError as e:
            out_bytes = e.output
            print(out_bytes.decode('utf-8'))
            print(">" * 80)


main()


NB: I am not a Python developer, so things above may not be "pythonic"... sorry for that.

The version of the Clair server I used for this work (using docker-compose) was not able to establish a diagnostic for the Alpine distribution. I guess that Alpine images will be supported in the next dockerized ready to run version of Clair.

If you use this kind of tool, you will realize that no matter how fresh your pulled images from the docker hub are; they may contain unfixed CVE bugs ranked as "High".

At the present time, I don't know how to manage correctly this kind of situation: what operational feedback to give to the user when an official image contains unfixed flaws. There is nothing to do as long as the distro does not emit an update, and this information produces noise comparing to a real alert (=you have to rebuild an image because the underlying layers have been fixed). I don't even know how to distinguish automatically these two cases (maybe emit an alert only if something has changed between two analysis ?).

I need more time to find a way ...