Add reusable Grype composite actions
This commit is contained in:
commit
259880fca6
4 files changed
+325
No files matched your search
@@ -0,0 +1 @@
|
||||
.opencode/
|
||||
@@ -0,0 +1,116 @@
|
||||
# actions
|
||||
|
||||
Reusable composite actions shared across the `git.sysmd.uk` CI workflows.
|
||||
|
||||
These actions implement **mechanism only** (install a tool, scan an image).
|
||||
Each calling workflow keeps its own **policy**: what triggers a build, and
|
||||
whether a finding notifies, fails the run, blocks a push, or blocks a deploy.
|
||||
|
||||
## Actions
|
||||
|
||||
| Action | Purpose |
|
||||
| --- | --- |
|
||||
| `install-grype` | Installs a pinned, signature-verified Grype (and the pinned Cosign used to verify it) into a job-local directory on `PATH`. |
|
||||
| `scan-image` | Scans an image with Grype and fails the step on fixable High/Critical findings; writes the raw JSON report. |
|
||||
|
||||
## Usage
|
||||
|
||||
Reference the actions from another repository with the **full instance URL**. An
|
||||
unprefixed `owner/repo@ref` is fetched from **github.com** (unless
|
||||
`DEFAULT_ACTIONS_URL=self`), and the `self:` prefix is not honoured by all
|
||||
runner versions. The absolute form works regardless:
|
||||
|
||||
```yaml
|
||||
uses: https://git.sysmd.uk/mdaleo404/actions/install-grype@v1
|
||||
```
|
||||
|
||||
Pin the ref (`@v1`, or a commit SHA for stronger supply-chain guarantees)
|
||||
rather than floating `@main`. The path is any directory containing
|
||||
`action.yml` — these live at the repo root, so the refs are short.
|
||||
|
||||
The repository must be reachable by the runner without credentials. If it is
|
||||
**private**, either make it public or vendor the action into the consuming
|
||||
repository and reference it locally as `./.gitea/actions/<name>`.
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
build-scan-push:
|
||||
runs-on: running-man
|
||||
env:
|
||||
IMAGE: git.sysmd.uk/team/example:latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Login to registry
|
||||
run: echo "$REGISTRY_TOKEN" | docker login git.sysmd.uk -u "$REGISTRY_USER" --password-stdin
|
||||
env:
|
||||
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
|
||||
- name: Build image
|
||||
run: docker build --no-cache -t "${IMAGE}" .
|
||||
|
||||
- uses: https://git.sysmd.uk/mdaleo404/actions/install-grype@v1
|
||||
|
||||
# continue-on-error lets the caller inspect the outcome and decide policy.
|
||||
- name: Scan image
|
||||
id: scan
|
||||
continue-on-error: true
|
||||
uses: https://git.sysmd.uk/mdaleo404/actions/scan-image@v1
|
||||
with:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
- name: Notify on vulnerabilities
|
||||
if: steps.scan.outcome == 'failure'
|
||||
run: |
|
||||
# read grype.json (written by scan-image) and post wherever you like
|
||||
jq '{repo: "owner/repo", image: env.IMAGE, count: (.matches | length)}' grype.json
|
||||
|
||||
- name: Fail if vulnerabilities found
|
||||
if: steps.scan.outcome == 'failure'
|
||||
run: exit 1
|
||||
|
||||
- name: Push image
|
||||
if: steps.scan.outcome != 'failure'
|
||||
run: docker push "${IMAGE}"
|
||||
```
|
||||
|
||||
## Interface
|
||||
|
||||
### `install-grype`
|
||||
|
||||
| Input | Default | Notes |
|
||||
| --- | --- | --- |
|
||||
| `grype-version` | `v0.110.0` | Release tag including `v`. |
|
||||
| `cosign-version` | `v3.0.5` | Cosign used only to verify Grype's checksums. |
|
||||
| `install-dir` | job-local dir on `PATH` | Override to choose the install location. |
|
||||
|
||||
Outputs: `grype-path`, `version`.
|
||||
|
||||
Supports `amd64` and `arm64`. Cosign is checksum-pinned per architecture; Grype
|
||||
is verified against its keyless-signed checksum file.
|
||||
|
||||
### `scan-image`
|
||||
|
||||
| Input | Default | Notes |
|
||||
| --- | --- | --- |
|
||||
| `image` | — (required) | Tag or digest to scan. |
|
||||
| `report-path` | `grype.json` | Raw JSON report location, relative to the workspace. |
|
||||
| `fail-on-fixable` | `true` | Set `false` to always pass and inspect outputs instead. |
|
||||
|
||||
Outputs: `fixable-count`, `total-count`, `report-path`.
|
||||
|
||||
The step exits non-zero when fixable High/Critical findings exist and
|
||||
`fail-on-fixable` is `true`. Combine with `continue-on-error: true` and
|
||||
`steps.<id>.outcome` to implement notify-then-fail, as above.
|
||||
|
||||
## Versioning
|
||||
|
||||
`install-grype` centralises the Cosign and Grype versions. Bump them here,
|
||||
deliberately, and tag a new release (`v1`, `v2`, ...) so callers can pin.
|
||||
|
||||
## Notes
|
||||
|
||||
- The `scan-image` action writes the raw report to the workspace; the caller's
|
||||
notification step can keep using the same `grype.json` it already expects.
|
||||
- Nothing here reads or writes secrets. Tokens stay in the calling workflow.
|
||||
@@ -0,0 +1,110 @@
|
||||
name: Install Grype
|
||||
description: >-
|
||||
Install a pinned, signature-verified Grype into a job-local directory and add
|
||||
it to PATH. Also bootstraps the pinned Cosign used to verify Grype's checksums.
|
||||
|
||||
# Mechanism only. The caller decides what to scan and what to do with the result.
|
||||
|
||||
inputs:
|
||||
grype-version:
|
||||
description: Grype release tag, including the leading "v".
|
||||
required: false
|
||||
default: v0.110.0
|
||||
cosign-version:
|
||||
description: Cosign release tag used to verify Grype's signed checksums.
|
||||
required: false
|
||||
default: v3.0.5
|
||||
install-dir:
|
||||
description: >-
|
||||
Directory to install the binaries into. Defaults to a job-local directory
|
||||
that is added to PATH.
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
outputs:
|
||||
grype-path:
|
||||
description: Absolute path to the installed grype binary.
|
||||
value: ${{ steps.install.outputs.grype-path }}
|
||||
version:
|
||||
description: Installed grype version string.
|
||||
value: ${{ steps.install.outputs.version }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- id: install
|
||||
shell: bash
|
||||
env:
|
||||
INPUT_GRYPE_VERSION: ${{ inputs.grype-version }}
|
||||
INPUT_COSIGN_VERSION: ${{ inputs.cosign-version }}
|
||||
INPUT_INSTALL_DIR: ${{ inputs.install-dir }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
grype_version="${INPUT_GRYPE_VERSION:?grype-version is required}"
|
||||
cosign_version="${INPUT_COSIGN_VERSION:?cosign-version is required}"
|
||||
install_dir="${INPUT_INSTALL_DIR:-${RUNNER_TEMP:-/tmp}/grype-bin}"
|
||||
|
||||
# Pinned, checksum-verified Cosign bootstrap binaries. Bump deliberately.
|
||||
case "$(uname -m)" in
|
||||
x86_64|amd64)
|
||||
arch=amd64
|
||||
cosign_sha=db15cc99e6e4837daabab023742aaddc3841ce57f193d11b7c3e06c8003642b2
|
||||
;;
|
||||
aarch64|arm64)
|
||||
arch=arm64
|
||||
cosign_sha=d098f3168ae4b3aa70b4ca78947329b953272b487727d1722cb3cb098a1a20ab
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported architecture: $(uname -m)" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
mkdir -p "$install_dir"
|
||||
work="$(mktemp -d)"
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
cd "$work"
|
||||
|
||||
# Bootstrap Cosign: pinned by checksum before it is trusted.
|
||||
curl --fail --location --retry 3 --output cosign \
|
||||
"https://github.com/sigstore/cosign/releases/download/${cosign_version}/cosign-linux-${arch}"
|
||||
printf '%s %s\n' "$cosign_sha" cosign | sha256sum --check --status -
|
||||
chmod 0755 cosign
|
||||
|
||||
version_no_v="${grype_version#v}"
|
||||
tarball="grype_${version_no_v}_linux_${arch}.tar.gz"
|
||||
base="https://github.com/anchore/grype/releases/download/${grype_version}"
|
||||
|
||||
curl --fail --location --retry 3 --output "$tarball" "${base}/${tarball}"
|
||||
curl --fail --location --retry 3 --output checksums.txt \
|
||||
"${base}/grype_${version_no_v}_checksums.txt"
|
||||
curl --fail --location --retry 3 --output checksums.txt.sig \
|
||||
"${base}/grype_${version_no_v}_checksums.txt.sig"
|
||||
curl --fail --location --retry 3 --output checksums.txt.pem \
|
||||
"${base}/grype_${version_no_v}_checksums.txt.pem"
|
||||
|
||||
# Verify Grype's signed checksum file with keyless Cosign.
|
||||
./cosign verify-blob \
|
||||
--signature checksums.txt.sig \
|
||||
--certificate checksums.txt.pem \
|
||||
--certificate-identity-regexp "https://github.com/anchore/grype" \
|
||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
|
||||
checksums.txt
|
||||
|
||||
check="$(grep " ${tarball}$" checksums.txt)"
|
||||
[ -n "$check" ] || { echo "No checksum entry for ${tarball}" >&2; exit 1; }
|
||||
printf '%s\n' "$check" | sha256sum --check --status -
|
||||
|
||||
tar -xzf "$tarball"
|
||||
install -m 0755 grype "$install_dir/grype"
|
||||
|
||||
echo "$install_dir" >> "${GITHUB_PATH:-/dev/null}"
|
||||
|
||||
version="$("$install_dir/grype" version 2>/dev/null | awk '/^Version:/{print $2; exit}')"
|
||||
{
|
||||
echo "grype-path=$install_dir/grype"
|
||||
echo "version=${version}"
|
||||
} >> "${GITHUB_OUTPUT:-/dev/null}"
|
||||
|
||||
echo "Installed grype ${version:-$grype_version} at $install_dir/grype"
|
||||
@@ -0,0 +1,98 @@
|
||||
name: Scan Image
|
||||
description: >-
|
||||
Scan a container image with Grype and fail the step on fixable High/Critical
|
||||
findings. Writes the raw JSON report so the caller can notify or act on it.
|
||||
|
||||
# Mechanism only. The caller keeps the policy: notify, fail, block a push, etc.
|
||||
#
|
||||
# Intended to be used with `continue-on-error: true` so the caller can inspect
|
||||
# `steps.<id>.outcome` and decide what happens next.
|
||||
|
||||
inputs:
|
||||
image:
|
||||
description: Image reference (tag or digest) to scan.
|
||||
required: true
|
||||
report-path:
|
||||
description: Where to write the raw Grype JSON report, relative to the workspace.
|
||||
required: false
|
||||
default: grype.json
|
||||
fail-on-fixable:
|
||||
description: Fail the step when fixable High/Critical findings exist.
|
||||
required: false
|
||||
default: 'true'
|
||||
|
||||
outputs:
|
||||
fixable-count:
|
||||
description: Number of fixable High/Critical findings.
|
||||
value: ${{ steps.scan.outputs.fixable-count }}
|
||||
total-count:
|
||||
description: Total number of findings reported by Grype.
|
||||
value: ${{ steps.scan.outputs.total-count }}
|
||||
report-path:
|
||||
description: Path to the raw Grype JSON report.
|
||||
value: ${{ steps.scan.outputs.report-path }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- id: scan
|
||||
shell: bash
|
||||
env:
|
||||
INPUT_IMAGE: ${{ inputs.image }}
|
||||
INPUT_REPORT_PATH: ${{ inputs.report-path }}
|
||||
INPUT_FAIL_ON_FIXABLE: ${{ inputs.fail-on-fixable }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
image="${INPUT_IMAGE:?image is required}"
|
||||
report="${INPUT_REPORT_PATH:-grype.json}"
|
||||
fail_on_fixable="${INPUT_FAIL_ON_FIXABLE:-true}"
|
||||
|
||||
grype "$image" -o json > "$report"
|
||||
|
||||
echo "Fixable HIGH/CRITICAL vulnerabilities:"
|
||||
jq -r '
|
||||
.matches[]
|
||||
| select(
|
||||
(
|
||||
.vulnerability.severity == "High" or
|
||||
.vulnerability.severity == "Critical"
|
||||
)
|
||||
and
|
||||
(
|
||||
(.vulnerability.fix.versions | length) > 0
|
||||
)
|
||||
)
|
||||
| "\(.artifact.name)@\(.artifact.version) -> \(.vulnerability.id) [\(.vulnerability.severity)] | fixed: \(.vulnerability.fix.versions[0])"
|
||||
' "$report"
|
||||
|
||||
fixable="$(jq '
|
||||
[
|
||||
.matches[]
|
||||
| select(
|
||||
(
|
||||
.vulnerability.severity == "High" or
|
||||
.vulnerability.severity == "Critical"
|
||||
)
|
||||
and
|
||||
(
|
||||
(.vulnerability.fix.versions | length) > 0
|
||||
)
|
||||
)
|
||||
]
|
||||
| length
|
||||
' "$report")"
|
||||
total="$(jq '.matches | length' "$report")"
|
||||
|
||||
{
|
||||
echo "fixable-count=${fixable}"
|
||||
echo "total-count=${total}"
|
||||
echo "report-path=${report}"
|
||||
} >> "${GITHUB_OUTPUT:-/dev/null}"
|
||||
|
||||
echo "Fixable HIGH/CRITICAL count: ${fixable} (total findings: ${total})"
|
||||
|
||||
if [ "$fail_on_fixable" = "true" ] && [ "$fixable" != "0" ]; then
|
||||
echo "::error::Grype found ${fixable} fixable HIGH/CRITICAL vulnerabilities."
|
||||
exit 1
|
||||
fi
|
||||
Reference in new issue
Block a user