With the release of Anchore Enterprise 2.1 (based on Anchore Engine 0.5.0), local image analysis is now available. Inline Analysis gives users the ability to perform image analysis on a locally built Docker image without the need for it to exist inside a registry. Local image scanning analyzes an image from a local Docker engine and exports the analysis into your existing Anchore Engine deployment.

Local Analysis vs Typical Anchore Deployments

While local scanning is convenient when access to a registry is not available, Anchore recommends scanning images that have been pushed to the registry as it is a more robust solution. Local scanning is not meant to alter the fundamental deployment of Anchore Engine nor the image analysis strategy of Anchore. Adding an image via local scanning removes some of the wonderful features that are included in Anchore, like monitoring a registry for image tag or repository updates, subscriptions, or webhook notifications. Rather, it is intended to allow users to analyze images as one-off events, such as prior to moving them to a registry or deploying them from tarball in an air-gapped network. Additionally, by extracting the image from the Docker engine, local analysis can be used to analyze images from custom-tailored sources, such as OpenShift source-to-image or Pivotal kpack builds, or even on systems that don’t access any Continuous Integration/Continuous Deployment (CI/CD) processes.

Running Local Analysis on an Air-Gapped Network

As an example for this blog, I chose to perform a local analysis on an image I built but doing so while my network was disconnected from the Internet. Many systems don’t have access to Internet-facing registries, such as Docker Hub.

Getting Started

To start, an Internet-accessible machine is required to pull the local image analysis script, Anchore Docker images, and the base Alpine Docker image I use for my local build.

Using the following docker-compose file on an Internet-accessible machine, I can pull down the Anchore Enterprise Docker images:

# All-in-one docker-compose deployment of a full anchore-enterprise service system
---
version: '2.1'
volumes:
  anchore-db-volume:
    # Set this to 'true' to use an external volume. In which case, it must be created manually with "docker volume create anchore-db-volume"
    external: false
  anchore-scratch: {}
  feeds-workspace-volume:
    # Set this to 'true' to use an external volume. In which case, it must be created manually with "docker volume create feeds-workspace-volume"
    external: false
  enterprise-feeds-db-volume:
    # Set this to 'true' to use an external volume. In which case, it must be created manually with "docker volume create enterprise-feeds-db-volume"
    external: false

