Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/example-fix-pr-review.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ jobs:
persist-credentials: false
# Scan the workspace rather than interpolating changed filenames into inputs.
# Fork contents are analyzed without repository secrets or write permissions.
- uses: kubescape/github-action@main
# Exercise this checkout's action implementation in repository CI.
- uses: ./
with:
files: .
fixFiles: true
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/example-scan-image.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ jobs:
security-events: write
steps:
- uses: actions/checkout@v3
- uses: kubescape/github-action@main
# Exercise this checkout's action implementation in repository CI.
- uses: ./
continue-on-error: true
with:
image: quay.io/kubescape/kubescape:v3.0.3
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/example-scan.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ jobs:
security-events: write
steps:
- uses: actions/checkout@v3
- uses: kubescape/github-action@main
# Exercise this checkout's action implementation in repository CI.
- uses: ./
continue-on-error: true
with:
format: sarif
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@ jobs:
run: bash tests/entrypoint_test.sh
- name: Run action command-construction tests
run: bash tests/action_test.sh
- name: Run report finalization tests
run: bash tests/report_test.sh
- name: Run container report-access tests
run: bash tests/report-container_test.sh
4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,11 @@ runs:
INPUT_ARTIFACTS: ${{ inputs.artifacts }}
KUBESCAPE_VERSION: ${{ steps.resolve_version.outputs.version }}
run: |
# Reports stay owner-only; create them as the runner that consumes them.
# Numeric users need a writable HOME even without an /etc/passwd entry.
docker run --rm \
--user "$(id -u):$(id -g)" \
-e HOME=/tmp \
-e GITHUB_ACTIONS=true \
-e GITHUB_WORKSPACE=/github/workspace \
-e GITHUB_REPOSITORY \
Expand Down
47 changes: 37 additions & 10 deletions entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -129,21 +129,25 @@ fi

# Do not log arguments containing account or registry credentials.
echo "Running Kubescape scan"
"${scan_command[@]}"
scan_status=0
"${scan_command[@]}" || scan_status=$?

# Post-processing for SARIF to ensure relative paths and remove results with empty URIs
if contains "${output_formats}" "sarif"; then
# Use a subshell so its temporary-file cleanup cannot affect the caller's traps.
normalize_sarif() (
actual_sarif="${output_file}"
if [ ! -f "${actual_sarif}" ] && [ -f "${output_file}.sarif" ]; then
actual_sarif="${output_file}.sarif"
fi

if [ -f "${actual_sarif}" ]; then
echo "Normalizing paths and filtering invalid results in ${actual_sarif}..."
# mktemp keeps report contents private regardless of the caller's umask.
sarif_temp=$(mktemp -- "${actual_sarif}.XXXXXX") || exit 1
trap 'rm -f -- "${sarif_temp}"' EXIT

# 1. Clean up URIs
# 2. Filter out results that have an empty URI (which GitHub rejects)
jq '
if ! jq -e '
walk(if type == "object" and has("uri") and (.uri | type == "string") then
.uri |= sub("^file:///github/workspace/"; "") |
.uri |= sub("^/github/workspace/"; "") |
Expand All @@ -155,21 +159,44 @@ if contains "${output_formats}" "sarif"; then
.locations[0].physicalLocation.artifactLocation.uri != "" and
.locations[0].physicalLocation.artifactLocation.uri != null
))
' "${actual_sarif}" > "${actual_sarif}.tmp" && mv "${actual_sarif}.tmp" "${actual_sarif}"
' -- "${actual_sarif}" > "${sarif_temp}"; then
echo "Failed to normalize SARIF file ${actual_sarif}" >&2
exit 1
fi
mv -f -- "${sarif_temp}" "${actual_sarif}" || exit 1

echo "Processing complete. Final URI list:"
jq -r '.runs[].results[].locations[0].physicalLocation.artifactLocation.uri' "${actual_sarif}" | head -n 20
jq -r '.runs[].results[].locations[0].physicalLocation.artifactLocation.uri' -- "${actual_sarif}" | head -n 20
else
echo "Warning: SARIF file ${output_file} not found."
echo "SARIF file ${output_file} not found." >&2
exit 1
fi
)

# Threshold failures may still produce useful reports. Finalize them before
# returning the scanner's status, without turning a failed scan into success.
finalization_status=0
if contains "${output_formats}" "sarif"; then
normalize_sarif || finalization_status=$?
fi

if [ "$should_fix_files" = "true" ]; then
if contains "${output_formats}" "json"; then
json_file="${output_file}"
if [ ! -f "${json_file}" ] && [ -f "${output_file}.json" ]; then
json_file="${output_file}.json"
fi
if [ -f "${json_file}" ]; then
kubescape fix --no-confirm "${json_file}"
if [ ! -f "${json_file}" ]; then
echo "JSON file ${output_file} not found." >&2
finalization_status=1
fi
fi

