The Ocular Manual

About This Guide

Ocular is an extension to Kubernetes, providing a simplified set of resources that allow for easy configuration of static code scanning over a vast amount of software assets.

Ocular allows for the core aspects of code scanning to be configured via containers. This enables security engineers to highly tailor the scanning to the needs of their organization. Ocular additionally ships with a set of default integrations for common use cases.

This document is meant to be a guide for users and implementers, to help them understand the concepts behind Ocular and our implementation. It should help you better understand Ocular’s behavior and some of the design decisions behind it.

This document is NOT intended to be a tutorial overview. For that, see the Getting Started Guide for an easy introduction to Ocular.

Source Code Availability

Source code is available at crashappsec/ocular. See the installation guide for how to download and install Ocular to your Kubernetes cluster. We will be making source code available at the time of our public launch.

Basic Concepts

Ocular is intended to be a dedicated code scanning orchestration system decoupled from continuous integration/continuous deployment (CI/CD) pipelines responsible for code deployment. This separation allows security engineers to not disrupt developer workflows and provide a testbed for security tooling. Ocular allows for executions to be regularly occurring or run ad-hoc and additionally over a large set of software assets.

The system is architected to provide a flexible and configurable framework, enabling users to define:

  • Targets: The specific assets or artifacts to be analyzed.
  • Scanners: The tools and processes utilized for scanning.
  • Result Triage: The method of parsing and managing scan outputs.
  • Enumeration: The means by which you determine which targets to analyze

The most basic unit of execution is a container image, which allows for a high level of customization, so long as those containers conform to the standard specified later in this manual

Use of Kubernetes

Ocular is built atop kubernetes very transparently. We encourage users to use any existing kubernetes to help them monitor or configure scans. If you see a section in this guide prefixed with ‘[K8s]:’, it will describe the kubernetes implementation behind the section.

API Access/Authentication

Ocular is a Kubernetes Controller. “Interacting” with Ocular just means interacting with specific resources on the Kubernetes API. See more here

This means you can create and run Ocular resources via kubectl or any other standard way of talking to the kubernetes API.

Static Resource

Ocular is configured via the definition of a few kubernetes resources, which define some configuration options for the scanning process.

Containers

The system allows customization of many aspects of the application through container definitions. Most resources in Ocular are a superset of the standard kubernetes container

There exists 2 main types of container based resources in the Ocular:

  • Container: A standard container definition that is used to define a container image to run.
  • Container With Parameters: A container definition that is used to define a container image to run, along with a set of parameters that can be passed to the container when it is invoked. It is a superset of the container definition, and adds a set of parameters that can be passed to the container when it is invoked.

The following resources are container definition:

  • Downloaders: Used to define a container image that will download the target content to a specific directory in the container filesystem.
  • Scanner (a subset of Profile): Used to define a container image that will scan the target content and produce artifacts.

The following resources use the container definitions with parameters:

  • Uploaders: Used to define a container image that will upload the artifacts produced by the scanners to a specific location.
  • Crawlers: Used to define a container image that will enumerate targets to scan and call the API to start scans for those targets.

Downloaders

Downloaders are container images (defined by the user) that are used to write a target to disk for scanning. The container images are expected to read the target identifier and optional identifier version from environment variables and then write the target to the current directory. Downloaders will be defined via the API and then can be ran by referencing them when creating a new pipeline (see the pipelines section for more details on how to execute a pipeline).

A Downloader definitions is the same as a User Container definition. To view all options, see the section linked, the example below is provided for reference and does not include all options.

# Example definition of a downloader.
# It can be created via 'kubectl apply -f [filename]'
# This downloader will assume the
# target identifier is a git repository URL
# and optionally read the gitconfig from the
# secret 'downloader-secrets' field 'gitconfig'
apiVersion: ocular.crashoverride.run/v1beta1
kind: Downloader
metadata:
  name: example-downloader
