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
14 changes: 14 additions & 0 deletions .github/actions/grype-report/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ inputs:
description: Slack incoming webhook. When empty, the Slack post is skipped.
required: false
default: ''
always_post:
description: >
'true' posts the per-image table to Slack even when there are no findings, so a
quiet run still shows that its scans ran. Otherwise Slack only hears about findings.
required: false
default: 'false'
context:
description: Text shown in the Slack message (workflow name and branch).
required: false
Expand Down Expand Up @@ -45,7 +51,15 @@ runs:
SLACK_WEBHOOK_URL: ${{ inputs.slack_webhook_url }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
REF: ${{ inputs.context }}
RESULTS: ${{ inputs.results }}
ALWAYS_POST: ${{ inputs.always_post }}
run: |
if [ "$ALWAYS_POST" = "true" ]; then
mkdir -p grype-reports
mapfile -t pairs < <(printf '%s\n' "$RESULTS" | sed '/^[[:space:]]*$/d')
SUMMARY="$(SUMMARY_FORMAT=slack "${{ github.action_path }}/../../scripts/grype-scan-summary.sh" grype-reports "${pairs[@]}")"
export SUMMARY
fi
payload="$("${{ github.action_path }}/../../scripts/grype-slack-digest.sh" grype-reports)"
if [ -z "$payload" ]; then
echo "No Grype findings in any image; nothing to post."
Expand Down
18 changes: 13 additions & 5 deletions .github/scripts/grype-scan-summary.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@ set -euo pipefail
dir="${1:?usage: $0 <dir> <label>=<result>...}"
shift

echo "### Grype scan results"
echo
echo "| Image | Result |"
echo "| --- | --- |"
# SUMMARY_FORMAT=slack prints mrkdwn lines for the Slack digest instead of a table.
format="${SUMMARY_FORMAT:-markdown}"
if [ "$format" != "slack" ]; then
echo "### Grype scan results"
echo
echo "| Image | Result |"
echo "| --- | --- |"
fi
for pair in "$@"; do
label="${pair%%=*}"
result="${pair#*=}"
Expand All @@ -37,5 +41,9 @@ for pair in "$@"; do
status="⚠️ ${count} findings (${detail})"
fi
fi
echo "| \`${label}\` | ${status} |"
if [ "$format" = "slack" ]; then
echo "\`${label}\` ${status}"
else
echo "| \`${label}\` | ${status} |"
fi
done
24 changes: 20 additions & 4 deletions .github/scripts/grype-slack-digest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
# 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.
# SUMMARY optional per-image status lines (from grype-scan-summary.sh, slack format).
# When set, a payload is produced even with no findings (the all-clear
# table), so a quiet run still shows that its scans ran.
#
# Prints the JSON payload on stdout, or nothing when there are no findings and no SUMMARY.
# 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".
Expand All @@ -17,11 +21,12 @@ set -euo pipefail
dir="${1:?usage: $0 <dir>}"
shopt -s nullglob
files=("$dir"/*/grype.json)
[ "${#files[@]}" -gt 0 ] || exit 0
[ "${#files[@]}" -gt 0 ] || [ -n "${SUMMARY:-}" ] || exit 0

jq -n \
--arg run_url "${RUN_URL:-}" \
--arg ref "${REF:-}" \
--arg summary "${SUMMARY:-}" \
--argjson max_blocks 45 \
--argjson max_chars 2900 '
def rank: {"Critical":0,"High":1,"Medium":2,"Low":3,"Negligible":4}[.] // 5;
Expand All @@ -39,7 +44,17 @@ jq -n \
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
| ($summary | if . == "" then [] else [{type: "section", text: {type: "mrkdwn", text: .}}] end) as $summary_blocks
| if ($rows | length) == 0 then
(if $summary == "" then empty else {
text: "Grype scan results",
blocks: (
[{type: "header", text: {type: "plain_text", text: "Grype scan results"}},
{type: "context", elements: [{type: "mrkdwn",
text: ("`\($ref)`" + (if $run_url != "" then " · <\($run_url)|workflow run>" else "" end))}]}]
+ $summary_blocks)
} end)
else
($rows | group_by([.pkg, .id]) | map(. + [] | {
pkg: .[0].pkg, id: .[0].id, sev: .[0].sev, url: .[0].url,
ver: ([.[].ver] | unique | join(", ")),
Expand All @@ -60,11 +75,12 @@ jq -n \
text: "Grype findings: \($vulns | length) unique (\($summary))"}},
{type: "context", elements: [{type: "mrkdwn",
text: ("`\($ref)`" + (if $run_url != "" then " · <\($run_url)|workflow run>" else "" end))}]}]
+ $summary_blocks
+ ($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[@]}"
' ${files[@]+"${files[@]}"}
1 change: 1 addition & 0 deletions .github/workflows/container-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ jobs:
with:
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
context: ${{ github.workflow }} · ${{ github.head_ref || github.ref_name }}
always_post: 'true'
results: |
jammy=${{ needs.simplerisk-jammy.result }}
noble=${{ needs.simplerisk-noble.result }}
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/scheduled-grype-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ jobs:
with:
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
context: ${{ github.workflow }} · testing
always_post: 'true'
results: |
testing-minimal=${{ needs.scan-minimal.result }}
testing-full=${{ needs.scan-full.result }}
Loading