Add install-syft, generalize scan-image (target/severities), add self-CI and governance
This commit is contained in:
5 files changed
+321
-26
No files matched your search
@@ -0,0 +1,101 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
name: validate
|
||||
runs-on: running-man
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Validate action metadata (YAML)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
status=0
|
||||
for f in install-grype/action.yml install-syft/action.yml scan-image/action.yml; do
|
||||
if [ ! -f "$f" ]; then
|
||||
echo "::error::missing $f"
|
||||
status=1
|
||||
continue
|
||||
fi
|
||||
python3 -c "import yaml, sys; yaml.safe_load(open(sys.argv[1]))" "$f"
|
||||
echo "OK $f"
|
||||
done
|
||||
exit "$status"
|
||||
|
||||
- name: Shellcheck action scripts (best effort)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if ! command -v shellcheck >/dev/null 2>&1; then
|
||||
echo "shellcheck not installed on the runner; skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
python3 - <<'PY' > /tmp/action-runblocks.sh
|
||||
import glob, sys, yaml
|
||||
for path in sorted(glob.glob('*/action.yml')):
|
||||
doc = yaml.safe_load(open(path))
|
||||
for step in doc.get('runs', {}).get('steps', []):
|
||||
run = step.get('run')
|
||||
if run:
|
||||
sys.stdout.write(run.rstrip() + "\n\n")
|
||||
PY
|
||||
|
||||
shellcheck -s bash -S error /tmp/action-runblocks.sh
|
||||
|
||||
smoke:
|
||||
name: smoke
|
||||
runs-on: running-man
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
# Exercise the actions from the working tree, not a published tag.
|
||||
- name: Install Grype
|
||||
uses: https://git.sysmd.uk/mdaleo404/actions/install-grype
|
||||
|
||||
- name: Install Syft
|
||||
uses: https://git.sysmd.uk/mdaleo404/actions/install-syft
|
||||
|
||||
- name: Verify installs
|
||||
run: |
|
||||
set -euo pipefail
|
||||
grype version
|
||||
syft version
|
||||
|
||||
- name: Generate SBOM fixture
|
||||
run: |
|
||||
set -euo pipefail
|
||||
syft dir:. -o json > sbom.json
|
||||
echo "Packages discovered: $(jq -r '.artifacts | length' sbom.json)"
|
||||
|
||||
- name: Scan SBOM (generalized path, non-failing)
|
||||
uses: https://git.sysmd.uk/mdaleo404/actions/scan-image
|
||||
with:
|
||||
target: sbom:sbom.json
|
||||
fail-on-fixable: 'false'
|
||||
|
||||
- name: Assert report and outputs
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -s grype.json
|
||||
jq -e '.matches | type == "array"' grype.json
|
||||
echo "total findings: $(jq '.matches | length' grype.json)"
|
||||
@@ -0,0 +1,5 @@
|
||||
# Every path is owned by the maintainer. This repository is public and its
|
||||
# actions run with consumer secrets, so require review before merge and restrict
|
||||
# who can move release tags (configured in the instance, not here).
|
||||
|
||||
* @mdaleo404
|
||||
@@ -11,7 +11,8 @@ whether a finding notifies, fails the run, blocks a push, or blocks a deploy.
|
||||
| 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. |
|
||||
| `install-syft` | Installs a pinned, signature-verified Syft the same way, for SBOM generation. |
|
||||
| `scan-image` | Scans an image, directory, or SBOM with Grype and fails the step on fixable findings at the configured severities; writes the raw JSON report. |
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -75,6 +76,27 @@ jobs:
|
||||
run: docker push "${IMAGE}"
|
||||
```
|
||||
|
||||
### SBOM / directory scan
|
||||
|
||||
For a repository rather than an image, generate an SBOM with Syft and scan it
|
||||
with the same action by passing `target`:
|
||||
|
||||
```yaml
|
||||
- uses: https://git.sysmd.uk/mdaleo404/actions/install-grype@v1
|
||||
- uses: https://git.sysmd.uk/mdaleo404/actions/install-syft@v1
|
||||
|
||||
- name: Generate SBOM
|
||||
run: syft dir:. -o json > sbom.json
|
||||
|
||||
- name: Scan SBOM
|
||||
id: scan
|
||||
continue-on-error: true
|
||||
uses: https://git.sysmd.uk/mdaleo404/actions/scan-image@v1
|
||||
with:
|
||||
target: sbom:sbom.json
|
||||
severities: Medium,High,Critical
|
||||
```
|
||||
|
||||
## Interface
|
||||
|
||||
### `install-grype`
|
||||
@@ -90,24 +112,53 @@ Outputs: `grype-path`, `version`.
|
||||
Supports `amd64` and `arm64`. Cosign is checksum-pinned per architecture; Grype
|
||||
is verified against its keyless-signed checksum file.
|
||||
|
||||
### `install-syft`
|
||||
|
||||
| Input | Default | Notes |
|
||||
| --- | --- | --- |
|
||||
| `syft-version` | `v1.42.3` | Release tag including `v`. |
|
||||
| `cosign-version` | `v3.0.5` | Cosign used only to verify Syft's checksums. |
|
||||
| `install-dir` | job-local dir on `PATH` | Override to choose the install location. |
|
||||
|
||||
Outputs: `syft-path`, `version`.
|
||||
|
||||
Supports `amd64` and `arm64`, verified the same way as Grype.
|
||||
|
||||
### `scan-image`
|
||||
|
||||
| Input | Default | Notes |
|
||||
| --- | --- | --- |
|
||||
| `image` | — (required) | Tag or digest to scan. |
|
||||
| `image` | — | Image reference to scan. Required unless `target` is set. |
|
||||
| `target` | — | Grype target, e.g. `sbom:sbom.json` or `dir:.`. Overrides `image` when set. |
|
||||
| `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. |
|
||||
| `severities` | `High,Critical` | Comma-separated severities that count as findings. |
|
||||
|
||||
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
|
||||
The step exits non-zero when fixable findings exist at the selected severities
|
||||
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.
|
||||
`install-grype` and `install-syft` centralise the Cosign, Grype, and Syft
|
||||
versions. Bump them here, deliberately, and tag a new release (`v1`, `v2`, ...)
|
||||
so callers can pin.
|
||||
|
||||
## Consuming safely
|
||||
|
||||
This repository is **public**, and every action here runs inside the caller's
|
||||
job — with the caller's secrets, on the caller's runner. Treat a merge here as a
|
||||
release to every consumer:
|
||||
|
||||
- **Pin a commit SHA, not a moving ref.** `@v1` can be moved; a full SHA cannot.
|
||||
- Keep secrets in the calling workflow; nothing here reads or writes secrets.
|
||||
- Installs verify their payloads against keyless Cosign signatures (and pinned
|
||||
Cosign checksums) before use, fetching only from
|
||||
`github.com/sigstore/cosign` and `github.com/anchore/{grype,syft}`.
|
||||
- Report suspected issues privately to the maintainer (see `CODEOWNERS`) rather
|
||||
than opening a public issue.
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
name: Install Syft
|
||||
description: >-
|
||||
Install a pinned, signature-verified Syft into a job-local directory and add
|
||||
it to PATH. Also bootstraps the pinned Cosign used to verify Syft's checksums.
|
||||
|
||||
# Mechanism only. The caller decides what to scan and what to do with the result.
|
||||
|
||||
inputs:
|
||||
syft-version:
|
||||
description: Syft release tag, including the leading "v".
|
||||
required: false
|
||||
default: v1.42.3
|
||||
cosign-version:
|
||||
description: Cosign release tag used to verify Syft's signed checksums.
|
||||
required: false
|
||||
default: v3.0.5
|
||||
install-dir:
|
||||
description: >-
|
||||
Directory to install the binary into. Defaults to a job-local directory
|
||||
that is added to PATH.
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
outputs:
|
||||
syft-path:
|
||||
description: Absolute path to the installed syft binary.
|
||||
value: ${{ steps.install.outputs.syft-path }}
|
||||
version:
|
||||
description: Installed syft version string.
|
||||
value: ${{ steps.install.outputs.version }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- id: install
|
||||
shell: bash
|
||||
env:
|
||||
INPUT_SYFT_VERSION: ${{ inputs.syft-version }}
|
||||
INPUT_COSIGN_VERSION: ${{ inputs.cosign-version }}
|
||||
INPUT_INSTALL_DIR: ${{ inputs.install-dir }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
syft_version="${INPUT_SYFT_VERSION:?syft-version is required}"
|
||||
cosign_version="${INPUT_COSIGN_VERSION:?cosign-version is required}"
|
||||
install_dir="${INPUT_INSTALL_DIR:-${RUNNER_TEMP:-/tmp}/syft-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="${syft_version#v}"
|
||||
tarball="syft_${version_no_v}_linux_${arch}.tar.gz"
|
||||
base="https://github.com/anchore/syft/releases/download/${syft_version}"
|
||||
|
||||
curl --fail --location --retry 3 --output "$tarball" "${base}/${tarball}"
|
||||
curl --fail --location --retry 3 --output checksums.txt \
|
||||
"${base}/syft_${version_no_v}_checksums.txt"
|
||||
curl --fail --location --retry 3 --output checksums.txt.sig \
|
||||
"${base}/syft_${version_no_v}_checksums.txt.sig"
|
||||
curl --fail --location --retry 3 --output checksums.txt.pem \
|
||||
"${base}/syft_${version_no_v}_checksums.txt.pem"
|
||||
|
||||
# Verify Syft'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/syft" \
|
||||
--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 syft "$install_dir/syft"
|
||||
|
||||
echo "$install_dir" >> "${GITHUB_PATH:-/dev/null}"
|
||||
|
||||
version="$("$install_dir/syft" version 2>/dev/null | awk '/^Version:/{print $2; exit}')"
|
||||
{
|
||||
echo "syft-path=$install_dir/syft"
|
||||
echo "version=${version}"
|
||||
} >> "${GITHUB_OUTPUT:-/dev/null}"
|
||||
|
||||
echo "Installed syft ${version:-$syft_version} at $install_dir/syft"
|
||||
+48
-20
@@ -1,7 +1,8 @@
|
||||
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.
|
||||
Scan a container image, directory, or SBOM with Grype and fail the step on
|
||||
fixable findings at the configured severities. 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.
|
||||
#
|
||||
@@ -10,16 +11,29 @@ description: >-
|
||||
|
||||
inputs:
|
||||
image:
|
||||
description: Image reference (tag or digest) to scan.
|
||||
required: true
|
||||
description: >-
|
||||
Image reference (tag or digest) to scan. Required unless `target` is set.
|
||||
required: false
|
||||
default: ''
|
||||
target:
|
||||
description: >-
|
||||
Grype target to scan instead of an image, e.g. "sbom:sbom.json" or "dir:.".
|
||||
Takes precedence over `image` when set.
|
||||
required: false
|
||||
default: ''
|
||||
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.
|
||||
description: Fail the step when fixable findings exist at the selected severities.
|
||||
required: false
|
||||
default: 'true'
|
||||
severities:
|
||||
description: >-
|
||||
Comma-separated severities that count as findings (e.g. High,Critical).
|
||||
required: false
|
||||
default: High,Critical
|
||||
|
||||
outputs:
|
||||
fixable-count:
|
||||
@@ -39,25 +53,42 @@ runs:
|
||||
shell: bash
|
||||
env:
|
||||
INPUT_IMAGE: ${{ inputs.image }}
|
||||
INPUT_TARGET: ${{ inputs.target }}
|
||||
INPUT_REPORT_PATH: ${{ inputs.report-path }}
|
||||
INPUT_FAIL_ON_FIXABLE: ${{ inputs.fail-on-fixable }}
|
||||
INPUT_SEVERITIES: ${{ inputs.severities }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
image="${INPUT_IMAGE:?image is required}"
|
||||
image="${INPUT_IMAGE:-}"
|
||||
target="${INPUT_TARGET:-}"
|
||||
report="${INPUT_REPORT_PATH:-grype.json}"
|
||||
fail_on_fixable="${INPUT_FAIL_ON_FIXABLE:-true}"
|
||||
severities="${INPUT_SEVERITIES:-High,Critical}"
|
||||
|
||||
grype "$image" -o json > "$report"
|
||||
if [ -n "$target" ]; then
|
||||
scan_ref="$target"
|
||||
elif [ -n "$image" ]; then
|
||||
scan_ref="$image"
|
||||
else
|
||||
echo "image or target is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Fixable HIGH/CRITICAL vulnerabilities:"
|
||||
jq -r '
|
||||
# Normalise the comma-separated severity list into a jq array once.
|
||||
sevs_json="$(printf '%s' "$severities" \
|
||||
| tr ',' '\n' \
|
||||
| sed 's/^[[:space:]]*//; s/[[:space:]]*$//' \
|
||||
| grep -v '^$' \
|
||||
| jq -Rs 'split("\n") | map(select(length > 0))')"
|
||||
|
||||
grype "$scan_ref" -o json > "$report"
|
||||
|
||||
echo "Fixable ${severities} vulnerabilities:"
|
||||
jq -r --argjson sevs "$sevs_json" '
|
||||
.matches[]
|
||||
| select(
|
||||
(
|
||||
.vulnerability.severity == "High" or
|
||||
.vulnerability.severity == "Critical"
|
||||
)
|
||||
((.vulnerability.severity) as $s | $sevs | index($s))
|
||||
and
|
||||
(
|
||||
(.vulnerability.fix.versions | length) > 0
|
||||
@@ -66,14 +97,11 @@ runs:
|
||||
| "\(.artifact.name)@\(.artifact.version) -> \(.vulnerability.id) [\(.vulnerability.severity)] | fixed: \(.vulnerability.fix.versions[0])"
|
||||
' "$report"
|
||||
|
||||
fixable="$(jq '
|
||||
fixable="$(jq --argjson sevs "$sevs_json" '
|
||||
[
|
||||
.matches[]
|
||||
| select(
|
||||
(
|
||||
.vulnerability.severity == "High" or
|
||||
.vulnerability.severity == "Critical"
|
||||
)
|
||||
((.vulnerability.severity) as $s | $sevs | index($s))
|
||||
and
|
||||
(
|
||||
(.vulnerability.fix.versions | length) > 0
|
||||
@@ -90,9 +118,9 @@ runs:
|
||||
echo "report-path=${report}"
|
||||
} >> "${GITHUB_OUTPUT:-/dev/null}"
|
||||
|
||||
echo "Fixable HIGH/CRITICAL count: ${fixable} (total findings: ${total})"
|
||||
echo "Fixable ${severities} count: ${fixable} (total findings: ${total})"
|
||||
|
||||
if [ "$fail_on_fixable" = "true" ] && [ "$fixable" != "0" ]; then
|
||||
echo "::error::Grype found ${fixable} fixable HIGH/CRITICAL vulnerabilities."
|
||||
echo "::error::Grype found ${fixable} fixable ${severities} vulnerabilities."
|
||||
exit 1
|
||||
fi
|
||||
Reference in new issue
Block a user