spec:
  container:
    name: git
    image: alpine/git:latest
    imagePullPolicy: IfNotPresent
    command: [ /bin/sh, -c ]
    args:
      - |
        if [ -f /etc/ocular/gitconfig ]; then
          git config --global include.path /etc/ocular/gitconfig
        fi
        git clone $OCULAR_TARGET_IDENTIFIER $OCULAR_TARGET_DIR
    volumeMounts:
      - name:  downloader-secrets
        mountPath: /etc/ocular/
        readOnly: true
  volumes:
    - name: downloader-secrets
      secret:
        optional: true
        secretName: git-downloader-secret
        items:
          - key: gitconfig
            path: gitconfig

Profiles

A profile is a collection of scanners (containers), a list of artifacts that each scanner produces, and a list of uploader names that will be used to upload the artifacts produced by the scanners. A profile will be executed as part of a pipeline, where a target is downloaded to the current working directory, the scanners are executed in parallel followed by the uploaders. Uploaders are defined separately and can be reused across different profiles, see the uploaders section for more information on how to define an uploader. For more information on how to execute a profile, see the pipelines section. Profiles are intended to separate the types of scans and allow for triggering different sets of scans based on the target type, target source, or other criteria.

A profile has 3 components:

  • Scanners: A list of container images that will be executed in parallel to scan the target content. This has the same definition as a Container definition
  • Artifacts: A list of artifacts that the scanners will produce. Each artifact is a file path relative to the ‘results’ directory in the container filesystem. The path to the results directory is provided as the OCULAR_RESULTS_DIR environment variable.
  • Uploaders: A list of uploader names and parameters that will be used to upload the artifacts produced by the scanners. See the uploaders section for more information on how to define an uploader.
# Example definition of a profile.
# It can be created via 'kubectl apply -f [filename]'
# This profile will run trufflehog and semgrep
# and send both their results to the uploaders
# as 'thog.json' and 'semgrep.json' respectively
apiVersion: ocular.crashoverride.run/v1beta1
kind: Profile
metadata:
  name: example-profile
spec:
  containers:
    - name: trufflehog
      image: trufflesecurity/trufflehog:latest
      imagePullPolicy: IfNotPresent
      command: [ /bin/sh, -c ]
      args: [ "trufflehog git file://. -j --no-update > $OCULAR_RESULTS_DIR/thog.json" ]
    - name: semgrep
      image: semgrep/semgrep:latest-nonroot
      imagePullPolicy: IfNotPresent
      command: [ /bin/sh, -c ]
      args: [ "semgrep scan --config=auto --json . > $OCULAR_RESULTS_DIR/semgrep.json" ]
  # Files that are output from each scanner
  artifacts:
    - thog.json
    - semgrep.json
  uploaderRefs:
	# List of uploaders to use for 
	# uploading the artifacts produced by the scanners.
	# Each item is a reference to
	# an uploader resource in the same namespace
	# The uploader must exist or the profile will fail to be created.
	# Additionally, All required parameters for the 
	# uploader must be provided or the profile 
	# will fail to be created.
	# To view the parameters that can be passed 
	# to an uploader, check the definitions by running 
	# 'kubectl describe uploader'
	# for the specific uploader.
	
    - name: example-uploader # uploader created in 'uploaders section'
      parameters:
        - name: EXAMPLE
          value: example value

Uploaders

Uploaders are container images that are used to process or upload data to a another system. The container images are expected to read the files to upload from the paths given to them via command line arguments and then perform the upload operation on those files. Uploaders will be defined via the API and then can be ran by referencing them in a profile definition (see the profiles sections for more details on how to define a profile).

An uploader is a container with parameters (see containers section).

# Example definition of an uploader.
# It can be created via 'kubectl apply -f [filename]'
# This uploader will print the value of the parameter
# Then for each file, print the name and contents of the file
apiVersion: ocular.crashoverride.run/v1beta1
kind: Uploader
metadata:
  name: example-uploader
