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_DIRenvironment 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:
- 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.
- 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
artifactsdirectory in the container filesystem (the path is given as theOCULAR_ARTIFACTS_DIRenvironment variable). - 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.).
- 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:
- 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.
- 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.)
- 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.