services:
  # The primary API endpoint service
  engine-api:
    image: docker.io/anchore/anchore-engine:latest
    depends_on:
    - anchore-db
    - engine-catalog
    #volumes:
    #- ./config-engine.yaml:/config/config.yaml:z
    ports:
    - "8228:8228"
    logging:
      driver: "json-file"
      options:
        max-size: 100m
    environment:
    - ANCHORE_ENDPOINT_HOSTNAME=engine-api
    - ANCHORE_DB_HOST=anchore-db
    - ANCHORE_DB_PASSWORD=mysecretpassword
    - ANCHORE_AUTHZ_HANDLER=external
    - ANCHORE_EXTERNAL_AUTHZ_ENDPOINT=http://enterprise-rbac-authorizer:8228
    - ANCHORE_ENABLE_METRICS=true
    command: ["anchore-manager", "service", "start",  "apiext"]
  # Catalog is the primary persistence and state manager of the system
  engine-catalog:
    image: docker.io/anchore/anchore-engine:latest
    depends_on:
    - anchore-db
    #volumes:
    #- ./config-engine.yaml:/config/config.yaml:z
    logging:
      driver: "json-file"
      options:
        max-size: 100m
    expose:
    - 8228
    environment:
    - ANCHORE_ENDPOINT_HOSTNAME=engine-catalog
    - ANCHORE_DB_HOST=anchore-db
    - ANCHORE_DB_PASSWORD=mysecretpassword
    - ANCHORE_ENABLE_METRICS=true
    command: ["anchore-manager", "service", "start",  "catalog"]
  engine-simpleq:
    image: docker.io/anchore/anchore-engine:latest
    depends_on:
    - anchore-db
    - engine-catalog
    #volumes:
    #- ./config-engine.yaml:/config/config.yaml:z
    expose:
    - 8228
    logging:
      driver: "json-file"
      options:
        max-size: 100m
    environment:
    - ANCHORE_ENDPOINT_HOSTNAME=engine-simpleq
    - ANCHORE_DB_HOST=anchore-db
    - ANCHORE_DB_PASSWORD=mysecretpassword
    - ANCHORE_ENABLE_METRICS=true
    command: ["anchore-manager", "service", "start",  "simplequeue"]
  engine-policy-engine:
    image: docker.io/anchore/anchore-engine:latest
    depends_on:
    - anchore-db
    - engine-catalog
    #volumes:
    #- ./config-engine.yaml:/config/config.yaml:z
    expose:
    - 8228
    logging:
      driver: "json-file"
      options:
        max-size: 100m
    environment:
    - ANCHORE_ENDPOINT_HOSTNAME=engine-policy-engine
    - ANCHORE_DB_HOST=anchore-db
    - ANCHORE_DB_PASSWORD=mysecretpassword
    - ANCHORE_ENABLE_METRICS=true
    command: ["anchore-manager", "service", "start",  "policy_engine"]
  engine-analyzer:
    image: docker.io/anchore/anchore-engine:latest
    depends_on:
    - anchore-db
    - engine-catalog
    #volumes:
    #- ./config-engine.yaml:/config/config.yaml:z
    expose:
    - 8228
    logging:
      driver: "json-file"
      options:
        max-size: 100m
    environment:
    - ANCHORE_ENDPOINT_HOSTNAME=engine-analyzer
    - ANCHORE_DB_HOST=anchore-db
    - ANCHORE_DB_PASSWORD=mysecretpassword
    - ANCHORE_ENABLE_METRICS=true
    volumes:
    - anchore-scratch:/analysis_scratch
    command: ["anchore-manager", "service", "start",  "analyzer"]
  anchore-db:
    image: "postgres:9"
    volumes:
    - anchore-db-volume:/var/lib/postgresql/data
    environment:
    - POSTGRES_PASSWORD=mysecretpassword
    expose:
    - 5432
    logging:
      driver: "json-file"
      options:
        max-size: 100m
  enterprise-feeds-db:
    image: "postgres:9"
    volumes:
    - enterprise-feeds-db-volume:/var/lib/postgresql/data
    environment:
    - POSTGRES_PASSWORD=mysecretpassword
    expose:
    - 5432
    logging:
      driver: "json-file"
      options:
        max-size: 100m
  enterprise-rbac-authorizer:
    image: docker.io/anchore/enterprise:latest
    volumes:
    - ./license.yaml:/license.yaml:ro
    #- ./config-enterprise.yaml:/config/config.yaml:z
    depends_on:
    - anchore-db
    - engine-catalog
    expose:
    - 8089
    logging:
      driver: "json-file"
      options:
        max-size: 100m
    environment:
    - ANCHORE_ENDPOINT_HOSTNAME=enterprise-rbac-authorizer
    - ANCHORE_DB_HOST=anchore-db
    - ANCHORE_DB_PASSWORD=mysecretpassword
    - ANCHORE_ENABLE_METRICS=true
    command: ["anchore-enterprise-manager", "service", "start",  "rbac_authorizer"]
  enterprise-rbac-manager:
    image: docker.io/anchore/enterprise:latest
    volumes:
    - ./license.yaml:/license.yaml:ro
    #- ./config-enterprise.yaml:/config/config.yaml:z
    depends_on:
    - anchore-db
    - engine-catalog
    ports:
    - "8229:8228"
    logging:
      driver: "json-file"
      options:
        max-size: 100m
    environment:
    - ANCHORE_ENDPOINT_HOSTNAME=enterprise-rbac-manager
    - ANCHORE_DB_HOST=anchore-db
    - ANCHORE_DB_PASSWORD=mysecretpassword
    - ANCHORE_AUTHZ_HANDLER=external
    - ANCHORE_EXTERNAL_AUTHZ_ENDPOINT=http://enterprise-rbac-authorizer:8228
    - ANCHORE_ENABLE_METRICS=true
    command: ["anchore-enterprise-manager", "service", "start",  "rbac_manager"]
  enterprise-feeds:
    image: docker.io/anchore/enterprise:latest
    volumes:
    - feeds-workspace-volume:/workspace
    - ./license.yaml:/license.yaml:ro
    #- ./config-enterprise.yaml:/config/config.yaml:z
    depends_on:
    - enterprise-feeds-db
    ports:
    - "8448:8228"
    logging:
      driver: "json-file"
      options:
        max-size: 100m
    environment:
    - ANCHORE_ENDPOINT_HOSTNAME=enterprise-feeds
    - ANCHORE_DB_HOST=enterprise-feeds-db
    - ANCHORE_DB_PASSWORD=mysecretpassword
    - ANCHORE_ENABLE_METRICS=true
    command: ["anchore-enterprise-manager", "service", "start",  "feeds"]
  enterprise-reports:
    image: docker.io/anchore/enterprise:latest
    volumes:
    - ./license.yaml:/license.yaml:ro
    depends_on:
    - anchore-db
    - engine-catalog
    ports:
    - "8558:8228"
    logging:
      driver: "json-file"
      options:
        max-size: 100m
    environment:
    - ANCHORE_ENDPOINT_HOSTNAME=enterprise-reports
    - ANCHORE_DB_HOST=anchore-db
    - ANCHORE_DB_PASSWORD=mysecretpassword
    - ANCHORE_ENABLE_METRICS=true
    - ANCHORE_AUTHZ_HANDLER=external
    - ANCHORE_EXTERNAL_AUTHZ_ENDPOINT=http://enterprise-rbac-authorizer:8228
    command: ["anchore-enterprise-manager", "service", "start",  "reports"]
  enterprise-ui-redis:
    image: "docker.io/library/redis:4"
    expose:
    - 6379
    logging:
      driver: "json-file"
      options:
        max-size: 100m
  enterprise-ui:
    image: docker.io/anchore/enterprise-ui:latest
    volumes:
    - ./license.yaml:/license.yaml:ro
    #- ./config-ui.yaml:/config/config-ui.yaml:z
    depends_on:
    - engine-api
    - enterprise-ui-redis
    - anchore-db
    ports:
    - "3000:3000"
    logging:
      driver: "json-file"
      options:
        max-size: 100m
    environment:
    - ANCHORE_ENGINE_URI=http://engine-api:8228/v1
    - ANCHORE_RBAC_URI=http://enterprise-rbac-manager:8228/v1
    - ANCHORE_REDIS_URI=redis://enterprise-ui-redis:6379
    - ANCHORE_APPDB_URI=postgres://postgres:mysecretpassword@anchore-db:5432/postgres
    - ANCHORE_REPORTS_URI=http://enterprise-reports:8228/v1
    - ANCHORE_POLICY_HUB_URI=https://hub.anchore.io

