diff --git a/.github/actions/cve-scan/README.md b/.github/actions/cve-scan/README.md index e887c191..39b600a9 100644 --- a/.github/actions/cve-scan/README.md +++ b/.github/actions/cve-scan/README.md @@ -14,7 +14,13 @@ If you are wiring this into a release workflow on the advertised advisory-only p Every outcome writes a Job Summary entry, including the ones that never produce a report — a skipped or inconclusive run is the one most in need of a human noticing. -**Slack only fires on `schedule` and `release` events.** A `workflow_dispatch` or PR run writes the Job Summary but never posts, so testing the action can't spam the channel — the same restriction `govulncheck` applies, for the same reason. A channel that alerts every time an engineer pokes an action is a channel people stop reading. +**Slack fires only on the events in `notify-events`, which defaults to `schedule,release`.** So out of the box a `workflow_dispatch` or PR run writes the Job Summary but never posts, and testing the action can't spam the channel — the same restriction `govulncheck` applies, for the same reason. A channel that alerts every time an engineer pokes an action is a channel people stop reading. + +Widen it when a pipeline genuinely needs to post on another trigger: `notify-events: workflow_dispatch`. Two things to know before you do. + +A reusable workflow sees the **caller's** event name. A `workflow_call` callee invoked from a workflow that was itself dispatched arrives as `workflow_dispatch`, not as whatever published the release, so allow the caller's trigger rather than the one you have in mind. `vcluster-pro`'s release path is exactly this shape: `release.yaml` is `workflow_dispatch`-only by design, so its pre-release pipeline never sees a `release` event. + +The list is matched case- and whitespace-insensitively, and entries match whole, so `release` does not match `prerelease`. An empty value (what `${{ vars.SOME_UNSET_VARIABLE }}` resolves to) warns and falls back to the default rather than silently disabling every notification. A blocked event is annotated with a `::notice::` naming the event and the allowlist, so an absent Slack message is always explainable from the log. Be deliberate about `severity-threshold` on the scheduled sweep. Real images usually carry some `high` findings, so a daily sweep across several images at the default threshold will post most days — and an alert that arrives every day is an alert nobody acts on. Consider `critical` for the schedule and `high` for the release trigger. @@ -22,21 +28,22 @@ Be deliberate about `severity-threshold` on the scheduled sweep. Real images usu -| INPUT | TYPE | REQUIRED | DEFAULT | DESCRIPTION | -|--------------------|--------|----------|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| block-on-findings | string | false | `"false"` | When `true` (or `yes`/`1`/`on`), findings at or
above `severity-threshold` fail the job. When
`false` (default) or unrecognised, findings are
always reported but never fail the
job. A scanner error never fails
the job either way; a config
error always does — see README. | -| dockerfile-path | string | false | | Optional path to the Dockerfile that
built the image, passed to the
scanner as `--file`. This buys base-image
remediation advice only; it does not
change which vulnerabilities are found, since
application dependencies are scanned by default.
Leave empty when scanning a published
image with no checkout. If set,
it must exist. | -| enabled | string | false | `"true"` | Kill switch. Only an explicit falsey
value (`false`/`no`/`0`/`off`, any case) skips the scan, and
the skip is annotated rather than
silent. An unrecognised value scans anyway
and warns — resolving toward scanning
so a typo in a repo
variable can't quietly disable a security
control. | -| image-ref | string | true | | Full registry reference to scan, e.g.
`ghcr.io/loft-sh/vcluster-pro:head` or `ghcr.io/loft-sh/vcluster-pro:v0.38.0-rc.1`. | -| notify | string | false | `"true"` | Send a Slack notification on findings,
a scanner error, or a config
error. Accepts the same tolerant spellings
as `enabled`/`block-on-findings` (`yes`/`1`/`on`, `no`/`0`/`off`, any case); an unrecognised value
still notifies and warns. Only fires
on `schedule` and `release` events —
a `workflow_dispatch` or PR run never notifies,
so testing the action can't spam
the channel. The Job Summary is
written either way. | -| registry | string | false | `"ghcr.io"` | Registry host to authenticate against, e.g.
`ghcr.io`. Only used when `registry-password` is
set. | -| registry-password | string | false | | Password or token for the registry
holding `image-ref`. For GHCR pass the
caller's `secrets.GITHUB_TOKEN` with `packages: read`. Masked in
the log before use. | -| registry-username | string | false | `"${{ github.actor }}"` | Username for the registry holding `image-ref`.
Defaults to the triggering actor, which
is what GHCR expects. GHCR only
checks the token, but docker/login-action requires
a username value to be present
— same constraint promote-release documents. | -| scanner | string | false | `"snyk"` | Which scanner adapter to run. Selects
`src/scanners/.sh`. The action's inputs/outputs never change
when this does. | -| scanner-token | string | false | | Credential the selected adapter needs, if
any. Named generically on purpose —
its meaning depends on `scanner`, so swapping
tools repoints the secret a caller
maps in here without renaming this
input. | -| scanner-version | string | false | `"v1.1306.3"` | Version of the scanner CLI to
install when it isn't already on
the runner. Pinned so results are
reproducible; the adapter installs and checksum-verifies
it. Renovate keeps the default current. | -| severity-threshold | string | false | `"high"` | Minimum severity that counts toward gating:
`critical`, `high`, `medium`, or `low`. | -| slack-webhook-url | string | false | | Slack incoming webhook URL for the
ci-test-notify action. Required when `notify: true`. | +| INPUT | TYPE | REQUIRED | DEFAULT | DESCRIPTION | +|--------------------|--------|----------|-------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| block-on-findings | string | false | `"false"` | When `true` (or `yes`/`1`/`on`), findings at or
above `severity-threshold` fail the job. When
`false` (default) or unrecognised, findings are
always reported but never fail the
job. A scanner error never fails
the job either way; a config
error always does — see README. | +| dockerfile-path | string | false | | Optional path to the Dockerfile that
built the image, passed to the
scanner as `--file`. This buys base-image
remediation advice only; it does not
change which vulnerabilities are found, since
application dependencies are scanned by default.
Leave empty when scanning a published
image with no checkout. If set,
it must exist. | +| enabled | string | false | `"true"` | Kill switch. Only an explicit falsey
value (`false`/`no`/`0`/`off`, any case) skips the scan, and
the skip is annotated rather than
silent. An unrecognised value scans anyway
and warns — resolving toward scanning
so a typo in a repo
variable can't quietly disable a security
control. | +| image-ref | string | true | | Full registry reference to scan, e.g.
`ghcr.io/loft-sh/vcluster-pro:head` or `ghcr.io/loft-sh/vcluster-pro:v0.38.0-rc.1`. | +| notify | string | false | `"true"` | Send a Slack notification on findings,
a scanner error, or a config
error. Accepts the same tolerant spellings
as `enabled`/`block-on-findings` (`yes`/`1`/`on`, `no`/`0`/`off`, any case); an unrecognised value
still notifies and warns. Also gated
by `notify-events`, which by default keeps
a `workflow_dispatch` or PR run silent
so testing the action can't spam
the channel. The Job Summary is
written either way. | +| notify-events | string | false | `"schedule,release"` | Comma-separated allowlist of `github.event_name` values permitted
to notify. Case- and whitespace-insensitive, and
entries match whole, so `release` does
not match `prerelease`. An empty value
warns and falls back to the
default. Note a reusable workflow sees
the *caller's* event name, so a
`workflow_call` callee must allow whatever triggered
the caller. | +| registry | string | false | `"ghcr.io"` | Registry host to authenticate against, e.g.
`ghcr.io`. Only used when `registry-password` is
set. | +| registry-password | string | false | | Password or token for the registry
holding `image-ref`. For GHCR pass the
caller's `secrets.GITHUB_TOKEN` with `packages: read`. Masked in
the log before use. | +| registry-username | string | false | `"${{ github.actor }}"` | Username for the registry holding `image-ref`.
Defaults to the triggering actor, which
is what GHCR expects. GHCR only
checks the token, but docker/login-action requires
a username value to be present
— same constraint promote-release documents. | +| scanner | string | false | `"snyk"` | Which scanner adapter to run. Selects
`src/scanners/.sh`. The action's inputs/outputs never change
when this does. | +| scanner-token | string | false | | Credential the selected adapter needs, if
any. Named generically on purpose —
its meaning depends on `scanner`, so swapping
tools repoints the secret a caller
maps in here without renaming this
input. | +| scanner-version | string | false | `"v1.1306.3"` | Version of the scanner CLI to
install when it isn't already on
the runner. Pinned so results are
reproducible; the adapter installs and checksum-verifies
it. Renovate keeps the default current. | +| severity-threshold | string | false | `"high"` | Minimum severity that counts toward gating:
`critical`, `high`, `medium`, or `low`. | +| slack-webhook-url | string | false | | Slack incoming webhook URL for the
ci-test-notify action. Required when `notify: true`. | @@ -187,6 +194,8 @@ Uploading SARIF to the Security tab (caller's choice — kept out of this action sarif_file: ${{ steps.scan.outputs.sarif-path }} ``` +When the scanner produced a SARIF, the Slack message links to the Security tab filtered to the ref that was scanned. The link is derived from `github.ref`, the same ref `upload-sarif` defaults to, so the two cannot point at different places. The filter is not optional: the tab shows the default branch otherwise, and an RC scanned from a tag reads as empty. Uploading the SARIF elsewhere, or not at all, leaves the link pointing at a view with nothing in it. + ## Suppressing a finding Use Snyk's own [`.snyk` policy file](https://docs.snyk.io/manage-risk/prioritize-issues-for-fixing/ignore-issues). Snyk reads it from the working directory during the scan, so suppressed findings never reach this action and no input is needed: diff --git a/.github/actions/cve-scan/action.yml b/.github/actions/cve-scan/action.yml index d2ee34cc..927d0227 100644 --- a/.github/actions/cve-scan/action.yml +++ b/.github/actions/cve-scan/action.yml @@ -50,9 +50,13 @@ inputs: required: false default: "false" notify: - description: "Send a Slack notification on findings, a scanner error, or a config error. Accepts the same tolerant spellings as `enabled`/`block-on-findings` (`yes`/`1`/`on`, `no`/`0`/`off`, any case); an unrecognised value still notifies and warns. Only fires on `schedule` and `release` events — a `workflow_dispatch` or PR run never notifies, so testing the action can't spam the channel. The Job Summary is written either way." + description: "Send a Slack notification on findings, a scanner error, or a config error. Accepts the same tolerant spellings as `enabled`/`block-on-findings` (`yes`/`1`/`on`, `no`/`0`/`off`, any case); an unrecognised value still notifies and warns. Also gated by `notify-events`, which by default keeps a `workflow_dispatch` or PR run silent so testing the action can't spam the channel. The Job Summary is written either way." required: false default: "true" + notify-events: + description: "Comma-separated allowlist of `github.event_name` values permitted to notify. Case- and whitespace-insensitive, and entries match whole, so `release` does not match `prerelease`. An empty value warns and falls back to the default. Note a reusable workflow sees the *caller's* event name, so a `workflow_call` callee must allow whatever triggered the caller." + required: false + default: "schedule,release" slack-webhook-url: description: "Slack incoming webhook URL for the ci-test-notify action. Required when `notify: true`." required: false @@ -128,19 +132,19 @@ runs: ENABLED: ${{ inputs.enabled }} BLOCK_ON_FINDINGS: ${{ inputs.block-on-findings }} NOTIFY: ${{ inputs.notify }} + NOTIFY_EVENTS: ${{ inputs.notify-events }} run: ${{ github.action_path }}/run.sh - name: Send Slack notification # Two gates, for two different reasons. # - # Event allowlist: only the two triggers this action is built for can - # post. A `workflow_dispatch` run is someone testing, and a channel that + # notify-effective carries both the tolerant `notify` comparison and the + # `notify-events` allowlist, resolved in run.sh where a mismatch can be + # annotated. The allowlist defaults to schedule,release: a channel that # gets a message every time an engineer pokes the action is a channel - # people learn to ignore — which costs more than the alert is worth. - # `govulncheck` restricts to `schedule` for the same reason; the release - # event is added here because the prerelease scan is the case this action - # exists for. Deliberately an allowlist, not a denylist: a new trigger - # should have to opt in rather than start posting by surprise. + # people learn to ignore. Still an allowlist, not a denylist — a new + # trigger opts in by passing notify-events, rather than starting to post + # by surprise. # # Outcome gate: `outcome == 'failure'` is the third case. A config error # (bad severity-threshold, a scanner that couldn't @@ -154,10 +158,12 @@ runs: # arm a killed scheduled scan produces no page and no Job Summary # entry — a security scan that silently stopped scanning, which is the # exact failure this action's error taxonomy exists to prevent. + # A cancel that lands before run.sh resolves notify-effective leaves it + # unset, so notify-events isn't applied on that path: unresolved gates + # resolve toward notifying, same as an unrecognised notify value. if: >- always() && steps.scan.outputs.notify-effective != 'false' && - (github.event_name == 'schedule' || github.event_name == 'release') && (steps.scan.outputs.has-vulnerabilities == 'true' || steps.scan.outputs.scanner-error == 'true' || steps.scan.outcome == 'failure' || @@ -177,9 +183,15 @@ runs: # all, so it can't be distinguished from a config error any other way. # Otherwise ordered so a blocked scan with findings reads as findings, # not as a config error — it fails the job too. + # + # The link is derived, not an input, so it can't drift from the upload: + # upload-sarif defaults to the same github.ref. Needs the ref filter or + # the page shows the default branch and reads as empty, and sits outside + # the fence because Slack won't linkify inside one. details: | ${{ steps.scan.outcome == 'cancelled' && 'CANCELLED — the scan step was killed (job timeout or a manual cancel) before it could finish; nothing was verified' || (steps.scan.outputs.scanner-error == 'true' && 'SCANNER ERROR — no scan was performed, nothing was verified' || (steps.scan.outputs.has-vulnerabilities == 'true' && 'FINDINGS at or above the severity threshold' || 'CONFIGURATION ERROR — cve-scan could not run. An authoring or provisioning mistake, not a scan result; see the job log.')) }} ``` ${{ steps.scan.outputs.summary }} ``` + ${{ steps.scan.outputs.sarif-path != '' && format('Findings: <{0}/{1}/security/code-scanning?query=ref:{2}|View in Security tab>', github.server_url, github.repository, github.ref) || '' }} webhook-url: ${{ inputs.slack-webhook-url }} diff --git a/.github/actions/cve-scan/run.sh b/.github/actions/cve-scan/run.sh index 7ec9c1ad..893ab87e 100755 --- a/.github/actions/cve-scan/run.sh +++ b/.github/actions/cve-scan/run.sh @@ -54,6 +54,34 @@ for flag in ENABLED BLOCK_ON_FINDINGS NOTIFY; do echo "::warning::cve-scan: unrecognised ${input}='${!flag}' — using the safe default" done +# Resolved here rather than in action.yml's if: for the same reason +# notify-effective exists — YAML can't annotate a mismatch, so "schedule, release" +# with a space would silence notifications with nothing saying why. +# Non-colon default: unset takes the default silently, but an explicitly empty +# value (`${{ vars.UNSET }}`) warns instead of looking like it worked. +NOTIFY_EVENTS=$(normalize_flag "${NOTIFY_EVENTS-schedule,release}") +if [ -z "$NOTIFY_EVENTS" ]; then + echo "::warning::cve-scan: empty notify-events — using the safe default 'schedule,release'" + NOTIFY_EVENTS="schedule,release" +fi + +# Empty only when run outside Actions: no event to test, so leave NOTIFY alone +# rather than resolving toward silence. +# `!= false`, not `= true`: to_bool passes an unrecognised value through, so a +# typo'd `truee` would otherwise skip the gate and still satisfy the caller's +# `!= 'false'`. Matching the caller's comparison keeps the two gates independent, +# the way they were when the event check lived in YAML. +EVENT_NAME=$(normalize_flag "${GITHUB_EVENT_NAME:-}") +if [ -n "$EVENT_NAME" ] && [ "$NOTIFY" != false ]; then + # Comma-wrapped so entries match whole: `release` must not match `prerelease`. + if [[ ",${NOTIFY_EVENTS}," != *",${EVENT_NAME},"* ]]; then + NOTIFY=false + # Notice, not warning: staying silent here is the designed behaviour, but + # "why did no Slack message arrive" still needs an answer in the log. + echo "::notice::cve-scan: not notifying on event '${EVENT_NAME}' — notify-events allows '${NOTIFY_EVENTS}'" + fi +fi + write_output() { echo "$1=$2" >> "$GITHUB_OUTPUT" } @@ -67,7 +95,8 @@ write_output() { # pure YAML, so nothing could annotate the mismatch. This output exists so the # caller's if: gets the same tolerant, warned comparison for free, on every # outcome — scanner-error and config-error included, which is exactly when a -# notification matters most. +# notification matters most. The event allowlist folds in here too, so the +# caller's if: is one comparison rather than two. write_output notify-effective "$NOTIFY" # Every outcome leaves a Job Summary entry, including the ones with no report. diff --git a/.github/actions/cve-scan/test/helpers.bash b/.github/actions/cve-scan/test/helpers.bash index 868712db..dfcad7d5 100644 --- a/.github/actions/cve-scan/test/helpers.bash +++ b/.github/actions/cve-scan/test/helpers.bash @@ -12,6 +12,10 @@ grab_output() { # The temp dir, the GITHUB_OUTPUT file and the image ref every suite needs. setup_tmp_env() { + # Actions sets GITHUB_EVENT_NAME on every runner, and run.sh reads it for the + # notify-events allowlist, so inheriting it makes results depend on where the + # suite runs: green locally, red in CI. Tests that care set it themselves. + unset GITHUB_EVENT_NAME TEST_DIR=$(mktemp -d) export TEST_DIR export GITHUB_OUTPUT="$TEST_DIR/github_output" diff --git a/.github/actions/cve-scan/test/run.bats b/.github/actions/cve-scan/test/run.bats index 5120535c..3bdf90e3 100644 --- a/.github/actions/cve-scan/test/run.bats +++ b/.github/actions/cve-scan/test/run.bats @@ -217,7 +217,9 @@ teardown() { has-vulnerabilities=false forged=yes" run bash "$SCRIPT" [ "$status" -eq 1 ] - [[ "$output" == *"whitespace"* ]] + # A newline is a control character, so the control-character guard rejects it + # before the whitespace check is reached — same message as its three siblings. + [[ "$output" == *"control characters"* ]] [ "$(grep -c '^forged=' "$GITHUB_OUTPUT" || true)" = "0" ] } @@ -547,6 +549,109 @@ EOF [ "$(grab_output notify-effective)" = "true" ] } +# --- notify-events allowlist --------------------------------------------------------- +# The allowlist lives here rather than in action.yml's if: so a mistyped or +# whitespaced list annotates instead of silently dropping every notification. +# These cases are what that move buys: none of them is reachable from YAML. + +@test "the default allowlist notifies on schedule and release" { + for e in schedule release; do + GITHUB_EVENT_NAME="$e" NOTIFY=true run bash "$SCRIPT" + [ "$status" -eq 0 ] + [ "$(grab_output notify-effective)" = "true" ] || { + echo "event='$e' should have been allowed by the default" + return 1 + } + done +} + +@test "the default allowlist stays silent on workflow_dispatch and pull_request" { + for e in workflow_dispatch pull_request; do + GITHUB_EVENT_NAME="$e" NOTIFY=true run bash "$SCRIPT" + [ "$status" -eq 0 ] + [ "$(grab_output notify-effective)" = "false" ] || { + echo "event='$e' should not have been allowed by the default" + return 1 + } + done +} + +@test "a blocked event says so, so an absent Slack message is explainable" { + GITHUB_EVENT_NAME=workflow_dispatch NOTIFY=true run bash "$SCRIPT" + [ "$status" -eq 0 ] + [[ "$output" == *"not notifying on event 'workflow_dispatch'"* ]] +} + +@test "a custom allowlist opts one event in without opting the defaults in" { + GITHUB_EVENT_NAME=workflow_dispatch NOTIFY_EVENTS=workflow_dispatch NOTIFY=true run bash "$SCRIPT" + [ "$status" -eq 0 ] + [ "$(grab_output notify-effective)" = "true" ] + + GITHUB_EVENT_NAME=schedule NOTIFY_EVENTS=workflow_dispatch NOTIFY=true run bash "$SCRIPT" + [ "$status" -eq 0 ] + [ "$(grab_output notify-effective)" = "false" ] +} + +# The reason the gate is in bash: `${{ }}` can't strip a space or lowercase a +# word, so "Schedule, Release" typed into a repo variable would match nothing. +@test "the allowlist tolerates whitespace and case, like the other switches" { + GITHUB_EVENT_NAME=release NOTIFY_EVENTS=" Schedule, Release " NOTIFY=true run bash "$SCRIPT" + [ "$status" -eq 0 ] + [ "$(grab_output notify-effective)" = "true" ] +} + +@test "allowlist entries match whole, so release does not match prerelease" { + GITHUB_EVENT_NAME=prerelease NOTIFY_EVENTS=release NOTIFY=true run bash "$SCRIPT" + [ "$status" -eq 0 ] + [ "$(grab_output notify-effective)" = "false" ] +} + +# `notify-events: ${{ vars.UNSET_VARIABLE }}` arrives empty. Reverting to the +# default silently would look like the caller's widening worked. +@test "an empty allowlist warns and falls back to the default" { + GITHUB_EVENT_NAME=schedule NOTIFY_EVENTS="" NOTIFY=true run bash "$SCRIPT" + [ "$status" -eq 0 ] + [ "$(grab_output notify-effective)" = "true" ] + [[ "$output" == *"empty notify-events"* ]] +} + +# The two gates have to stay independent. to_bool passes an unrecognised value +# through, so guarding the allowlist on `= true` would let a typo'd notify skip +# it and still satisfy the caller's `!= 'false'`. +@test "an unrecognised notify value still cannot post on a blocked event" { + GITHUB_EVENT_NAME=workflow_dispatch NOTIFY=truee run bash "$SCRIPT" + [ "$status" -eq 0 ] + [ "$(grab_output notify-effective)" = "false" ] + [[ "$output" == *"unrecognised notify"* ]] +} + +@test "an unrecognised notify value still notifies on an allowed event" { + GITHUB_EVENT_NAME=schedule NOTIFY=truee run bash "$SCRIPT" + [ "$status" -eq 0 ] + [ "$(grab_output notify-effective)" != "false" ] +} + +@test "an allowed event cannot re-enable a caller who asked not to notify" { + GITHUB_EVENT_NAME=schedule NOTIFY=false run bash "$SCRIPT" + [ "$status" -eq 0 ] + [ "$(grab_output notify-effective)" = "false" ] +} + +# Outside Actions there is no event to test. Suppressing on that would make the +# gate fire on every direct invocation, including this suite's other cases. +@test "an absent event name leaves notify untouched" { + NOTIFY=true run bash "$SCRIPT" + [ "$status" -eq 0 ] + [ "$(grab_output notify-effective)" = "true" ] + [[ "$output" != *"not notifying on event"* ]] +} + +@test "the event gate applies on a config-error outcome too" { + GITHUB_EVENT_NAME=pull_request NOTIFY=true SCANNER=made-up-tool run bash "$SCRIPT" + [ "$status" -eq 1 ] + [ "$(grab_output notify-effective)" = "false" ] +} + # --- Input passthrough + output plumbing ---------------------------------------------- @test "passes IMAGE_REF through to process-findings.sh" { diff --git a/.github/workflows/test-cve-scan.yaml b/.github/workflows/test-cve-scan.yaml index 2ace2dc5..14a5304b 100644 --- a/.github/workflows/test-cve-scan.yaml +++ b/.github/workflows/test-cve-scan.yaml @@ -16,9 +16,17 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false + # This action installs bats, it does not run anything. It has no `tests` + # input, and an unknown input is a warning rather than an error, so a job + # that only calls it passes without executing a single test. - uses: bats-core/bats-action@77d6fb60505b4d0d1d73e48bd035b55074bbfb43 # 4.0.0 with: - tests: .github/actions/cve-scan/test + support-install: false + assert-install: false + detik-install: false + file-install: false + - name: Run cve-scan tests + run: bats .github/actions/cve-scan/test/*.bats # Drives the composite through `uses:` so action.yml's own wiring is exercised. # The bats suite invokes src/*.sh directly with env exported by hand, so none diff --git a/README.md b/README.md index d1fe8c56..cf85ed42 100644 --- a/README.md +++ b/README.md @@ -893,7 +893,8 @@ jobs: - `enabled` (optional, default: `true`): kill switch; resolves toward scanning on an unrecognised value - `block-on-findings` (optional, default: `false`): advisory vs. blocking posture - `registry` (optional, default: `ghcr.io`) / `registry-username` / `registry-password`: the action pulls the image with the caller's own credentials rather than relying on the scanner vendor's registry integration -- `notify` / `slack-webhook-url` (optional, default: `true` / —) +- `notify` / `slack-webhook-url` (optional, default: `true` / —): gated by `notify-events` as well, so both have to admit a run before it posts +- `notify-events` (optional, default: `schedule,release`): comma-separated allowlist of `github.event_name` values permitted to notify, so a `workflow_dispatch` or PR run stays silent unless it opts in. A `workflow_call` callee sees the caller's event name **Outputs:**