if [ "${scan_status}" -ne 0 ]; then
exit "${scan_status}"
fi
if [ "${finalization_status}" -ne 0 ]; then
exit "${finalization_status}"
fi
if [ "$should_fix_files" = "true" ]; then
kubescape fix --no-confirm "${json_file}"
fi
49 changes: 49 additions & 0 deletions tests/action_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,55 @@ else
fail "composite action keeps inputs data-only"
fi

if grep -Fxq -- '--user' "${args_file}" &&
grep -Fxq -- "$(id -u):$(id -g)" "${args_file}" &&
grep -Fxq -- 'HOME=/tmp' "${args_file}"; then
pass "container uses the runner identity with a writable home"
else
fail "container uses the runner identity with a writable home"
fi

# All report-producing CI examples must exercise the checkout under test rather
# than a separately downloaded upstream action that may not contain the fix.
for workflow in example-scan.yaml example-scan-image.yaml example-fix-pr-review.yaml; do
workflow_file="${repo_root}/.github/workflows/${workflow}"
if grep -Eq '^ - uses: \./$' "$workflow_file" &&
! grep -Eq '^ - uses: kubescape/github-action@' "$workflow_file"; then
pass "${workflow} scans with the checked-out action"
else
fail "${workflow} scans with the checked-out action"
fi
done

# Exercise the container test's default Docker route without a daemon. The fake
# runtime stops the first run deliberately; reaching it proves that the inner
# Docker shim delegates instead of resolving itself again through PATH.
probe_bin="${test_root}/runtime-probe/bin"
probe_args="${test_root}/runtime-probe/args"
mkdir -p "$probe_bin"
cat > "${probe_bin}/docker" <<'PROBE'
#!/bin/bash
if [ "$1" = run ]; then
printf '%s\n' "$@" > "$PROBE_ARGS_FILE"
exit 88
fi
exit 0
PROBE
chmod +x "${probe_bin}/docker"
probe_status=0
timeout 5s env -u CONTAINER_RUNTIME \
PATH="${probe_bin}:${PATH}" PROBE_ARGS_FILE="$probe_args" \
bash "${repo_root}/tests/report-container_test.sh" \
> "${test_root}/runtime-probe/log" 2>&1 || probe_status=$?
if [ "$probe_status" -eq 1 ] && [ -f "$probe_args" ] &&
[ "$(grep -Fxc -- run "$probe_args")" -eq 1 ] &&
grep -Fxq -- 'TEST_UMASK=0022' "$probe_args" &&
grep -Fxq -- 'INPUT_OUTPUTFILE=reports with spaces/nested/results' "$probe_args"; then
pass "default Docker container test reaches the configured runtime once"
else
fail "default Docker container test reaches the configured runtime once"
fi

# Version input must not introduce shell code or container tag syntax.
version_script="${test_root}/version.sh"
awk '
Expand Down
131 changes: 131 additions & 0 deletions tests/report-container_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#!/bin/bash

# Exercise the composite action's actual run script across a container/runner
# filesystem boundary. The scanner fixture models the CLI's 0600/0750 policy;
# tests/report_test.sh covers finalization failures separately.
set -euo pipefail
repo_root=${REPO_UNDER_TEST:-$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd -P)}
runtime=${CONTAINER_RUNTIME:-docker}
# Resolve before the scan subshell shadows docker on PATH with its test shim.
runtime_bin=$(command -v "$runtime")
test_root=$(mktemp -d)
image=
cleanup() {
if [ -n "$image" ]; then
"$runtime_bin" image rm "$image" > /dev/null 2>&1 || true
fi
if [ "$runtime" = podman ]; then
"$runtime_bin" unshare rm -rf -- "$test_root"
else
rm -rf -- "$test_root"
fi
}
trap cleanup EXIT
mkdir -p "${test_root}/context" "${test_root}/bin"
cp "${repo_root}/entrypoint.sh" "${test_root}/context/entrypoint.sh"
cat > "${test_root}/context/Dockerfile" <<'DOCKERFILE'
FROM alpine:latest
RUN apk add --no-cache bash git jq
COPY entrypoint.sh /entrypoint.sh
COPY kubescape /usr/local/bin/kubescape
ENTRYPOINT ["/entrypoint.sh"]
DOCKERFILE
cat > "${test_root}/context/kubescape" <<'SCANNER'
#!/bin/bash
set -e
umask "${TEST_UMASK:-0022}"
if [ "$1" = fix ]; then
printf '%s\n' fixed >> manifests/pod.yaml
exit 0
fi
output=results
threshold=
while [ "$#" -gt 0 ]; do
case "$1" in
--output) output=$2; shift ;;
--severity-threshold) threshold=$2; shift ;;
esac
shift
done
mkdir -p "$HOME/.cache" "$(dirname -- "$output")"
if [ "${TEST_UMASK:-0022}" = 0077 ]; then
chmod 700 "$(dirname -- "$output")"
else
chmod 750 "$(dirname -- "$output")"
fi
printf '%s\n' cached > "$HOME/.cache/scanner"
printf '%s\n' '{"runs":[{"results":[{"locations":[{"physicalLocation":{"artifactLocation":{"uri":"/github/workspace/manifests/pod.yaml"}}}]}]}]}' > "${output}.sarif"
printf '%s\n' '{}' > "${output}.json"
chmod 600 "${output}.sarif" "${output}.json"
[ "$threshold" != high ]
SCANNER
chmod +x "${test_root}/context/kubescape"
image="kubescape-action:report-test-$$"
"$runtime_bin" build -q -t "$image" "${test_root}/context"

