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 |
|
| 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`. |
|
| `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
|
## Usage
|
||||||
|
|
||||||
@@ -75,6 +76,27 @@ jobs:
|
|||||||
run: docker push "${IMAGE}"
|
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
|
## Interface
|
||||||
|
|
||||||
### `install-grype`
|
### `install-grype`
|
||||||
@@ -90,24 +112,53 @@ Outputs: `grype-path`, `version`.
|
|||||||
Supports `amd64` and `arm64`. Cosign is checksum-pinned per architecture; Grype
|
Supports `amd64` and `arm64`. Cosign is checksum-pinned per architecture; Grype
|
||||||
is verified against its keyless-signed checksum file.
|
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`
|
### `scan-image`
|
||||||
|
|
||||||
| Input | Default | Notes |
|
| 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. |
|
| `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. |
|
| `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`.
|
Outputs: `fixable-count`, `total-count`, `report-path`.
|
||||||
|
|
||||||
The step exits non-zero when fixable High/Critical findings exist and
|
The step exits non-zero when fixable findings exist at the selected severities
|
||||||
`fail-on-fixable` is `true`. Combine with `continue-on-error: true` and
|
and `fail-on-fixable` is `true`. Combine with `continue-on-error: true` and
|
||||||
`steps.<id>.outcome` to implement notify-then-fail, as above.
|
`steps.<id>.outcome` to implement notify-then-fail, as above.
|
||||||
|
|
||||||
## Versioning
|
## Versioning
|
||||||
|
|
||||||
`install-grype` centralises the Cosign and Grype versions. Bump them here,
|
`install-grype` and `install-syft` centralise the Cosign, Grype, and Syft
|
||||||
deliberately, and tag a new release (`v1`, `v2`, ...) so callers can pin.
|
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
|
## 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
|
name: Scan Image
|
||||||
description: >-
|
description: >-
|
||||||
Scan a container image with Grype and fail the step on fixable High/Critical
|
Scan a container image, directory, or SBOM with Grype and fail the step on
|
||||||
findings. Writes the raw JSON report so the caller can notify or act on it.
|
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.
|
# Mechanism only. The caller keeps the policy: notify, fail, block a push, etc.
|
||||||
#
|
#
|
||||||
@@ -10,16 +11,29 @@ description: >-
|
|||||||
|
|
||||||
inputs:
|
inputs:
|
||||||
image:
|
image:
|
||||||
description: Image reference (tag or digest) to scan.
|
description: >-
|
||||||
required: true
|
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:
|
report-path:
|
||||||
description: Where to write the raw Grype JSON report, relative to the workspace.
|
description: Where to write the raw Grype JSON report, relative to the workspace.
|
||||||
required: false
|
required: false
|
||||||
default: grype.json
|
default: grype.json
|
||||||
fail-on-fixable:
|
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
|
required: false
|
||||||
default: 'true'
|
default: 'true'
|
||||||
|
severities:
|
||||||
|
description: >-
|
||||||
|
Comma-separated severities that count as findings (e.g. High,Critical).
|
||||||
|
required: false
|
||||||
|
default: High,Critical
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
fixable-count:
|
fixable-count:
|
||||||
@@ -39,25 +53,42 @@ runs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
INPUT_IMAGE: ${{ inputs.image }}
|
INPUT_IMAGE: ${{ inputs.image }}
|
||||||
|
INPUT_TARGET: ${{ inputs.target }}
|
||||||
INPUT_REPORT_PATH: ${{ inputs.report-path }}
|
INPUT_REPORT_PATH: ${{ inputs.report-path }}
|
||||||
INPUT_FAIL_ON_FIXABLE: ${{ inputs.fail-on-fixable }}
|
INPUT_FAIL_ON_FIXABLE: ${{ inputs.fail-on-fixable }}
|
||||||
|
INPUT_SEVERITIES: ${{ inputs.severities }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
image="${INPUT_IMAGE:?image is required}"
|
image="${INPUT_IMAGE:-}"
|
||||||
|
target="${INPUT_TARGET:-}"
|
||||||
report="${INPUT_REPORT_PATH:-grype.json}"
|
report="${INPUT_REPORT_PATH:-grype.json}"
|
||||||
fail_on_fixable="${INPUT_FAIL_ON_FIXABLE:-true}"
|
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:"
|
# Normalise the comma-separated severity list into a jq array once.
|
||||||
jq -r '
|
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[]
|
.matches[]
|
||||||
| select(
|
| select(
|
||||||
(
|
((.vulnerability.severity) as $s | $sevs | index($s))
|
||||||
.vulnerability.severity == "High" or
|
|
||||||
.vulnerability.severity == "Critical"
|
|
||||||
)
|
|
||||||
and
|
and
|
||||||
(
|
(
|
||||||
(.vulnerability.fix.versions | length) > 0
|
(.vulnerability.fix.versions | length) > 0
|
||||||
@@ -66,14 +97,11 @@ runs:
|
|||||||
| "\(.artifact.name)@\(.artifact.version) -> \(.vulnerability.id) [\(.vulnerability.severity)] | fixed: \(.vulnerability.fix.versions[0])"
|
| "\(.artifact.name)@\(.artifact.version) -> \(.vulnerability.id) [\(.vulnerability.severity)] | fixed: \(.vulnerability.fix.versions[0])"
|
||||||
' "$report"
|
' "$report"
|
||||||
|
|
||||||
fixable="$(jq '
|
fixable="$(jq --argjson sevs "$sevs_json" '
|
||||||
[
|
[
|
||||||
.matches[]
|
.matches[]
|
||||||
| select(
|
| select(
|
||||||
(
|
((.vulnerability.severity) as $s | $sevs | index($s))
|
||||||
.vulnerability.severity == "High" or
|
|
||||||
.vulnerability.severity == "Critical"
|
|
||||||
)
|
|
||||||
and
|
and
|
||||||
(
|
(
|
||||||
(.vulnerability.fix.versions | length) > 0
|
(.vulnerability.fix.versions | length) > 0
|
||||||
@@ -90,9 +118,9 @@ runs:
|
|||||||
echo "report-path=${report}"
|
echo "report-path=${report}"
|
||||||
} >> "${GITHUB_OUTPUT:-/dev/null}"
|
} >> "${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
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
Reference in new issue
Block a user