spec:
  container:
    name: example-uploader
    image: ubuntu:latest
    command: ["/bin/bash", "-c"] 
	args:
		- | 
			echo param is $OCULAR_PARAM_EXAMPLE
			ls -al $OCULAR_RESULTS_DIR
			for fileArg in \"${@:1}\"; do
				echo reading '$fileArg'
				cat $fileArg
			done
  parameters:
    - name: "EXAMPLE"
      description: "My example parameter."
      required: true
    - name: "OPTIONAL_PARAM"
      description: "An optional parameter."
      required: false

Crawlers

Crawlers are container images that are used to enumerate targets to scan. The container is expected to gather a set of targets to scan, then create pipelines for each. The container will be set with a service account that has access to pipelines and searches The crawlers can be run on a schedule or on demand, and can be configured to pass a set of parameters when invoked. For more information on how to execute a crawler, see the searches section.

A crawler is a container with parameters (see containers section).

# Example definition of a crawler.
# It can be created via 'kubectl apply -f [filename]'
# This crawler will start one pipeline from the parameters
# given.
apiVersion: ocular.crashoverride.run/v1beta1
kind: Crawler
metadata:
  name: example-crawler
spec:
  container:
    name: example-uploader
    image: bitnami/kubectl:latest
    command: [ "/bin/bash", "-c"] 
	args: 
		- |
			echo running search $OCULAR_SEARCH_NAME
			cat <<EOF | kubectl create -f -
			apiVersion: ocular.crashoverride.run/v1beta1
			kind: Pipeline
			metadata:
				generate-name: example-pipeline-
			spec:
				downloaderRef:
					name: "$OCULAR_PARAM_DOWNLOADER"
				profileRef:
					name: "$OCULAR_PARAM_PROFILE"
				target:
					identifier: "$OCULAR_PARAM_TARGET_ID"
	        EOF
  parameters:
    - name: "DOWNLOADER"
      description: "The downloader to use"
      required: true
    - name: "PROFILE"
      description: "The profile to use"
      required: true
    - name: "TARGET_ID"
      description: "The target to scan"
      required: true

Execution Resources

Executions represent the actual execution of the containers to either complete a scan (pipelines) or to find targets (search).

Pipelines

Pipelines are the core of the Ocular system. They are used to download target content, run scanners on it, and upload the results to 3rd party systems. When triggering a pipeline, the user will provide a target identifier (e.g. a URL or a git repository), an optional target version, the name of the downloader to use, and a profile to run. The pipeline will then execute the following steps:

  1. Download: The pipeline will run the specified downloader with the target identifier and version set as environment variables. The downloader is expected to download the target content to its current working directory. Once the container exits (with code 0), the pipeline will proceed to the next step.
  2. Scan: The pipeline will run the scanners specified by the provided profile, which are run in parallel. Each scanner will be executed in its own container (but still on the same pod), with the current working directory set to the directory where the downloader wrote the target content. The scanners should produce artifacts, and send them to the artifacts directory in the container filesystem (the path is given as the OCULAR_ARTIFACTS_DIR environment variable).
  3. Upload: Once all scanners have completed, the pipeline will extract the artifacts (listed in the profile) and run the uploaders in parallel. The uploaders will be passed the paths of the artifacts produced by the scanners as command line arguments. The uploaders are expected to upload the artifacts to a specific location (e.g. a database, cloud storage, etc.).
  4. Complete: Once all uploaders have completed, the pipeline will be considered complete.

[K8s]: A pipeline is executed via 2 kubernetes jobs. One for the scanning that has the downloader as an init container, and the ‘scanners’ section of the profile as the main containers. The other job is the upload job, where the uploaders are the main containers. We transfer the artifact files between the 2 using a side car container. If there are no uploaders defined, only 1 kubernetes job will be created for the scanners and downlodaer

# Example definition of a pipeline
# It can be created via 'kubectl create -f [filename]'
# This pipeline will download the target identifier using
# the downloader and profile defined in previous sections
apiVersion: ocular.crashoverride.run/v1beta1
kind: Pipeline
metadata:
	# For execution resources we recommend using
	# 'generate-name' since two pipelines cannot
	# have the same name. generate name will add
	# a random suffix, meaning you can run two
	# pipelines for scanning the diffeerent targets
	# with the same profile by just changing the identifier
	generate-name: example-pipeline-