awk '
/^ - name: Run Kubescape scan$/ { in_step=1; next }
in_step && /^ run: \|$/ { in_run=1; next }
in_run && /^ / { sub(/^ /, ""); print; next }
in_run { exit }
' "${repo_root}/action.yml" > "${test_root}/run.sh"

# Podman keep-id models a rootful Docker bind mount: explicit --user wins;
# without it, force the image's root default rather than keep-id's user default.
cat > "${test_root}/bin/docker" <<'RUNTIME'
#!/bin/bash
command=$1
shift
if [ "$CONTAINER_RUNTIME" = podman ]; then
exec "$CONTAINER_RUNTIME_BIN" "$command" --security-opt label=disable --userns=keep-id --user 0:0 \
-e "TEST_UMASK=$TEST_UMASK" "$@"
fi
exec "$CONTAINER_RUNTIME_BIN" "$command" -e "TEST_UMASK=$TEST_UMASK" "$@"
RUNTIME
chmod +x "${test_root}/bin/docker"

for threshold in critical high autofix; do
for mask in 0022 0077; do
workspace="${test_root}/${threshold}-${mask}"
mkdir -p "${workspace}/manifests"
# Allow both baseline root and candidate runner to create outputs, so a
# baseline failure is report access, not inability to start the scan.
chmod 777 "$workspace"
printf '%s\n' original > "${workspace}/manifests/pod.yaml"
report='reports with spaces/nested/results'
status=0
(
export CONTAINER_RUNTIME=$runtime PATH="${test_root}/bin:${PATH}"
export CONTAINER_RUNTIME_BIN=$runtime_bin
export TEST_UMASK=$mask
export GITHUB_WORKSPACE=$workspace KUBESCAPE_VERSION="report-test-$$"
export INPUT_ACCOUNT='' INPUT_ACCESSKEY='' INPUT_SERVER='' INPUT_FAILEDTHRESHOLD=''
export INPUT_COMPLIANCETHRESHOLD='' INPUT_EXCEPTIONS='' INPUT_CONTROLSCONFIG=''
export INPUT_ARTIFACTS='' INPUT_FRAMEWORKS=nsa INPUT_CONTROLS='' INPUT_FILES=manifests
export INPUT_IMAGE='' INPUT_REGISTRYUSERNAME='' INPUT_REGISTRYPASSWORD=''
export INPUT_VERBOSE=false INPUT_SEVERITYTHRESHOLD=$threshold
export INPUT_FORMAT=sarif,json INPUT_OUTPUTFILE=$report INPUT_FIXFILES=false
if [ "$threshold" = autofix ]; then
export INPUT_FIXFILES=true
fi
umask "$mask"
bash "${test_root}/run.sh"
) > "${workspace}/log" 2>&1 || status=$?
expected_status=0
[ "$threshold" != high ] || expected_status=1
[ "$status" -eq "$expected_status" ] || { cat "${workspace}/log"; exit 1; }
for extension in sarif json; do
file="${workspace}/${report}.${extension}"
[ "$(stat -c '%a:%u:%g' "$file")" = "600:$(id -u):$(id -g)" ]
jq -e . "$file" > /dev/null
done
jq -e '.runs[0].results[0].locations[0].physicalLocation.artifactLocation.uri == "manifests/pod.yaml"' \
"${workspace}/${report}.sarif" > /dev/null
if [ "$threshold" = autofix ]; then
grep -Fxq fixed "${workspace}/manifests/pod.yaml"
else
[ "$(cat "${workspace}/manifests/pod.yaml")" = original ]
fi
printf 'ok - runner reads private nested SARIF/JSON after %s scan (umask %s)\n' "$threshold" "$mask"
done
done
Loading
Loading