Many organizations use CI/CD pipelines to enforce development or security policies. For example, a pipeline may check whether any vulnerable dependencies are included in the build. These pipelines often enforce sophisticated policies, but these policies are often executed and reported away from those who would be responsible to remediate any issues, namely, the developer.
GitOps—executing certain types of scans or verifying policy from within Git-based environments—can be used to keep many issues close to the developer so they can be remediated faster. Through GitOps, organizations can create automation, trigger that automation on a pull request (PR) or other type of event, and enforce checks and policies prior to code being merged.
Cycode Supports GitOps
Cycode can support the GitOps approach in different ways. Natively, Cycode can integrate with pull requests by scanning for secrets in commits, infrastructure-as-code misconfigurations, open source vulnerabilities and license compliance, and CWEs such as the OWASP Top Ten. Certain Git environments can be configured to block the changes from being merged until all the results of these different scans have been remediated.
With just a few clicks, pull request scanning can be enabled on either entire organizations or just a handful of repositories. Blocking further merging can be easily configured as well.
Also, the developer can be given options to remediate the results right from the code review comments within the PR. All that the developer has to do is reply to the code review comment with certain hashtags.
However, we are not limited to the out-of-the-box pull request scanning capabilities of Cycode. Organizations may wish to create custom policies that address their specific concerns.
The Cycode Knowledge Graph is a great utility for creating customized reports and policies. Customized policy violations, however, are reported out-of-band via the alert channels that you can set up inside the Cycode platform. To support GitOps, organizations can create their own automations that execute the knowledge graph queries when certain events occur.
To demonstrate how to do this, I will walk through an example to address an issue brought up by a Cycode client. This client wanted to identify Dockerfiles that were not using the correct base images, or “golden images,” and block PRs if the Dockerfile in that repository wasn’t using that golden image. That way, the developer could address the issue immediately, rather than having an alert get raised, a ticket get created, and the issue get addressed later.
Developing the Knowledge Graph Query
We can use the knowledge graph to find dockerfiles and identify their image dependencies. Then using an API call, we can call that knowledge graph query. That API call can be encapsulated into a GitHub action, which can be set up as a required check for a PR.
The above query will find a list of Dockerfiles and their Image Dependencies, but we need to supply some filters to find the Docker files that aren’t dependent on a given image. To do this, we add a filter to find a particular Image URI, and we have to negate the relationship between the docker file and the image dependency to find the Dockerfiles that do not have that dependency. In this example, I’ve used “alpine:latest” as the golden image.
This is enough to create a policy and get alerts on violations, but it’s not enough to create a GitHub Action and require a check for a Pull Request. We need to enter the Repository and Branch entities as well and filter on names of those entities. Otherwise, the required check will always fail as long as any repository or any branch contains a Dockerfile that is in violation of that policy. We need to keep the policy focused on the repository and branch involved in this PR.
Our knowledge graph query will now look like this:
Creating an API Call
Now that we have our query, we can extract an API call. This feature must be enabled on your Cycode platform, so contact your customer success manager or account manager and ask to get the Knowledge Graph API Usage enabled.
By clicking the API Usage button, you can copy a CURL command that will call that knowledge graph query. The result CURL command in our example is this:
curl 'https://api.cycode.com/graph/api/v1/graph/query?mode=AlertWhen&page_number=0&page_size=50' -H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' -d '{"id":"Y74YnZzuj","connections":[{"id":"x6C7HUn0G","connections":[],"exists":false,"is_optional":false,"resource_type":"container_image_dependency","edge_type":"depends_on","edge_direction":"outbound","filters":[{"mode":"And","filters":[{"name":"image_uri","operator":"Eq","value":"alpine:latest","type":"String"}]}],"variables":[],"edge_filters":[],"parent_resource_type":"dockerfile","edge_opposite_type":"depends_on","edge_type_id":"dockerfile_depends_on_container_image_dependency","sort_by":null,"sort_order":"Asc","limit":null},{"id":"4ICD7OLGf","connections":[{"id":"_4hbYt8YN","connections":[],"exists":true,"is_optional":false,"resource_type":"scm_branch","edge_type":"has","edge_direction":"outbound","filters":[{"mode":"And","filters":[{"name":"name","operator":"Eq","value":"main","type":"String"}]}],"variables":[],"edge_filters":[],"parent_resource_type":"scm_repository","edge_opposite_type":"has","edge_type_id":"repository_has_branch_edge","sort_by":null,"sort_order":"Asc","limit":null}],"exists":true,"is_optional":false,"resource_type":"scm_repository","edge_type":"modified_in","edge_direction":"outbound","filters":[{"mode":"And","filters":[{"name":"name","operator":"Eq","value":"juice-shop","type":"String"}]}],"variables":[],"edge_filters":[],"parent_resource_type":"dockerfile","edge_opposite_type":"modified_in","edge_type_id":"dockerfile_modified_in_repository_edge","sort_by":null,"sort_order":"Asc","limit":null}],"exists":true,"is_optional":false,"resource_type":"dockerfile","edge_type":"","filters":[],"variables":[],"edge_filters":[],"parent_resource_type":"","sort_by":"_key","sort_order":"Asc","limit":-1,"fast_query":true}'
There are a couple of things we need to do to the CURL command in order to get it to work in our GitHub action:
- We need a bearer token to fill in for the <token> placeholder above. In order to get the bearer token, we need to generate a client id and secret from within the Cycode platform.
- We need to parameterize the curl command to substitute the repository name, branch name, and image-uri with parameters that will be provided by the GitHub action so that the results are relevant to that repo and branch.
Generating the Bearer Token
Inside the Cycode platform, go to your user settings and select access Tokens.
Click the “Create New” button. After providing a description of what the token will be used for, you will be presented with a client id and client secret. Copy both of those values into a text file locally, clearly labeling which is which. You will need a client ID and secret for testing your GitHub action. Any consumers of the GitHub Action will need their own client ID and secret, which will be passed as a parameter into the GitHub Action.
Parameterizing the Query
Examining the curl command, we can see that we filtered on Repository Name as juice-shop, the branch as main, and the image-uri as alpine:latest. We also have the token variable that we must parameterize. We need to create variables as placeholders for those filters, and we need to escape them so that the shell script will interpolate those variables’ values as opposed to just using them as part of the string:
Our parameterized curl command looks like this:
curl "https://api.cycode.com/graph/api/v1/graph/query?mode=AlertWhen&page_number=0&page_size=50" -H "Authorization: Bearer $token" -H 'Content-Type: application/json' -d '{"id":"2vPouZAM9","connections":[{"id":"KWzQob34G","connections":[],"exists":false,"is_optional":false,"resource_type":"container_image_dependency","edge_type":"depends_on","edge_direction":"outbound","filters":[{"mode":"And","filters":[{"name":"image_uri","operator":"Eq","value":"'$image_uri'","type":"String"}]}],"variables":[],"edge_filters":[],"parent_resource_type":"dockerfile","edge_opposite_type":"depends_on","edge_type_id":"dockerfile_depends_on_container_image_dependency","sort_by":null,"sort_order":"Asc","limit":null},{"id":"0bpPGrJCe","connections":[{"id":"AmDGCkEYIF","connections":[],"exists":true,"is_optional":false,"resource_type":"scm_branch","edge_type":"has","edge_direction":"outbound","filters":[{"mode":"And","filters":[{"name":"name","operator":"Eq","value":"'$branch_name'","type":"String"}]}],"variables":[],"edge_filters":[],"parent_resource_type":"scm_repository","edge_opposite_type":"has","edge_type_id":"repository_has_branch_edge","sort_by":null,"sort_order":"Asc","limit":null}],"exists":true,"is_optional":false,"resource_type":"scm_repository","edge_type":"modified_in","edge_direction":"outbound","filters":[{"mode":"And","filters":[{"name":"name","operator":"Eq","value":"'$repo_name'","type":"String"}]}],"variables":[],"edge_filters":[],"parent_resource_type":"dockerfile","edge_opposite_type":"modified_in","edge_type_id":"dockerfile_modified_in_repository_edge","sort_by":null,"sort_order":"Asc","limit":null}],"exists":true,"is_optional":false,"resource_type":"dockerfile","edge_type":"","filters":[],"variables":[],"edge_filters":[],"parent_resource_type":"","sort_by":"_key","sort_order":"Asc","limit":-1,"fast_query":true}'
Notice that the variable names are also surrounded by single quotes(‘) – this is to prevent the direct-quoting of those variable names and allow for the values of those variables to serve as substitutes in the query.
Now we can use this in a shell script:
#!/bin/bash # image_uri=$1 repo_name=$2 branch_name=$3 client_id=$4 client_secret=$5 token=$(curl "https://api.cycode.com/api/v1/auth/api-token" -X POST -H "Content-type: application/json" --data '{"clientId": "'$client_id'", "secret": "'$client_secret'"}' | jq -r '.token') dockerfiles=$(curl "https://api.cycode.com/graph/api/v1/graph/query?mode=AlertWhen&page_number=0&page_size=50" -H "Authorization: Bearer $token" -H 'Content-Type: application/json' -d '{"id":"2vPouZAM9","connections":[{"id":"KWzQob34G","connections":[],"exists":false,"is_optional":false,"resource_type":"container_image_dependency","edge_type":"depends_on","edge_direction":"outbound","filters":[{"mode":"And","filters":[{"name":"image_uri","operator":"Eq","value":"'$image_uri'","type":"String"}]}],"variables":[],"edge_filters":[],"parent_resource_type":"dockerfile","edge_opposite_type":"depends_on","edge_type_id":"dockerfile_depends_on_container_image_dependency","sort_by":null,"sort_order":"Asc","limit":null},{"id":"0bpPGrJCe","connections":[{"id":"AmDGCkEYIF","connections":[],"exists":true,"is_optional":false,"resource_type":"scm_branch","edge_type":"has","edge_direction":"outbound","filters":[{"mode":"And","filters":[{"name":"name","operator":"Eq","value":"'$branch_name'","type":"String"}]}],"variables":[],"edge_filters":[],"parent_resource_type":"scm_repository","edge_opposite_type":"has","edge_type_id":"repository_has_branch_edge","sort_by":null,"sort_order":"Asc","limit":null}],"exists":true,"is_optional":false,"resource_type":"scm_repository","edge_type":"modified_in","edge_direction":"outbound","filters":[{"mode":"And","filters":[{"name":"name","operator":"Eq","value":"'$repo_name'","type":"String"}]}],"variables":[],"edge_filters":[],"parent_resource_type":"dockerfile","edge_opposite_type":"modified_in","edge_type_id":"dockerfile_modified_in_repository_edge","sort_by":null,"sort_order":"Asc","limit":null}],"exists":true,"is_optional":false,"resource_type":"dockerfile","edge_type":"","filters":[],"variables":[],"edge_filters":[],"parent_resource_type":"","sort_by":"_key","sort_order":"Asc","limit":-1,"fast_query":true}' | jq '.result[].resource.metadata.file_name') echo "The following Dockerfiles not using golden image of $image_uri:" echo $dockerfiles if [ -z "$dockerfiles" ]; then exit 0 else exit 1 fi
To summarize what the script does, first, as input, it accepts the parameters for the uri of the golden image, the repo name, the branch name, the client id, and the client secret.
Then we use curl and jq to get the bearer token using the client ID and client secret. That bearer token will be used to authenticate to Cycode in any subsequent API calls. That bearer token is used in the following curl command along with the repo name, branch name, and image URI to query any docker images in that repo and branch that do not match the image URI. The jq command is used to extract the image list.
Then the script simply checks to see if there are any images in the list that have been returned. If it is empty, then it is a success – there are no docker files that do not match the golden image. If it isn’t empty, it means that there are docker files that aren’t using the golden image and thus the check has failed.
We can then package this into a docker image that can be used in a GitHub action.
Packaging the Query as a GitHub Action
There are multiple tutorials on developing GitHub actions online. You can start with GitHub’s own documentation: https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions
First, we need an action.yml file to describe the inputs, outputs, and execution method of the GitHub Action:
Our action.yml file looks like this:
# action.yml name: 'Cycode Docker Image Check' description: 'Check if Dockerfiles use the correct base image' inputs: image-uri: # id of input description: 'The name of the base image uri' required: true repo-name: description: 'The name of repository containing the Dockerfiles' required: true default: ${{ github.event.repository.name }} branch-name: description: 'The branch to be examined for Dockerfiles golden images' required: true default: ${{ github.ref_name }} cycode-client-id: description: 'The client id to authenticate to Cycode' required: true cycode-client-secret: description: 'The client id to authenticate to Cycode' required: true outputs: image-list: # id of output description: 'Dockerfiles that do not use the golden image image’ runs: using: 'docker' image: 'Dockerfile' args: - ${{ inputs.image-uri }} - ${{ inputs.repo-name }} - ${{ inputs.branch-name }} - ${{ inputs.cycode-client-id }} - ${{ inputs.cycode-client-secret }}
Because we are using “docker” as the using clause in our action.yml file, we have to create a Dockerfile:
FROM dwdraju/alpine-curl-jq:latest COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh # Code file to execute when the docker container starts up (`entrypoint.sh`) ENTRYPOINT ["https://e5s6t7j5.rocketcdn.me/entrypoint.sh"]
Notice that the Dockerfile uses an entrypoint script, which I’ve titled entrypoint.sh. Copy the shell script we created with the parameterized curl commands into a file called entrypoint.sh
Your github action is now ready to go! You can use it in your organizations, share it amongst your repositories, and create required checks that enforce the usage the correct image.
This exact use case was a real-life example, but it is just that – an example. You can apply similar concepts to many different knowledge graph queries and turn them into GitHub actions or even GitLab runners as long as you include repository and branch as entities, copy the query as a CURL command, and parameterize the CURL command so that it can be reused by different repositories.
Unlock the power of the knowledge graph for real-time custom policy enforcement.
Conclusion
Cycode offers peace of mind from code to cloud through application security posture management (ASPM) ensuring end-to-end security coverage. Learn more here or book a demo.
Originally published: October 3, 2023