spec:
	downloaderRef:
		name: "$OCULAR_PARAM_DOWNLOADER"
	profileRef:
		name: "$OCULAR_PARAM_PROFILE"
	target:
		identifier: "$OCULAR_PARAM_TARGET_ID"
	ttlSecondsAfterFinish: 180 # delete 3 minutes after it finishes

Searches

Searches are used to find targets that can be scanned by the pipeline. They are typically used to discover new targets or to find targets that match certain criteria. Searches are executed by running a crawler, which is a container image that is expected to gather a set of targets to scan and create pipeline resources to scan each one. The running container will be given a service account that has the ability to create, update, delete both pipelines and searches within the same namespace.

The search will execute the following steps:

  1. Run Crawler: The search will run the specified crawler with the parameters provided in the request. The crawler is expected to gather a set of targets to scan and call the API to start scans for those targets.
  2. Start Pipelines: Once the crawler has gathered the targets, it should call the API to start pipelines for each target. The pipelines will execute as normal (see pipelines for more details). NOTE: crawlers should space out the pipeline creation to avoid overwhelming the system with too many pipelines at once. (A solution to this is actively being worked on, but currently the crawler should implement its own throttling logic.)
  3. Complete: Once the crawler has completed, the search will be considered complete.
# Example definition of a search 
# It can be created via 'kubectl create -f [filename]'
# The search invokes the 'example-crawler'
# created in a previous section
apiVersion: ocular.crashoverride.run/v1beta1
kind: Search
metadata:
  # For execution resources we recommend using
  # 'generate-name' since two pipelines cannot
  # have the same name. generate name will add
  # a random suffix, meaning you can run two
  # searches for enumerating different targets
  # just changing the parameters
  generate-name: example-search-
spec:
  crawlerRef:
    name: example-crawler
    parameters:
      - name: "TARGET_ID"
        value: "https://github.com/crashappsec/ocular"
	  - name: "DOWNLOADER"
	    value: "example-downloader"
	  - name: "PROFILE"
	    value: "example-profile"
  ttlSecondsAfterFinished: 180 # delete 3 minutes after finished

If you want to instead run this search every day, you can use the resource CronSearch to have searches created on a cron schedule

If the search is scheduled, the request should use the endpoint POST /api/v1/scheduled/searches with the addition of the schedule field, which is a cron expression that defines when the search should be executed.

# Example definition of a cron search 
# It can be created via 'kubectl apply -f [filename]'
# The cron search invokes the 'example-crawler'
# created in a previous section
apiVersion: ocular.crashoverride.run/v1beta1
kind: CronSearch
metadata:
	name: "daily-search"
spec:
  schedule: "0 0 * * *" # every day at midnight
  template:
	crawlerRef:
		name: example-crawler
		parameters:
			- name: "TARGET_ID"
			  value: "https://github.com/crashappsec/ocular"
			- name: "DOWNLOADER"
			  value: "example-downloader"
			- name: "PROFILE"
			  value: "example-profile"
     ttlSecondsAfterFinished: 180 # delete 3 minutes after finished

Default Integrations

Ocular comes bundled with a set of default integrations meant to solve a lot of common use-cases of Ocular. Currently Ocular bundles default uploaders, downloaders and crawlers, along side the Helm chart installation.

The source code of these implementations can be found in the GitHub repository crashappsec/ocular-default-integrations.

For the most up to date documentation on each of these, be sure to get the definition of the integration using the command kubectl describe (crawler|uploader|downloader) ocular-defaults-$NAME" or view the definitions in the GitHub repository

Debugging Techniques

Since Ocular is built using kubernetes, we encourage you to use kubernetes tooling to debug.

A K8s client may be useful for looking at logs of jobs, or viewing events for why an image might not be pulled or failed to start.

Ocular is open-source to allow users as much control over their scanning as possible.