Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ci): Reusable workflow for GH Actions anti-pattern scan using zizmor #214

Merged
merged 1 commit into from
Feb 27, 2025
Merged
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
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,12 @@ Refer to the official Lefthook [installation guide](https://github.com/evilmarti
---

## Step 4: Sync Lefthook Hooks
This repo should already contain a `lefthook.yml` configuration file in the root directory. The configuration in the lefthook.yml file enforces commit message linting using Commitlint.
Package.json runs the below command as part of `pnpm install` to sync the Lefthook configuration with your Git hooks:
This repo should already contain a `lefthook.yml` configuration file in the root directory.

The lefthook hooks are synced as part of `pnpm install` command using a `postinstall` hook that runs the below command automatically

```bash
lefthook run pre-commit
lefthook install
```

---
Expand Down Expand Up @@ -70,4 +71,9 @@ To verify that Lefthook is correctly set up:

By setting up Lefthook, you ensure that all developers adhere to the commit message conventions..

# Setting Up zizmor for GH workflows Analysis

This guide will help you install and configure zizmor to analyze GH workflows and Actions locally.

## Step 1: Install zizmor
Installed as dependency during `pnpm install` along with all the other dependencies.
2 changes: 1 addition & 1 deletion code-check-actions/rust-lint/scripts/set-env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
set -euo pipefail

if [[ -n ${manifest_dir} ]]; then
echo "manifest_path=${manifest_dir}/Cargo.toml" >> $GITHUB_OUTPUT
echo "manifest_path=${manifest_dir}/Cargo.toml" >> "$GITHUB_OUTPUT"
fi
8 changes: 8 additions & 0 deletions lefthook.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
# Reference:
# https://github.com/evilmartians/lefthook/blob/master/docs/full_guide.md

pre-commit:
# Run `zizmor` only on matching files
commands:
gh-analyze:
run: zizmor --no-exit-codes --collect=all --persona=pedantic --format plain {staged_files}
glob: "{**/action,.github/workflows/*}.{yml,yaml}"
continue: true

commit-msg:
commands:
commitlint:
Expand Down
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,17 @@
"version:ci": "lerna version --yes --create-release github",
"version:dry-run": "pnpm run version:ci --no-push",
"lint": "eslint '**/*.{js,jsx,ts,tsx,vue}' --ignore-path '.eslintignore'",
"prepare": "lefthook run pre-commit"
"install-python-dependencies": "pip install -r requirements.txt",
"prepare": "pnpm run install-python-dependencies",
"postinstall": "lefthook install",
"pre-commit": "lefthook run pre-commit"
},
"keywords": [],
"devDependencies": {
"@commitlint/cli": "^17.6.6",
"@commitlint/config-conventional": "^17.6.6",
"@commitlint/config-lerna-scopes": "^17.6.6",
"@evilmartians/lefthook": "^1.9.2",
"@evilmartians/lefthook": "^1.10.10",
"@rushstack/eslint-patch": "^1.2.0",
"@typescript-eslint/eslint-plugin": "^5.51.0",
"@typescript-eslint/parser": "^5.51.0",
Expand Down
12 changes: 7 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
zizmor>=v1.2.2
32 changes: 16 additions & 16 deletions security-actions/sca/scripts/scan-metadata.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,37 +22,37 @@ if [[ -z ${DIR} && -z ${FILE} ]]; then
fi

if [[ -n ${DIR} ]]; then
echo "scan_dir=${DIR}" >> $GITHUB_OUTPUT
echo "scan_dir=${DIR}" >> "$GITHUB_OUTPUT"
fi

if [[ -n ${FILE} ]]; then
echo "scan_file=${FILE}" >> $GITHUB_OUTPUT
echo "scan_file=${FILE}" >> "$GITHUB_OUTPUT"
fi

if [[ -n ${ASSET_PREFIX} ]]; then
echo "sbom_spdx_file=${ASSET_PREFIX##*/}-${spdx_ext}" >> $GITHUB_OUTPUT
echo "sbom_cyclonedx_file=${ASSET_PREFIX##*/}-${cyclonedx_ext}" >> $GITHUB_OUTPUT
echo "grype_json_file=${ASSET_PREFIX##*/}-${cve_json_ext}" >> $GITHUB_OUTPUT
echo "grype_sarif_file=${ASSET_PREFIX##*/}-${cve_sarif_ext}" >> $GITHUB_OUTPUT
echo "cis_json_file=${ASSET_PREFIX##*/}-${cis_json_ext}" >> $GITHUB_OUTPUT
echo "sbom_spdx_file=${ASSET_PREFIX##*/}-${spdx_ext}" >> "$GITHUB_OUTPUT"
echo "sbom_cyclonedx_file=${ASSET_PREFIX##*/}-${cyclonedx_ext}" >> "$GITHUB_OUTPUT"
echo "grype_json_file=${ASSET_PREFIX##*/}-${cve_json_ext}" >> "$GITHUB_OUTPUT"
echo "grype_sarif_file=${ASSET_PREFIX##*/}-${cve_sarif_ext}" >> "$GITHUB_OUTPUT"
echo "cis_json_file=${ASSET_PREFIX##*/}-${cis_json_ext}" >> "$GITHUB_OUTPUT"
else
echo "sbom_spdx_file=${spdx_ext}" >> $GITHUB_OUTPUT
echo "sbom_cyclonedx_file=${cyclonedx_ext}" >> $GITHUB_OUTPUT
echo "grype_json_file=${cve_json_ext}" >> $GITHUB_OUTPUT
echo "grype_sarif_file=${cve_sarif_ext}" >> $GITHUB_OUTPUT
echo "cis_json_file=${cis_json_ext}" >> $GITHUB_OUTPUT
echo "sbom_spdx_file=${spdx_ext}" >> "$GITHUB_OUTPUT"
echo "sbom_cyclonedx_file=${cyclonedx_ext}" >> "$GITHUB_OUTPUT"
echo "grype_json_file=${cve_json_ext}" >> "$GITHUB_OUTPUT"
echo "grype_sarif_file=${cve_sarif_ext}" >> "$GITHUB_OUTPUT"
echo "cis_json_file=${cis_json_ext}" >> "$GITHUB_OUTPUT"
fi

if [[ -n ${global_severity_cutoff} ]]; then
echo "global_severity_cutoff=${global_severity_cutoff}" >> $GITHUB_OUTPUT
echo "global_severity_cutoff=${global_severity_cutoff}" >> "$GITHUB_OUTPUT"
else
echo '::error ::set global_severity_cutoff in $0'
echo "::error ::set global_severity_cutoff in $0"
exit 1
fi

if [[ -n ${global_enforce_build_failure} ]]; then
echo "global_enforce_build_failure=${global_enforce_build_failure}" >> $GITHUB_OUTPUT
echo "global_enforce_build_failure=${global_enforce_build_failure}" >> "$GITHUB_OUTPUT"
else
echo '::error ::set global_enforce_build_failure in $0'
echo "::error ::set global_enforce_build_failure in $0"
exit 1
fi
32 changes: 16 additions & 16 deletions security-actions/scan-docker-image/scripts/scan-metadata.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,36 +19,36 @@ fi
# OCI archive should be passed as image instead of file
if [[ -n ${IMAGE} ]]; then
if [[ -n ${TAG} ]]; then
echo "scan_image=${IMAGE}:${TAG}" >> $GITHUB_OUTPUT
echo "scan_image=${IMAGE}:${TAG}" >> "$GITHUB_OUTPUT"
else
echo "scan_image=${IMAGE}" >> $GITHUB_OUTPUT
echo "scan_image=${IMAGE}" >> "$GITHUB_OUTPUT"
fi
fi

if [[ -n ${ASSET_PREFIX} ]]; then
echo "sbom_spdx_file=${ASSET_PREFIX##*/}-${spdx_ext}" >> $GITHUB_OUTPUT
echo "sbom_cyclonedx_file=${ASSET_PREFIX##*/}-${cyclonedx_ext}" >> $GITHUB_OUTPUT
echo "grype_json_file=${ASSET_PREFIX##*/}-${cve_json_ext}" >> $GITHUB_OUTPUT
echo "grype_sarif_file=${ASSET_PREFIX##*/}-${cve_sarif_ext}" >> $GITHUB_OUTPUT
echo "cis_json_file=${ASSET_PREFIX##*/}-${cis_json_ext}" >> $GITHUB_OUTPUT
echo "sbom_spdx_file=${ASSET_PREFIX##*/}-${spdx_ext}" >> "$GITHUB_OUTPUT"
echo "sbom_cyclonedx_file=${ASSET_PREFIX##*/}-${cyclonedx_ext}" >> "$GITHUB_OUTPUT"
echo "grype_json_file=${ASSET_PREFIX##*/}-${cve_json_ext}" >> "$GITHUB_OUTPUT"
echo "grype_sarif_file=${ASSET_PREFIX##*/}-${cve_sarif_ext}" >> "$GITHUB_OUTPUT"
echo "cis_json_file=${ASSET_PREFIX##*/}-${cis_json_ext}" >> "$GITHUB_OUTPUT"
else
echo "sbom_spdx_file=${spdx_ext}" >> $GITHUB_OUTPUT
echo "sbom_cyclonedx_file=${cyclonedx_ext}" >> $GITHUB_OUTPUT
echo "grype_json_file=${cve_json_ext}" >> $GITHUB_OUTPUT
echo "grype_sarif_file=${cve_sarif_ext}" >> $GITHUB_OUTPUT
echo "cis_json_file=${cis_json_ext}" >> $GITHUB_OUTPUT
echo "sbom_spdx_file=${spdx_ext}" >> "$GITHUB_OUTPUT"
echo "sbom_cyclonedx_file=${cyclonedx_ext}" >> "$GITHUB_OUTPUT"
echo "grype_json_file=${cve_json_ext}" >> "$GITHUB_OUTPUT"
echo "grype_sarif_file=${cve_sarif_ext}" >> "$GITHUB_OUTPUT"
echo "cis_json_file=${cis_json_ext}" >> "$GITHUB_OUTPUT"
fi

if [[ -n ${global_severity_cutoff} ]]; then
echo "global_severity_cutoff=${global_severity_cutoff}" >> $GITHUB_OUTPUT
echo "global_severity_cutoff=${global_severity_cutoff}" >> "$GITHUB_OUTPUT"
else
echo '::error ::set global_severity_cutoff in $0'
echo "::error ::set global_severity_cutoff in $0"
exit 1
fi

if [[ -n ${global_enforce_build_failure} ]]; then
echo "global_enforce_build_failure=${global_enforce_build_failure}" >> $GITHUB_OUTPUT
echo "global_enforce_build_failure=${global_enforce_build_failure}" >> "$GITHUB_OUTPUT"
else
echo '::error ::set global_enforce_build_failure in $0'
echo "::error ::set global_enforce_build_failure in $0"
exit 1
fi
145 changes: 145 additions & 0 deletions security-actions/scan-gh-workflows/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
name: GH Actions SAST
description: Static analyzer for GH actions
author: 'Kong'
inputs:
scan_path:
description: 'File, Dir, Repository formatted: "owner/repo[@<sha>]" containing workflow files'
required: true
default: '.' #Default is workspace
github_token:
description: 'PAT for fetching remote "scan_path" of format "owner/repo[@<sha>]"'
required: false
default: ''
asset_prefix:
description: 'prefix for generated artifacts'
required: false
offline_audit_checks:
description: "Runs offline audit checks but performs repository pulls"
required: true
default: true
type: choice
options:
- 'true'
- 'false'
persona:
description: 'Run specific audit checks based on selected persona.'
required: true
type: choice
default: 'regular'
options:
- 'regular'
- 'pedantic'
- 'auditor'
fail_on_findings:
description: 'Fail build / job on findings/errors'
required: true
type: choice
default: false
options:
- 'true'
- 'false'

runs:
using: 'composite'
steps:

- name: Set Scan metadata
shell: bash
id: meta
env:
SCAN_PATH: ${{ inputs.SCAN_PATH }}
PERSONA: ${{ inputs.persona }}
OFFLINE_AUDIT_CHECKS: ${{ inputs.offline_audit_checks }}
GITHUB_TOKEN: ${{ inputs.github_token }}
ASSET_PREFIX: ${{ inputs.asset_prefix }}
run: $GITHUB_ACTION_PATH/scripts/scan-metadata.sh

- name: Install cargo-hack from crates.io
uses: baptiste0928/cargo-install@91c5da15570085bcde6f4d7aed98cb82d6769fd3
with:
crate: zizmor
locked: true
version: '~1'

- name: Run GH Actions SAST - [SARIF format]
shell: bash
id: gh_actions_sast_sarif
# Continue on error to upload results
continue-on-error: true
run: |
zizmor ${{ env.SCAN_ARGS }} ${{ env.SCAN_PATH }} --format sarif > ${{ steps.meta.outputs.sarif_file }}
env:
SCAN_ARGS: ${{ steps.meta.outputs.scan_args }}
SCAN_PATH: ${{ inputs.scan_path }}
GH_TOKEN: ${{ inputs.github_token }}

- name: Run GH Actions SAST - [JSON format]
shell: bash
# Continue on error to upload results
continue-on-error: true
id: gh_actions_sast_json
run: |
zizmor ${{ env.SCAN_ARGS }} ${{ env.SCAN_PATH }} --format plain > ${{ steps.meta.outputs.json_file }}
env:
SCAN_ARGS: ${{ steps.meta.outputs.scan_args }}
SCAN_PATH: ${{ inputs.scan_path }}
GH_TOKEN: ${{ inputs.github_token }}

- name: Run GH Actions SAST - [Plain format]
shell: bash
# Continue on error to upload results
continue-on-error: true
id: gh_actions_sast_plain
run: |
zizmor ${{ env.SCAN_ARGS }} ${{ env.SCAN_PATH }} --format plain > ${{ steps.meta.outputs.out_file }}
env:
SCAN_ARGS: ${{ steps.meta.outputs.scan_args }}
SCAN_PATH: ${{ inputs.scan_path }}
GH_TOKEN: ${{ inputs.github_token }}

- name: Upload GH Actions SAST reports to Workflow
if: always() && steps.gh_actions_sast_plain.conclusion == 'success' && steps.gh_actions_sast_sarif.conclusion == 'success'
uses: actions/upload-artifact@v4
with:
name: ${{ steps.meta.outputs.report_file_name }}.zip
path: |
${{ steps.meta.outputs.sarif_file }}
${{ steps.meta.outputs.json_file }}
${{ steps.meta.outputs.out_file}}
if-no-files-found: warn

# - name: Add findings as check summary
# if: always()
# shell: bash
# run: |
# if [[ -f "${OUT_FILE}" ]]; then
# echo "## GH Actions SAST CI Scan Summary Report" >> $GITHUB_STEP_SUMMARY
# while IFS= read -r line; do
# echo "- $line" >> $GITHUB_STEP_SUMMARY
# done < ${OUT_FILE}
# fi
# env:
# OUT_FILE: ${{ steps.meta.outputs.out_file}}

- name: Print findings to console out
if: always()
shell: bash
run: |
echo "::group::Github Actions SAST Scan Summary Report"
if [[ -f "${OUT_FILE}" ]]; then
cat ${OUT_FILE}
fi
echo "::endgroup::"
env:
OUT_FILE: ${{ steps.meta.outputs.out_file}}

- name: Fail on findings
if: always()
shell: bash
run: |
if [[ ${SCAN_STATUS} == 'failure' ]] && [[ ${FAIL_BUILD} == 'true' ]]; then
exit 1
fi
env:
SCAN_STATUS: ${{ steps.gh_actions_sast_plain.outcome }}
FAIL_BUILD: ${{ steps.meta.outputs.global_enforce_build_failure == 'true' && steps.meta.outputs.global_enforce_build_failure || inputs.fail_on_findings }}
14 changes: 14 additions & 0 deletions security-actions/scan-gh-workflows/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "@security-actions/scan-gh-workflows",
"version": "4.0.0",
"description": "The package scans github actions and workflows for anti-patterns",
"main": "index.js",
"repository": {
"type": "git",
"url": "https://github.com/Kong/public-shared-actions",
"directory": "security-actions/scan-gh-workflows"
},
"private": false,
"author": "Kong, Inc.",
"license": "UNLICENSED"
}
Loading
Loading