Skip to content
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
70 changes: 70 additions & 0 deletions .github/scripts/grype-slack-digest.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env bash
# Build a Slack incoming-webhook payload from the grype JSON reports of one
# container-validation run.
#
# Usage: grype-slack-digest.sh <dir>
# <dir>/<grype-LABEL>/grype.json one report per scanned image (download-artifact layout)
#
# Env (all optional, used only for the message header/links):
# RUN_URL link to the workflow run REF branch or PR ref
#
# Prints the JSON payload on stdout, or nothing when there are no findings.
# Findings are de-duplicated across images (one line per package + vulnerability,
# listing every image it affects), sorted by package, then severity (worst first),
# then vulnerability ID, so the message reads as "what is vulnerable right now".
set -euo pipefail

dir="${1:?usage: $0 <dir>}"
shopt -s nullglob
files=("$dir"/*/grype.json)
[ "${#files[@]}" -gt 0 ] || exit 0

jq -n \
--arg run_url "${RUN_URL:-}" \
--arg ref "${REF:-}" \
--argjson max_blocks 45 \
--argjson max_chars 2900 '
def rank: {"Critical":0,"High":1,"Medium":2,"Low":3,"Negligible":4}[.] // 5;

def chunk($lines):
reduce $lines[] as $l ([""];
if (.[-1] | length) + ($l | length) + 1 > $max_chars then . + [$l]
else .[-1] += (if .[-1] == "" then "" else "\n" end) + $l
end);

[inputs
| (input_filename | split("/")[-2] | ltrimstr("grype-")) as $label
| .matches[]
| {pkg: .artifact.name, ver: .artifact.version,
fixed: ((.vulnerability.fix.versions // []) | join(", ")),
id: .vulnerability.id, sev: .vulnerability.severity,
url: (.vulnerability.dataSource // ""), label: $label}] as $rows
| if ($rows | length) == 0 then empty else
($rows | group_by([.pkg, .id]) | map(. + [] | {
pkg: .[0].pkg, id: .[0].id, sev: .[0].sev, url: .[0].url,
ver: ([.[].ver] | unique | join(", ")),
fixed: ([.[].fixed] | unique | join(" | ")),
images: ([.[].label] | unique | join(", "))})
| sort_by([.pkg, (.sev | rank), .id])) as $vulns
| ($vulns | map(
"`\(.pkg)` \(.ver) → \(.fixed) · " +
(if .url != "" then "<\(.url)|\(.id)>" else .id end) +
" · *\(.sev)* · \(.images)")) as $lines
| chunk($lines) as $chunks
| ($vulns | group_by(.sev) | map({(.[0].sev): length}) | add) as $by_sev
| ($by_sev | to_entries | sort_by(.key | rank) | map("\(.value) \(.key)") | join(", ")) as $summary
| {
text: "Grype: \($vulns | length) vulnerabilities in \($rows | map(.label) | unique | length) image(s)",
blocks: (
[{type: "header", text: {type: "plain_text",
text: "Grype findings: \($vulns | length) unique (\($summary))"}},
{type: "context", elements: [{type: "mrkdwn",
text: ("`\($ref)`" + (if $run_url != "" then " · <\($run_url)|workflow run>" else "" end))}]}]
+ ($chunks[:$max_blocks] | map({type: "section", text: {type: "mrkdwn", text: .}}))
+ (if ($chunks | length) > $max_blocks
then [{type: "context", elements: [{type: "mrkdwn",
text: "List truncated; see the workflow run for the full report."}]}]
else [] end))
}
end
' "${files[@]}"
44 changes: 44 additions & 0 deletions .github/workflows/container-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ jobs:
name: 'Verify simplerisk/simplerisk image based on Ubuntu 22.04 (Jammy)'
uses: ./.github/workflows/verify-image_rw.yml
with:
scan_label: "jammy"
context_path: "simplerisk/"
dockerfile_path: "simplerisk/Dockerfile"
image_tag: "simplerisk/simplerisk:testing"
Expand All @@ -22,6 +23,7 @@ jobs:
name: 'Verify simplerisk/simplerisk image based on Ubuntu 24.04 (Noble)'
uses: ./.github/workflows/verify-image_rw.yml
with:
scan_label: "noble"
context_path: "simplerisk/"
dockerfile_path: "simplerisk/Dockerfile"
image_tag: "simplerisk/simplerisk:testing"
Expand All @@ -31,6 +33,7 @@ jobs:
name: 'Verify simplerisk/simplerisk-minimal image based on PHP 8.3 with Apache'
uses: ./.github/workflows/verify-image_rw.yml
with:
scan_label: "minimal-php83"
context_path: "simplerisk-minimal/"
dockerfile_path: "simplerisk-minimal/Dockerfile"
image_tag: "simplerisk/simplerisk-minimal:testing"
Expand All @@ -40,6 +43,7 @@ jobs:
name: 'Verify simplerisk/simplerisk-minimal image based on PHP 8.4 with Apache'
uses: ./.github/workflows/verify-image_rw.yml
with:
scan_label: "minimal-php84"
context_path: "simplerisk-minimal/"
dockerfile_path: "simplerisk-minimal/Dockerfile"
image_tag: "simplerisk/simplerisk-minimal:testing"
Expand All @@ -49,11 +53,51 @@ jobs:
name: 'Verify simplerisk/simplerisk-minimal image based on PHP 8.5 with Apache'
uses: ./.github/workflows/verify-image_rw.yml
with:
scan_label: "minimal-php85"
context_path: "simplerisk-minimal/"
dockerfile_path: "simplerisk-minimal/Dockerfile"
image_tag: "simplerisk/simplerisk-minimal:testing"
build_args: "php_version=8.5\nPREGA_BUNDLE_FALLBACK=true"

grype_slack_digest:
name: 'Post Grype findings digest to Slack'
# always(): a critical finding fails its scan job, and that is exactly when the
# digest matters. Fork PRs get no secrets, so the post is skipped for them.
if: ${{ always() }}
needs:
- simplerisk-jammy
- simplerisk-noble
- simplerisk-minimal-php83
- simplerisk-minimal-php84
- simplerisk-minimal-php85
runs-on: ubuntu-latest
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Download Grype reports
if: ${{ env.SLACK_WEBHOOK_URL != '' }}
uses: actions/download-artifact@v4
with:
pattern: grype-*
path: grype-reports
- name: Post digest to Slack
if: ${{ env.SLACK_WEBHOOK_URL != '' }}
env:
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
REF: ${{ github.head_ref || github.ref_name }}
run: |
payload="$(./.github/scripts/grype-slack-digest.sh grype-reports)"
if [ -z "$payload" ]; then
echo "No Grype findings in any image; nothing to post."
exit 0
fi
curl -sSf -X POST -H 'Content-type: application/json' --data "$payload" "$SLACK_WEBHOOK_URL"
- name: Note skipped post
if: ${{ env.SLACK_WEBHOOK_URL == '' }}
run: echo "::notice::SLACK_WEBHOOK_URL is not available (unset, or a fork PR); skipping the Slack digest."

generator_checks:
name: 'Verify the Dockerfile generators (version/source-mode decoupling)'
runs-on: ubuntu-latest
Expand Down
28 changes: 27 additions & 1 deletion .github/workflows/verify-image_rw.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ on:
dockerfile_path:
required: true
type: string
scan_label:
required: true
type: string
description: Unique label for this scan; names the uploaded grype report artifact.
image_tag:
required: true
type: string
Expand Down Expand Up @@ -37,4 +41,26 @@ jobs:
- name: Install Grype
run: curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
- name: Scan vulnerabilities with Grype
run: grype -o table --fail-on critical --only-fixed ${{ inputs.image_tag }}
# Grype's --sort-by takes a single strategy (package OR severity), so scan to
# JSON and print the table ourselves: package name, then severity (worst first),
# then vulnerability ID. The exit code still comes from grype's --fail-on.
run: |
rc=0
grype -o json=grype.json --fail-on critical --only-fixed ${{ inputs.image_tag }} || rc=$?
if [ -s grype.json ]; then
jq -r '
def rank: {"Critical":0,"High":1,"Medium":2,"Low":3,"Negligible":4}[.] // 5;
(["PACKAGE","INSTALLED","FIXED IN","TYPE","VULNERABILITY","SEVERITY"] | @tsv),
(.matches | sort_by([.artifact.name, (.vulnerability.severity | rank), .vulnerability.id])[]
| [.artifact.name, .artifact.version, ((.vulnerability.fix.versions // []) | join(", ")), .artifact.type, .vulnerability.id, .vulnerability.severity] | @tsv)
' grype.json | column -t -s "$(printf '\t')"
fi
exit "$rc"
- name: Upload Grype report
if: ${{ always() }}
uses: actions/upload-artifact@v4
with:
name: grype-${{ inputs.scan_label }}
path: grype.json
if-no-files-found: ignore
retention-days: 1
49 changes: 16 additions & 33 deletions .grype.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,21 @@ ignore:
- vulnerability: CVE-2025-27558 # Not able to fix it at the moment
# False positive: Grype's binary classifier reads the PHP interpreter's own version
# string (embedded in /usr/local/bin/php, libphp.so, and the bundled extensions such
# as curl.so) as a "curl" binary, then flags these curl CVEs (all fixed in curl
# 8.21.0). The REAL curl is the Debian package, patched (8.14.1-2+deb13u4) and
# correctly not flagged. Scoped per-CVE to binary-classified curl so a genuine future
# curl finding still surfaces instead of being blanket-ignored.
- vulnerability: CVE-2026-11856
package:
name: curl
type: binary
- vulnerability: CVE-2026-10536
package:
name: curl
type: binary
- vulnerability: CVE-2026-8927
package:
name: curl
type: binary
- vulnerability: CVE-2026-8924
package:
name: curl
type: binary
# Same binary-classifier false positive as above, newly published CVE IDs.
# Verified against simplerisk-minimal (php 8.3/8.4/8.5): the only "curl"
# match is /usr/local/lib/php/extensions/.../curl.so reporting the PHP
# version (e.g. 8.5.10) as its own. The real Debian curl package
# (8.14.1-2+deb13u5) is also affected but Debian's tracker marks both IDs
# "wont-fix", so --only-fixed already excludes that (legitimate) match on
# its own; only the misclassified binary match needs ignoring here.
- vulnerability: CVE-2026-19931
package:
name: curl
type: binary
- vulnerability: CVE-2026-18924
package:
# as curl.so) as a "curl" binary, then flags every curl CVE fixed after that number
# (PHP 8.5.x is read as "curl 8.5.x"). The REAL curl is the Debian package
# (8.14.1-x), which grype scans separately as a deb. Scoped by package + location
# instead of per-CVE, so new curl CVEs no longer need a new entry each; a curl found
# anywhere outside PHP's extension directory (or as a deb) still surfaces.
- package:
name: curl
type: binary
location: "/usr/local/lib/php/extensions/**"
# False positive: the swagger-api/swagger-ui Composer package ships a release-script
# template at swagger-ui-dist-package/package.json whose version is the literal
# placeholder "$$VERSION". Grype cannot parse it, so it matches every swagger-ui-dist
# advisory. Nothing loads this file; the UI actually served is vendor/.../dist/
# (5.x, past the 4.1.3 fix).
- package:
name: swagger-ui-dist
type: npm
location: "/var/www/simplerisk/vendor/swagger-api/swagger-ui/swagger-ui-dist-package/**"
1 change: 1 addition & 0 deletions simplerisk/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ RUN mkdir -p /configurations \
RUN dpkg-divert --local --rename /usr/bin/ischroot && \
ln -sf /bin/true /usr/bin/ischroot && \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get -y upgrade && \
DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends apache2 \
php \
php-mysql \
Expand Down
1 change: 1 addition & 0 deletions simplerisk/generate_dockerfile.sh
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ RUN mkdir -p /configurations \\
RUN dpkg-divert --local --rename /usr/bin/ischroot && \\
ln -sf /bin/true /usr/bin/ischroot && \\
apt-get update && \\
DEBIAN_FRONTEND=noninteractive apt-get -y upgrade && \\
DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends apache2 \\
php \\
php-mysql \\
Expand Down
Loading