I can pull the images with the following command:

$ docker-compose -f docker-compose-enterprise.yaml pull
Pulling anchore-db ... done
Pulling engine-catalog ... done
Pulling engine-analyzer ... done
Pulling engine-policy-engine ... done
Pulling engine-simpleq ... done
Pulling engine-api ... done
Pulling enterprise-feeds-db ... done
Pulling enterprise-rbac-authorizer ... done
Pulling enterprise-rbac-manager ... done
Pulling enterprise-feeds ... done
Pulling enterprise-reports ... done
Pulling enterprise-ui-redis ... done
Pulling enterprise-ui ... done

Next, I’ll pull the Inline Scan image from Anchore:

$ docker pull docker.io/anchore/inline-scan:v0.5.0
Pulling docker.io/anchore/inline-scan:v0.5.0
v0.5.0: Pulling from anchore/inline-scan
c8d67acdb2ff: Already exists
79d11c1a86c4: Already exists
ced9ca3af39b: Already exists
c1e8af2e6afa: Already exists
ca674bdc4ffc: Already exists
7fa29b97cf4f: Already exists
15f5109f7371: Already exists
662a1f6a8a80: Already exists
6e87d34cd76e: Pull complete
7f7b513db561: Pull complete
5c7e09ac2f74: Pull complete
b50890f6248a: Pull complete
5f8043f17686: Pull complete
3a3cdaeaf045: Pull complete
c877ae27c8fe: Pull complete
58edd3c9fcf5: Pull complete
0ef916eddeef: Pull complete
Digest: sha256:650a7fae8f95286301cdb5061475c0be7e4fb762ba2c85ff489494d089883c1c
Status: Downloaded newer image for anchore/inline-scan:v0.5.0

Now I will pull the local image analysis script using curl from anchore.io’s ci-cd endpoint and make it executable:

$ curl -o inline_scan.sh https://ci-tools.anchore.io/inline_scan-v0.5.0
$ chmod +x inline_scan.sh

Finally, I will pull down the base Alpine image that I will use to build my local Docker image:

$ docker pull docker.io/library/alpine:latest

From here, I disconnect my Internet connection as the rest of the example is simulating an air-gapped network

Deploying Anchore Enterprise

In this example, I deploy Anchore Enterprise because the UI makes it simple to see results from the local image I analyze. Local image analysis is also available with OSS Anchore Engine v0.5.0.

Using the same docker-compose-enterprise.yaml from above, I can now deploy Anchore Enterprise:

$ docker-compose -f docker-compose-enterprise.yaml up -d
Creating network "aevolume_default" with the default driver
Creating aevolume_anchore-db_1 ... done
Creating aevolume_enterprise-ui-redis_1 ... done
Creating aevolume_enterprise-feeds-db_1 ... done
Creating aevolume_engine-catalog_1 ... done
Creating aevolume_enterprise-feeds_1 ... done
Creating aevolume_engine-simpleq_1 ... done
Creating aevolume_enterprise-reports_1 ... done
Creating aevolume_engine-analyzer_1 ... done
Creating aevolume_engine-policy-engine_1 ... done
Creating aevolume_enterprise-rbac-authorizer_1 ... done
Creating aevolume_enterprise-rbac-manager_1 ... done
Creating aevolume_engine-api_1 ... done
Creating aevolume_enterprise-ui_1 ... done

Build Local Image

For this example, I built the simplest Docker image from this Dockerfile:

FROM docker.io/library/alpine:latest

CMD echo "hello world"

Then I built it with:

$ docker build . -t local/example:latest
Sending build context to Docker daemon 2.048kB
Step 1/2 : FROM docker.io/library/alpine:latest
latest: Pulling from library/alpine
9d48c3bd43c5: Pull complete
Digest: sha256:72c42ed48c3a2db31b7dafe17d275b634664a708d901ec9fd57b1529280f01fb
Status: Downloaded newer image for alpine:latest
---> 961769676411
Step 2/2 : CMD echo "hello world"
---> Running in 74bdcd240547
Removing intermediate container 74bdcd240547
---> 325116ad4e62
Successfully built 325116ad4e62
Successfully tagged local/example:latest

Once built, I can view it in my local Docker images with:

$ docker images
REPOSITORY       TAG     IMAGE ID      CREATED        SIZE
local/example    latest  373de5bd56d3  9 seconds ago  5.58MB

Running Local Analysis

Since I haven’t really done anything with my local Docker image except echo “hello world”, any vulnerabilities found during the analysis will be a reflection on the base image used; in this case docker.io/library/alpine:latest.

I can perform the analysis on the image, passing in the URL to my locally running Anchore Engine, the username (admin), the password (foobar), the path to my Dockerfile, and the full image tag.

$ ./inline_scan.sh analyze -r https://localhost:8228/v1 -u admin -p foobar -f dockerfile local/example:latest
docker.io/anchore/inline-scan:v0.5.0
Saving local/example:latest for local analysis
Successfully prepared image archive -- /tmp/anchore/example:latest.tar

Analyzing local/example:latest...
[MainThread]  [INFO] using fulltag=localbuild/local/example:latest fulldigest=localbuild/local/example@sha256:325116ad4e6211cfec2acaea612b9ae78b2a2768ec71ea37c68e416730c95efa
 Analysis complete!

Sending analysis archive to http://localhost:8228


Cleaning up docker container: c492f64a122a9631eaf616f5018ad22b55379f8595839a9ea1e69fd110a2dfe5

Viewing the Results

After running the analysis, the results are imported into my Anchore Engine running locally and can now be viewed in the Enterprise UI.

After signing in and navigating to “Image Analysis”, I can see my locally built Docker image listed:

When I dig down into the analyzed image, I can see the vulnerability findings from the local analysis as if it were an image pulled from a registry:

Conclusion

I have successfully executed an analysis of a locally built image on an air-gapped network. I hope this overview of the new local image analysis from Anchore was able to provide some insight into it’s recommended use and that the example provided helps you with your container security needs. For more information regarding local image analysis, please see our inline analysis documentation.