Skip to content

Commit

Permalink
feat: support autofixing (#28)
Browse files Browse the repository at this point in the history
* feat: support autofixing files

This change adds support for autofixing files. This is achieved by
expanding the script that runs as the Docker entrypoint to run the
`kubescape fix` command if a user requests it. It also validates that
the prerequisites to fix the files are met: a scan must include a JSON
output.

* fix: use proper input variable

* chore!: remove artifacts to test

* fix: specify JSON format for fixing files

* refactor: use proper string non-zero checks

* refactor: implement most ShellCheck suggestions

* style: lowercase all script-local variables

* style: use curly brackets for variable expansion

* refactor: use native mechanism to skip confirmation

* feat: automatically add JSON to formats when fixes are requested

* feat: do not enforce severity threshold if fixes requested

* docs: document how to use autofixing

* docs: update `outputFile` usage for multiple outputs

This change specifies that `outputFile` should omit the extension.

* docs: mention artifacts TODO in entrypoint

* fix: do not suggest autofixes on push in README

Co-authored-by: David Wertenteil <[email protected]>

* fix: do not suggest fixes on push in example workflow

* docs: mention the autofixing workflow limitations

Co-authored-by: David Wertenteil <[email protected]>
  • Loading branch information
vladklokun and David Wertenteil authored Jan 18, 2023
1 parent a978d98 commit 1e1cf90
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 37 deletions.
38 changes: 37 additions & 1 deletion .github/workflows/example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,40 @@ jobs:
- name: Upload Kubescape scan results to Github Code Scanning
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: results.sarif
sarif_file: results.sarif

---

name: Suggest autofixes with Kubescape
on: [pull_request]
jobs:
kubescape:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Get changed files
id: changed-files
uses: tj-actions/[email protected]
- uses: kubescape/github-action@main
with:
account: ${{secrets.KUBESCAPE_ACCOUNT}}
files: ${{ steps.changed-files.outputs.all_changed_files }}
fixFiles: true
format: "sarif"
- uses: peter-evans/create-pull-request@v4
with:
add-paths: |
*.yaml
commit-message: "chore: fix K8s misconfigurations"
title: "[Kubescape] chore: fix K8s misconfigurations"
body: |
# What this PR changes
[Kubescape](https://github.com/kubescape/kubescape) has found misconfigurations in the targeted branch. This PR fixes the misconfigurations that have automatic fixes available.
You may still need to fix misconfigurations that do not have automatic fixes.
base: ${{ github.head_ref }}
branch: kubescape-auto-fix-${{ github.head_ref || github.ref_name }}
delete-branch: true
60 changes: 53 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ You need to make sure that workflows have [Read and write permissions](https://d

## Usage

### Scanning with Kubescape

To scan your repository with [Kubescape in your Github workflow](https://www.armosec.io/blog/kubescape-now-integrates-with-github-actions/?utm_source=github&utm_medium=repository), add the following steps to your workflow configuration:

```yaml
Expand All @@ -21,7 +23,7 @@ jobs:
continue-on-error: true
with:
format: sarif
outputFile: results.sarif
outputFile: results
# # Optional: Specify the Kubescape cloud account ID
# account: ${{secrets.KUBESCAPE_ACCOUNT}}
# # Optional: Scan a specific path. Default will scan the whole repository
Expand All @@ -35,12 +37,56 @@ jobs:
This workflow definition scans your repository with Kubescape and publishes the results to Github.
You can then see the results in the Pull Request that triggered the scan and the _Security → Code scanning_ tab.
### Automatically Suggest Fixes
To make Kubescape automatically suggest fixes to your pushes and pull requests, use the following workflow:
```yaml
name: Suggest autofixes with Kubescape
on: [pull_request]
jobs:
kubescape:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Get changed files
id: changed-files
uses: tj-actions/[email protected]
- uses: kubescape/github-action@main
with:
account: ${{secrets.KUBESCAPE_ACCOUNT}}
files: ${{ steps.changed-files.outputs.all_changed_files }}
fixFiles: true
format: "sarif"
- uses: peter-evans/create-pull-request@v4
with:
add-paths: |
*.yaml
commit-message: "chore: fix K8s misconfigurations"
title: "[Kubescape] chore: fix K8s misconfigurations"
body: |
# What this PR changes
[Kubescape](https://github.com/kubescape/kubescape) has found misconfigurations in the targeted branch. This PR fixes the misconfigurations that have automatic fixes available.
You may still need to fix misconfigurations that do not have automatic fixes.
base: ${{ github.head_ref }}
branch: kubescape-auto-fix-${{ github.head_ref || github.ref_name }}
delete-branch: true
```
Please note that since Kubescape provides automatic fixes only to the rendered YAML manifests, the workflow above will not produce correct fixes for Helm charts.
The next important thing to note is that Kubescape only fixes the files. It does not open pull requests on its own. In the example above, a separate step that runs a different action opens the appropriate pull request. Due to how Github works, there are [limitations](https://github.com/peter-evans/create-pull-request/blob/main/docs/concepts-guidelines.md#triggering-further-workflow-runs) on running and opening pull requests to forks. The action running in this step is maintained by its respective maintainers, and not the Kubescape team, so you should review its documentation when troubleshooting the process of triggering the workflow run and opening pull requests.
## Inputs
| Name | Description | Required |
| --- | --- | ---|
| files | YAML files or Helm charts to scan for misconfigurations. The files need to be provided with the complete path from the root of the repository. | No (default is `.` which scans the whole repository) |
| outputFile | Name of the output file where the scan result will be stored. | No (default is `results.out`) |
| outputFile | Name of the output file where the scan result will be stored without the extension. | No (default is `results`) |
| frameworks | Security framework(s) to scan the files against. Multiple frameworks can be specified separated by a comma with no spaces. Example - `nsa,devopsbest`. Run `kubescape list frameworks` in the [Kubescape CLI](https://hub.armo.cloud/docs/installing-kubescape) to get a list of all frameworks. Either frameworks have to be specified or controls. | No |
| controls | Security control(s) to scan the files against. Multiple controls can be specified separated by a comma with no spaces. Example - `Configured liveness probe,Pods in default namespace`. Run `kubescape list controls` in the [Kubescape CLI](https://hub.armo.cloud/docs/installing-kubescape) to get a list of all controls. You can use either the complete control name or the control ID such as `C-0001` to specify the control you want use. You must specify either the control(s) or the framework(s) you want used in the scan. | No |
| account | Account ID for [Kubescape cloud](https://cloud.armosec.io/). Used for custom configuration, such as frameworks, control configuration, etc. | No |
Expand All @@ -64,7 +110,7 @@ jobs:
continue-on-error: true
with:
format: sarif
outputFile: results.sarif
outputFile: results
# Specify the Kubescape cloud account ID
account: ${{secrets.KUBESCAPE_ACCOUNT}}
- name: Upload Kubescape scan results to Github Code Scanning
Expand All @@ -89,7 +135,7 @@ jobs:
continue-on-error: true
with:
format: sarif
outputFile: results.sarif
outputFile: results
# Scan a specific path. Default will scan the whole repository
files: "examples/kubernetes-manifests/*.yaml"
- name: Upload Kubescape scan results to Github Code Scanning
Expand All @@ -114,7 +160,7 @@ jobs:
continue-on-error: true
with:
format: sarif
outputFile: results.sarif
outputFile: results
frameworks: |
nsa,mitre
- name: Upload Kubescape scan results to Github Code Scanning
Expand All @@ -139,7 +185,7 @@ jobs:
continue-on-error: false
with:
format: sarif
outputFile: results.sarif
outputFile: results
failedThreshold: 50
- name: Upload Kubescape scan results to Github Code Scanning
uses: github/codeql-action/upload-sarif@v2
Expand All @@ -162,7 +208,7 @@ jobs:
continue-on-error: false
with:
format: sarif
outputFile: results.sarif
outputFile: results
severityThreshold: medium
- name: Upload Kubescape scan results to Github Code Scanning
uses: github/codeql-action/upload-sarif@v2
Expand Down
27 changes: 24 additions & 3 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ inputs:
required: false
outputFile:
description: |
Name of the output file. Default is "results.out".
Name of the output file, without the extension.
Default is "results".
Kubescape adds the appropriate extension automatically to support both
single and multiple output formats.
required: false
frameworks:
description: |
Expand All @@ -42,10 +47,26 @@ inputs:
description: |
Output format.
Can accept multiple comma-separated formats, e.g. "sarif,json,html"
Run `kubescape scan -h` for listing supported formats
Can take one or more formats. To use one format, omit the comma, e.g
`format: json`. To produce results in multiple formats, separate them with
a comma: `format: sarif,json`.
For example, when using `output: "results"` and `format: "sarif,json"`,
Kubescape will produce 2 files: `results.sarif` and `results.json`. You
can then use `results.sarif` to publish results to Github Code Scanning
and `results.json` to suggest automatic fixes.
Run `kubescape scan -h` to see a list of supported formats.
required: false
default: junit
fixFiles:
description: |
Whether Kubescape will automatically fix files or not.
If enabled, Kubescape will make fixes to the input files. You can then
use these fixes to open Pull Requests from your CI/CD pipeline.
required: false
default: false
runs:
using: docker
image: Dockerfile
Expand Down
98 changes: 72 additions & 26 deletions entrypoint.sh
Original file line number Diff line number Diff line change
@@ -1,50 +1,96 @@
#!/bin/sh

# Checks if `string` contains `substring`.
#
# Arguments:
# String to check.
#
# Returns:
# 0 if `string` contains `substring`, otherwise 1.
contains() {
case "$1" in
*$2*) return 0 ;;
*) return 1 ;;
esac
}

set -e

# Declear ks client
# Kubescape uses the client name to make a request for checking for updates
export KS_CLIENT="github_actions"

if [ ! -z "$INPUT_FRAMEWORKS" ] && [ ! -z "$INPUT_CONTROLS" ]; then
echo "Framework and Control is specified. Please specify either one of them or neither"
exit 1
if [ -n "${INPUT_FRAMEWORKS}" ] && [ -n "${INPUT_CONTROLS}" ]; then
echo "Framework and Control is specified. Please specify either one of them or neither"
exit 1
fi

# Split the controls by comma and concatenate with quotes around each control
if [ ! -z "$INPUT_CONTROLS" ]; then
CONTROLS=""
if [ -n "${INPUT_CONTROLS}" ]; then
controls=""
set -f; IFS=','
set -- $INPUT_CONTROLS
set -- "${INPUT_CONTROLS}"
set +f; unset IFS
for control in "$@"
do
control=$(echo $control | xargs) # Remove leading/trailing whitespaces
CONTROLS="$CONTROLS\"$control\","
control=$(echo "${control}" | xargs) # Remove leading/trailing whitespaces
controls="${controls}\"${control}\","
done
CONTROLS=$(echo "${CONTROLS%?}")
controls=$(echo "${controls%?}")
fi

frameworks_cmd=$([ -n "${INPUT_FRAMEWORKS}" ] && echo "framework ${INPUT_FRAMEWORKS}" || echo "")
controls_cmd=$([ -n "${INPUT_CONTROLS}" ] && echo control "${controls}" || echo "")

files=$([ -n "${INPUT_FILES}" ] && echo "${INPUT_FILES}" || echo .)

output_formats="${INPUT_FORMAT}"
have_json_format="false"
if [ -n "${output_formats}" ] && contains "${output_formats}" "json"; then
have_json_format="true"
fi

# Subcommands
ARTIFACTS_PATH="/home/ks/.kubescape"
FRAMEWORKS_CMD=$([ ! -z "$INPUT_FRAMEWORKS" ] && echo "framework $INPUT_FRAMEWORKS" || echo "")
CONTROLS_CMD=$([ ! -z "$INPUT_CONTROLS" ] && echo control $CONTROLS || echo "")
should_fix_files="false"
if [ "${INPUT_FIXFILES}" = "true" ]; then
should_fix_files="true"
fi

# If a user requested Kubescape to fix their files, but forgot to ask for JSON
# output, do it for them
if [ "${should_fix_files}" = "true" ] && [ "${have_json_format}" != "true" ]; then
output_formats="${output_formats},json"
fi

# Files to scan
FILES=$([ ! -z "$INPUT_FILES" ] && echo "$INPUT_FILES" || echo .)
output_file=$([ -n "${INPUT_OUTPUTFILE}" ] && echo "${INPUT_OUTPUTFILE}" || echo "results")

# Output file name
OUTPUT_FILE=$([ ! -z "$INPUT_OUTPUTFILE" ] && echo "$INPUT_OUTPUTFILE" || echo "results.out")
account_opt=$([ -n "${INPUT_ACCOUNT}" ] && echo --account "${INPUT_ACCOUNT}" || echo "")

# Command-line options
ACCOUNT_OPT=$([ ! -z "$INPUT_ACCOUNT" ] && echo --account $INPUT_ACCOUNT || echo "")
# If account ID is empty, we load artifacts from the local path, otherwise we
# load from the cloud (this will enable custom framework support)
artifacts_path="/home/ks/.kubescape"
artifacts_opt=$([ -n "${INPUT_ACCOUNT}" ] && echo "" || echo --use-artifacts-from "${artifacts_path}")

# If account ID is empty, we load artifacts from the local path, otherwise we load from the cloud (this will enable custom framework support)
ARTIFACTS=$([ ! -z "$INPUT_ACCOUNT" ] && echo "" || echo --use-artifacts-from $ARTIFACTS_PATH)
fail_threshold_opt=$([ -n "${INPUT_FAILEDTHRESHOLD}" ] && echo --fail-threshold "${INPUT_FAILEDTHRESHOLD}" || echo "")

FAIL_THRESHOLD_OPT=$([ ! -z "$INPUT_FAILEDTHRESHOLD" ] && echo --fail-threshold $INPUT_FAILEDTHRESHOLD || echo "")
SEVERITY_THRESHOLD_OPT=$([ ! -z "$INPUT_SEVERITYTHRESHOLD" ] && echo --severity-threshold $INPUT_SEVERITYTHRESHOLD || echo "")
# When a user requests to fix files, the action should not fail because the
# results exceed severity. This is subject to change in the future.
severity_threshold_opt=$(\
[ -n "${INPUT_SEVERITYTHRESHOLD}" ] \
&& [ "${should_fix_files}" = "false" ] \
&& echo --severity-threshold "${INPUT_SEVERITYTHRESHOLD}" \
|| echo "" \
)

COMMAND="kubescape scan $FRAMEWORKS_CMD $CONTROLS_CMD $FILES $ACCOUNT_OPT $FAIL_THRESHOLD_OPT $SEVERITY_THRESHOLD_OPT --format $INPUT_FORMAT --output $OUTPUT_FILE $ARTIFACTS"
# The `kubescape fix` subcommand requires the latest "json" format version.
# Other formats ignore this flag.
format_version_opt="--format-version v2"

eval $COMMAND
# TODO: include artifacts_opt once https://github.com/kubescape/kubescape/issues/1040 is resolved
scan_command="kubescape scan ${frameworks_cmd} ${controls_cmd} ${files} ${account_opt} ${fail_threshold_opt} ${severity_threshold_opt} --format ${output_formats} ${format_version_opt} --output ${output_file}"

echo "${scan_command}"
eval "${scan_command}"

if [ "$should_fix_files" = "true" ]; then
fix_command="kubescape fix --no-confirm ${output_file}.json"
eval "${fix_command}"
fi

0 comments on commit 1e1cf90

Please sign in to comment.