Conversation
Ports the security scan pipeline from n8n-io/scan-community-node-action as a releasable package. One entry workflow fans out to five scanners, GuardDog, Semgrep, OpenSSF Scorecard, CVE Lite CLI and Gitleaks, each a reusable workflow of its own. Helper actions, docs and an example caller live under scan-community-node/. Cross-file references use the `$/` self-repository prefix, every third-party action is pinned to a commit SHA and the Scorecard image to a digest. The release and CI workflows learn to treat a reusable workflow as a package: it is released under its file name, with sub-workflows named <package>-*.yml and docs under <package>/.
There was a problem hiding this comment.
19 issues found across 16 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name=".github/workflows/scan-community-node-cve-lite.yml">
<violation number="1" location=".github/workflows/scan-community-node-cve-lite.yml:52">
P2: Running npm inside the extracted, untrusted package tree lets the scanned (possibly malicious) package influence the scan: npm reads project-level `.npmrc` first, so a tarball that ships one can redirect the registry/proxy or inject token config into the resolution of the very tree this scanner distrusts. `--ignore-scripts` and `--package-lock-only` limit the blast radius (no code runs, tarballs aren't downloaded), but the resolved tree then comes from an attacker-chosen endpoint. Point npm at an empty config so the resolution cannot be steered by the scanned package.</violation>
<violation number="2" location=".github/workflows/scan-community-node-cve-lite.yml:52">
P2: `npm install --package-lock-only` is treated as a hard requirement, but it is only best-effort context: the comment itself notes that without a lockfile only pinned direct deps are scanned, and CVE Lite can still scan the package.json. Broken, incomplete, or deliberately malformed packages — exactly what this scanner exists to find — commonly fail npm resolution (invalid package.json, git:/file: protocols, opaque registry errors). When it fails, `Prepare scan target` fails, `steps.prep.outputs.path` is never set, and `Run CVE Lite CLI` runs with an empty `path` and fails, so the CVE scan silently misses the very packages that need it. Make the lockfile generation best-effort so the scan still runs when no lockfile can be produced.</violation>
</file>
<file name=".github/workflows/scan-community-node-gitleaks.yml">
<violation number="1" location=".github/workflows/scan-community-node-gitleaks.yml:48">
P2: `continue-on-error: true` on the combined pack/extract/scan step swallows genuine "scanner cannot run" failures, contradicting the package README contract ("A job fails only when a scanner cannot run"). Gitleaks exits 1 when leaks are found, which is why the suppression exists, but a bad `package`/`version` (npm pack failure), an extraction failure, or a gitleaks crash also passes the job and the summary then reports "No findings." for a scan that never ran. Keep finding-suppression but fail on run failures: remove `continue-on-error` and let the script exit non-zero unless gitleaks itself finishes with the findings exit code (e.g. `set +e; gitleaks dir ...; status=$?; [ "$status" -le 1 ] || exit "$status"`), or split the pack/extract commands into their own step without `continue-on-error`.</violation>
</file>
<file name="scan-community-node/actions/security-summary/action.yml">
<violation number="1" location="scan-community-node/actions/security-summary/action.yml:16">
P2: sarif-tools is resolved to the latest release on every run because `uvx --from sarif-tools` has no version pin. Every other pinned dependency in this package is pinned to a commit SHA or image digest (setup-uv, setup-node, Scorecard image), and the README/PR description call out reproducibility as a goal. An unpinned CLI means the summary and CSV layout can change between runs without any repo change, silently altering report content or breaking the `wc -l`/"No findings" branch. Pin a version, e.g. `uvx --from "sarif-tools==1.x.y"`.</violation>
<violation number="2" location="scan-community-node/actions/security-summary/action.yml:18">
P3: `mktemp -u` prints a name without creating or reserving the file, which is a time-of-check/time-of-use race that the mktemp documentation explicitly warns against; if another process creates that path first, `sarif csv -o` would overwrite it silently. Use `mktemp --suffix=.csv` (or remove the `.csv` suffix requirement) so the path is safely created.</violation>
<violation number="3" location="scan-community-node/actions/security-summary/action.yml:37">
P2: This escape only neutralizes lines that start exactly with a backtick, but a markdown closing code fence may be preceded by up to three spaces of indentation (and followed only by whitespace). Scan output containing e.g. ` ```` ` stays untouched and closes the fence, so the rest of the untrusted content renders as markdown in the step summary — contradicting the comment's claim. Escape every fence-shaped line instead of only lines whose first character is a backtick.</violation>
</file>
<file name="scan-community-node/README.md">
<violation number="1" location="scan-community-node/README.md:30">
P2: This guarantee doesn't match the workflows in the same PR. The GuardDog job (`.github/workflows/scan-community-node-guarddog.yml`) and the Semgrep job (`.github/workflows/scan-community-node-semgrep.yml`) run without `continue-on-error`, and both tools exit non-zero when they get findings (GuardDog on a malicious package, Semgrep on matched rules), with the GuardDog step even setting `pipefail` so that exit code is preserved. Only the Gitleaks job (line 48 of `scan-community-node-gitleaks.yml`) uses `continue-on-error: true`. So a package with malware, or a repo with Semgrep matches, fails these jobs — the opposite of "none of them fails its job on findings". Either add `continue-on-error: true` to the GuardDog and Semgrep run steps to match the documented design, or reword this section to say findings fail the job.</violation>
<violation number="2" location="scan-community-node/README.md:45">
P3: The "minimal form" snippet references `${{ inputs.package }}`, but the snippet as shown defines no input. In a caller workflow without `workflow_dispatch` or `workflow_call` inputs, `${{ inputs.package }}` fails before the run starts with "Unrecognized named-value: 'inputs'". Only `examples/ci-security-scan.yml` defines that input; the minimal snippet omits the `on:` block, so a reader who copies it verbatim gets a broken workflow. Show the `on:` block with the input, or use a literal placeholder here and leave the `inputs` version to the example.</violation>
<violation number="3" location="scan-community-node/README.md:72">
P3: This sentence garbles the exception: "always emit SARIF, CVE Lite CLI unless..." needs a clause boundary so the lockfile qualification applies only to CVE Lite CLI. Reword to e.g. "Semgrep and Gitleaks always emit SARIF, and CVE Lite CLI does too, unless the target has no lockfile."</violation>
</file>
<file name=".github/workflows/release.yml">
<violation number="1" location=".github/workflows/release.yml:20">
P3: The `package` input description still reads 'Action to release', but the new `scan-community-node` option is a reusable workflow (no `scan-community-node/action.yml` exists; it is released via `.github/workflows/scan-community-node.yml`). Update the description to something like 'Package to release' so the dispatch form matches the available options.</violation>
</file>
<file name=".github/workflows/scan-community-node-semgrep.yml">
<violation number="1" location=".github/workflows/scan-community-node-semgrep.yml:45">
P3: `tar -xzf ./*.tgz` assumes the packed tarball is the only `.tgz` in the workspace root. If the caller's checkout already contains a `.tgz`, the glob expands to multiple files and tar treats the extras as member names, so extraction fails or extracts the wrong content and the scan job fails. Capture the filename printed by `npm pack` instead, as the CVE Lite sibling in this package does (`ARCHIVE="$(npm pack --silent "$PACKAGE_SPEC")"`), and use `tar -xzf "$ARCHIVE"`.</violation>
<violation number="2" location=".github/workflows/scan-community-node-semgrep.yml:51">
P2: The Semgrep CLI is unpinned in both branches: `uvx semgrep scan` resolves the latest semgrep release from PyPI at run time, and `--config auto` pulls the floating registry rule set. Scan behavior and results can therefore change silently in every consuming repo, which contradicts the pinning policy the sibling scanners in this package follow (Scorecard digest-pins its image at `ghcr.io/ossf/scorecard:v5.5.0@sha256:...`; Gitleaks pins `GITLEAKS_VERSION=8.30.1` plus a verified SHA-256). Pin the semgrep version in both the package and workspace branches, e.g. `uvx semgrep@<pinned-version> scan --config auto ...`, so analysis runs are reproducible.</violation>
</file>
<file name=".github/workflows/scan-community-node-scorecard.yml">
<violation number="1" location=".github/workflows/scan-community-node-scorecard.yml:56">
P3: The Upload Scorecard SARIF step lacks `if: always()`, so when the scorecard scan step fails, the SARIF report (which may already be written) is never uploaded. The upload action itself gates on `always()`, showing the intent to upload whatever exists, and the sibling cve-lite workflow applies `if: always()` to its upload step for exactly this reason. Add `if: always()` here so a scanner failure doesn't silently drop code-scanning alerts.</violation>
</file>
<file name=".github/workflows/scan-community-node-guarddog.yml">
<violation number="1" location=".github/workflows/scan-community-node-guarddog.yml:36">
P2: This step has no `continue-on-error: true`, unlike the Gitleaks scan step. GuardDog exits non-zero when its scanners flag a package at or above the severity threshold, so a finding fails the job — contradicting the README contract in this PR that "none of them fails its job on findings. A job fails only when a scanner cannot run." Add `continue-on-error: true` if findings should not turn the check red.</violation>
<violation number="2" location=".github/workflows/scan-community-node-guarddog.yml:53">
P2: `uvx guarddog` installs the latest guarddog release from PyPI on every run, so scanner behavior and report content are not reproducible. This contradicts the pinning policy exercised everywhere else in this package (Gitleaks pins version + SHA-256, Scorecard pins the image digest) and means a newly released, or compromised, guarddog version silently changes results for all callers. Pin the version, e.g. `uvx --from 'guarddog==<version>' guarddog ...`.</violation>
<violation number="3" location=".github/workflows/scan-community-node-guarddog.yml:63">
P2: This upload step has no `if: always()`, so when the GuardDog step fails (e.g. it exits non-zero on a finding in local-verify mode, the only mode that produces `guarddog.sarif`), the opted-in SARIF upload is skipped exactly when there is a report to upload. The cve-lite workflow in this same PR sets `if: always()` on its upload step, and the composite action's own guard is `always() && ...`. Add `if: always()` here.</violation>
</file>
<file name="scan-community-node/actions/upload-security-sarif/action.yml">
<violation number="1" location="scan-community-node/actions/upload-security-sarif/action.yml:21">
P2: `upload-sarif` fails hard on pull requests from forks, and this step has no graceful fallback. For a fork PR, GitHub grants `GITHUB_TOKEN` read-only permissions (`security-events: write` is ignored), so `github/codeql-action/upload-sarif` returns 403 and the whole scan job/run fails — even though the scan itself succeeded. The shipped example caller enables SARIF upload on `pull_request`, so community repos adopting this package get failing CI on every fork PR, which is the main audience here. Add a fork check to the `if` condition, or `continue-on-error` if the step can tolerate a skip.</violation>
</file>
<file name="scan-community-node/actions/setup/action.yml">
<violation number="1" location="scan-community-node/actions/setup/action.yml:19">
P3: `node-version: 'lts/*'` resolves to whatever latest LTS is at run time, so the Node-dependent scanners (semgrep, gitleaks, cve-lite) behave differently as LTS versions change. Everything else in this package is pinned to a commit SHA or image digest for reproducibility; pin this to a specific version (e.g. `'22'`) the same way.</violation>
</file>
<file name="scan-community-node/examples/ci-security-scan.yml">
<violation number="1" location="scan-community-node/examples/ci-security-scan.yml:46">
P2: On `pull_request`, `inputs.package` is always empty, so SARIF upload is attempted on every PR. For the intended audience (public community-node repos) most contributors open fork PRs, where `GITHUB_TOKEN` is read-only and `github/codeql-action/upload-sarif` fails with 403 — turning the scan red on every external PR. Skip uploads outside `push`/`schedule`; the scan itself still runs on PRs.</violation>
</file>
Architecture diagram
sequenceDiagram
participant Consumer as Consumer Repo
participant Entry as scan-community-node.yml
participant GuardDog as GuardDog Workflow
participant Semgrep as Semgrep Workflow
participant Scorecard as Scorecard Workflow
participant CveLite as CVE Lite Workflow
participant Gitleaks as Gitleaks Workflow
participant Setup as Setup Action
participant Summary as Security Summary Action
participant Upload as Upload SARIF Action
participant NPM as npm Registry
participant Docker as Docker Hub
participant CodeScan as GitHub Code Scanning
Note over Consumer,CodeScan: Community Node Security Scan Flow
Consumer->>Entry: workflow_call (package, version, sandbox, upload-sarif)
Entry->>Entry: Write test target summary
Entry->>GuardDog: with (package, version, sandbox, upload-sarif)
Entry->>Semgrep: with (package, version, upload-sarif)
Entry->>Scorecard: with (package, upload-sarif)
Entry->>CveLite: with (package, version, upload-sarif)
Entry->>Gitleaks: with (package, version, upload-sarif)
par Parallel Scanner Jobs
GuardDog->>Setup: checkout + setup
Semgrep->>Setup: checkout + setup (node)
Scorecard->>Setup: checkout + setup
CveLite->>Setup: checkout + setup (node)
Gitleaks->>Setup: checkout + setup (node)
alt Package Scan Mode (package set)
Semgrep->>NPM: npm pack package@version
NPM-->>Semgrep: tarball
CveLite->>NPM: npm pack package@version
NPM-->>CveLite: tarball
Gitleaks->>NPM: npm pack package@version
NPM-->>Gitleaks: tarball
Scorecard->>Docker: docker run scorecard --npm=package
Docker-->>Scorecard: scorecard.txt
GuardDog->>GuardDog: uvx guarddog npm scan
else Workspace Mode (package empty)
Semgrep->>Semgrep: uvx semgrep scan .
GuardDog->>GuardDog: uvx guarddog npm verify
Scorecard->>Scorecard: ossf/scorecard-action
CveLite->>CveLite: cve-lite-cli local path
Gitleaks->>Gitleaks: gitleaks dir .
end
Semgrep->>Semgrep: Write SARIF to security-report/
Gitleaks->>Gitleaks: Write SARIF to security-report/
CveLite->>CveLite: Write SARIF to security-report/
GuardDog->>GuardDog: Write report to security-report/
Scorecard->>Scorecard: Write report to security-report/
GuardDog->>Summary: Render findings to step summary
Semgrep->>Summary: Render findings to step summary
Scorecard->>Summary: Render findings to step summary
CveLite->>Summary: Render findings to step summary
Gitleaks->>Summary: Render findings to step summary
alt upload-sarif enabled AND workspace mode AND SARIF exists
Semgrep->>Upload: Upload SARIF
CveLite->>Upload: Upload SARIF
Gitleaks->>Upload: Upload SARIF
Upload->>CodeScan: Upload SARIF files
else Package scan mode - skip upload
GuardDog->>Upload: Skip (not our findings)
Scorecard->>Upload: Skip (not our findings)
end
end
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| tar -xzf "$ARCHIVE" -C "$DIR" --strip-components=1 | ||
| # Tarballs ship no lockfile; without one only pinned direct deps are scanned. | ||
| # --package-lock-only resolves the tree without downloading or running anything. | ||
| (cd "$DIR" && npm install --package-lock-only --ignore-scripts) |
There was a problem hiding this comment.
P2: Running npm inside the extracted, untrusted package tree lets the scanned (possibly malicious) package influence the scan: npm reads project-level .npmrc first, so a tarball that ships one can redirect the registry/proxy or inject token config into the resolution of the very tree this scanner distrusts. --ignore-scripts and --package-lock-only limit the blast radius (no code runs, tarballs aren't downloaded), but the resolved tree then comes from an attacker-chosen endpoint. Point npm at an empty config so the resolution cannot be steered by the scanned package.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/scan-community-node-cve-lite.yml, line 52:
<comment>Running npm inside the extracted, untrusted package tree lets the scanned (possibly malicious) package influence the scan: npm reads project-level `.npmrc` first, so a tarball that ships one can redirect the registry/proxy or inject token config into the resolution of the very tree this scanner distrusts. `--ignore-scripts` and `--package-lock-only` limit the blast radius (no code runs, tarballs aren't downloaded), but the resolved tree then comes from an attacker-chosen endpoint. Point npm at an empty config so the resolution cannot be steered by the scanned package.</comment>
<file context>
@@ -0,0 +1,77 @@
+ tar -xzf "$ARCHIVE" -C "$DIR" --strip-components=1
+ # Tarballs ship no lockfile; without one only pinned direct deps are scanned.
+ # --package-lock-only resolves the tree without downloading or running anything.
+ (cd "$DIR" && npm install --package-lock-only --ignore-scripts)
+ # Hand the directory to the scan step as steps.prep.outputs.path.
+ echo "path=$DIR" >> "$GITHUB_OUTPUT"
</file context>
| tar -xzf "$ARCHIVE" -C "$DIR" --strip-components=1 | ||
| # Tarballs ship no lockfile; without one only pinned direct deps are scanned. | ||
| # --package-lock-only resolves the tree without downloading or running anything. | ||
| (cd "$DIR" && npm install --package-lock-only --ignore-scripts) |
There was a problem hiding this comment.
P2: npm install --package-lock-only is treated as a hard requirement, but it is only best-effort context: the comment itself notes that without a lockfile only pinned direct deps are scanned, and CVE Lite can still scan the package.json. Broken, incomplete, or deliberately malformed packages — exactly what this scanner exists to find — commonly fail npm resolution (invalid package.json, git:/file: protocols, opaque registry errors). When it fails, Prepare scan target fails, steps.prep.outputs.path is never set, and Run CVE Lite CLI runs with an empty path and fails, so the CVE scan silently misses the very packages that need it. Make the lockfile generation best-effort so the scan still runs when no lockfile can be produced.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/scan-community-node-cve-lite.yml, line 52:
<comment>`npm install --package-lock-only` is treated as a hard requirement, but it is only best-effort context: the comment itself notes that without a lockfile only pinned direct deps are scanned, and CVE Lite can still scan the package.json. Broken, incomplete, or deliberately malformed packages — exactly what this scanner exists to find — commonly fail npm resolution (invalid package.json, git:/file: protocols, opaque registry errors). When it fails, `Prepare scan target` fails, `steps.prep.outputs.path` is never set, and `Run CVE Lite CLI` runs with an empty `path` and fails, so the CVE scan silently misses the very packages that need it. Make the lockfile generation best-effort so the scan still runs when no lockfile can be produced.</comment>
<file context>
@@ -0,0 +1,77 @@
+ tar -xzf "$ARCHIVE" -C "$DIR" --strip-components=1
+ # Tarballs ship no lockfile; without one only pinned direct deps are scanned.
+ # --package-lock-only resolves the tree without downloading or running anything.
+ (cd "$DIR" && npm install --package-lock-only --ignore-scripts)
+ # Hand the directory to the scan step as steps.prep.outputs.path.
+ echo "path=$DIR" >> "$GITHUB_OUTPUT"
</file context>
| rm -f gitleaks gitleaks.tar.gz | ||
|
|
||
| - name: Run Gitleaks | ||
| continue-on-error: true |
There was a problem hiding this comment.
P2: continue-on-error: true on the combined pack/extract/scan step swallows genuine "scanner cannot run" failures, contradicting the package README contract ("A job fails only when a scanner cannot run"). Gitleaks exits 1 when leaks are found, which is why the suppression exists, but a bad package/version (npm pack failure), an extraction failure, or a gitleaks crash also passes the job and the summary then reports "No findings." for a scan that never ran. Keep finding-suppression but fail on run failures: remove continue-on-error and let the script exit non-zero unless gitleaks itself finishes with the findings exit code (e.g. set +e; gitleaks dir ...; status=$?; [ "$status" -le 1 ] || exit "$status"), or split the pack/extract commands into their own step without continue-on-error.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/scan-community-node-gitleaks.yml, line 48:
<comment>`continue-on-error: true` on the combined pack/extract/scan step swallows genuine "scanner cannot run" failures, contradicting the package README contract ("A job fails only when a scanner cannot run"). Gitleaks exits 1 when leaks are found, which is why the suppression exists, but a bad `package`/`version` (npm pack failure), an extraction failure, or a gitleaks crash also passes the job and the summary then reports "No findings." for a scan that never ran. Keep finding-suppression but fail on run failures: remove `continue-on-error` and let the script exit non-zero unless gitleaks itself finishes with the findings exit code (e.g. `set +e; gitleaks dir ...; status=$?; [ "$status" -le 1 ] || exit "$status"`), or split the pack/extract commands into their own step without `continue-on-error`.</comment>
<file context>
@@ -0,0 +1,72 @@
+ rm -f gitleaks gitleaks.tar.gz
+
+ - name: Run Gitleaks
+ continue-on-error: true
+ env:
+ PACKAGE: ${{ inputs.package }}
</file context>
| # compgen -G expands a glob and fails when nothing matches, so it doubles as an existence test. | ||
| if compgen -G "$DIR/*.sarif" > /dev/null; then | ||
| # sarif-tools reads every SARIF file in the directory. | ||
| uvx --from sarif-tools sarif summary "$DIR" > "$REPORT" |
There was a problem hiding this comment.
P2: sarif-tools is resolved to the latest release on every run because uvx --from sarif-tools has no version pin. Every other pinned dependency in this package is pinned to a commit SHA or image digest (setup-uv, setup-node, Scorecard image), and the README/PR description call out reproducibility as a goal. An unpinned CLI means the summary and CSV layout can change between runs without any repo change, silently altering report content or breaking the wc -l/"No findings" branch. Pin a version, e.g. uvx --from "sarif-tools==1.x.y".
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scan-community-node/actions/security-summary/action.yml, line 16:
<comment>sarif-tools is resolved to the latest release on every run because `uvx --from sarif-tools` has no version pin. Every other pinned dependency in this package is pinned to a commit SHA or image digest (setup-uv, setup-node, Scorecard image), and the README/PR description call out reproducibility as a goal. An unpinned CLI means the summary and CSV layout can change between runs without any repo change, silently altering report content or breaking the `wc -l`/"No findings" branch. Pin a version, e.g. `uvx --from "sarif-tools==1.x.y"`.</comment>
<file context>
@@ -0,0 +1,46 @@
+ # compgen -G expands a glob and fails when nothing matches, so it doubles as an existence test.
+ if compgen -G "$DIR/*.sarif" > /dev/null; then
+ # sarif-tools reads every SARIF file in the directory.
+ uvx --from sarif-tools sarif summary "$DIR" > "$REPORT"
+ # summary abbreviates each rule, so append the full per-finding CSV.
+ CSV="$(mktemp -u).csv"
</file context>
| | `scan-for-vulnerabilities` | [CVE Lite CLI](https://github.com/OWASP/cve-lite-cli) | Known vulnerabilities (CVEs) in the dependency tree, matched against OSV and the npm advisory API, with the upgrade commands that fix them | | ||
| | `scan-for-secrets` | [Gitleaks](https://github.com/gitleaks/gitleaks) | Hardcoded secrets and leaked credentials | | ||
|
|
||
| Every scanner reports; none of them fails its job on findings. A job fails only when a scanner cannot run. |
There was a problem hiding this comment.
P2: This guarantee doesn't match the workflows in the same PR. The GuardDog job (.github/workflows/scan-community-node-guarddog.yml) and the Semgrep job (.github/workflows/scan-community-node-semgrep.yml) run without continue-on-error, and both tools exit non-zero when they get findings (GuardDog on a malicious package, Semgrep on matched rules), with the GuardDog step even setting pipefail so that exit code is preserved. Only the Gitleaks job (line 48 of scan-community-node-gitleaks.yml) uses continue-on-error: true. So a package with malware, or a repo with Semgrep matches, fails these jobs — the opposite of "none of them fails its job on findings". Either add continue-on-error: true to the GuardDog and Semgrep run steps to match the documented design, or reword this section to say findings fail the job.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scan-community-node/README.md, line 30:
<comment>This guarantee doesn't match the workflows in the same PR. The GuardDog job (`.github/workflows/scan-community-node-guarddog.yml`) and the Semgrep job (`.github/workflows/scan-community-node-semgrep.yml`) run without `continue-on-error`, and both tools exit non-zero when they get findings (GuardDog on a malicious package, Semgrep on matched rules), with the GuardDog step even setting `pipefail` so that exit code is preserved. Only the Gitleaks job (line 48 of `scan-community-node-gitleaks.yml`) uses `continue-on-error: true`. So a package with malware, or a repo with Semgrep matches, fails these jobs — the opposite of "none of them fails its job on findings". Either add `continue-on-error: true` to the GuardDog and Semgrep run steps to match the documented design, or reword this section to say findings fail the job.</comment>
<file context>
@@ -0,0 +1,76 @@
+| `scan-for-vulnerabilities` | [CVE Lite CLI](https://github.com/OWASP/cve-lite-cli) | Known vulnerabilities (CVEs) in the dependency tree, matched against OSV and the npm advisory API, with the upgrade commands that fix them |
+| `scan-for-secrets` | [Gitleaks](https://github.com/gitleaks/gitleaks) | Hardcoded secrets and leaked credentials |
+
+Every scanner reports; none of them fails its job on findings. A job fails only when a scanner cannot run.
+
+## Usage
</file context>
| Every scanner reports; none of them fails its job on findings. A job fails only when a scanner cannot run. | |
| Every scanner reports. GuardDog and Semgrep fail the job when findings are detected, since the GuardDog and Semgrep steps exit non-zero on findings; Gitleaks, CVE Lite CLI and Scorecard report without failing the job. | |
| (If the intended behavior is that no job ever fails on findings, add `continue-on-error: true` to the GuardDog and Semgrep run steps instead, mirroring the Gitleaks job.) |
| # and disable its default ignores so shipped dist/ code is scanned. | ||
| DIR="$RUNNER_TEMP/semgrep-target" | ||
| rm -rf "$DIR" && mkdir -p "$DIR" | ||
| npm pack "$PACKAGE_SPEC" |
There was a problem hiding this comment.
P3: tar -xzf ./*.tgz assumes the packed tarball is the only .tgz in the workspace root. If the caller's checkout already contains a .tgz, the glob expands to multiple files and tar treats the extras as member names, so extraction fails or extracts the wrong content and the scan job fails. Capture the filename printed by npm pack instead, as the CVE Lite sibling in this package does (ARCHIVE="$(npm pack --silent "$PACKAGE_SPEC")"), and use tar -xzf "$ARCHIVE".
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/scan-community-node-semgrep.yml, line 45:
<comment>`tar -xzf ./*.tgz` assumes the packed tarball is the only `.tgz` in the workspace root. If the caller's checkout already contains a `.tgz`, the glob expands to multiple files and tar treats the extras as member names, so extraction fails or extracts the wrong content and the scan job fails. Capture the filename printed by `npm pack` instead, as the CVE Lite sibling in this package does (`ARCHIVE="$(npm pack --silent "$PACKAGE_SPEC")"`), and use `tar -xzf "$ARCHIVE"`.</comment>
<file context>
@@ -0,0 +1,63 @@
+ # and disable its default ignores so shipped dist/ code is scanned.
+ DIR="$RUNNER_TEMP/semgrep-target"
+ rm -rf "$DIR" && mkdir -p "$DIR"
+ npm pack "$PACKAGE_SPEC"
+ tar -xzf ./*.tgz -C "$DIR" --strip-components=1
+ : > "$DIR/.semgrepignore"
</file context>
| uses: $/scan-community-node/actions/security-summary | ||
|
|
||
| - name: Upload Scorecard SARIF file to GitHub | ||
| uses: $/scan-community-node/actions/upload-security-sarif |
There was a problem hiding this comment.
P3: The Upload Scorecard SARIF step lacks if: always(), so when the scorecard scan step fails, the SARIF report (which may already be written) is never uploaded. The upload action itself gates on always(), showing the intent to upload whatever exists, and the sibling cve-lite workflow applies if: always() to its upload step for exactly this reason. Add if: always() here so a scanner failure doesn't silently drop code-scanning alerts.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/scan-community-node-scorecard.yml, line 56:
<comment>The Upload Scorecard SARIF step lacks `if: always()`, so when the scorecard scan step fails, the SARIF report (which may already be written) is never uploaded. The upload action itself gates on `always()`, showing the intent to upload whatever exists, and the sibling cve-lite workflow applies `if: always()` to its upload step for exactly this reason. Add `if: always()` here so a scanner failure doesn't silently drop code-scanning alerts.</comment>
<file context>
@@ -0,0 +1,60 @@
+ uses: $/scan-community-node/actions/security-summary
+
+ - name: Upload Scorecard SARIF file to GitHub
+ uses: $/scan-community-node/actions/upload-security-sarif
+ with:
+ category: scorecard
</file context>
| - if: ${{ inputs.node == 'true' }} | ||
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | ||
| with: | ||
| node-version: 'lts/*' |
There was a problem hiding this comment.
P3: node-version: 'lts/*' resolves to whatever latest LTS is at run time, so the Node-dependent scanners (semgrep, gitleaks, cve-lite) behave differently as LTS versions change. Everything else in this package is pinned to a commit SHA or image digest for reproducibility; pin this to a specific version (e.g. '22') the same way.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scan-community-node/actions/setup/action.yml, line 19:
<comment>`node-version: 'lts/*'` resolves to whatever latest LTS is at run time, so the Node-dependent scanners (semgrep, gitleaks, cve-lite) behave differently as LTS versions change. Everything else in this package is pinned to a commit SHA or image digest for reproducibility; pin this to a specific version (e.g. `'22'`) the same way.</comment>
<file context>
@@ -0,0 +1,23 @@
+ - if: ${{ inputs.node == 'true' }}
+ uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
+ with:
+ node-version: 'lts/*'
+
+ - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
</file context>
| node-version: 'lts/*' | |
| node-version: '22' |
|
|
||
| Every scan writes a human-readable summary to the workflow run's step summary. Each scanner writes its report files into a `security-report/` directory in the workspace. | ||
|
|
||
| Semgrep, Gitleaks and CVE Lite CLI always emit SARIF, CVE Lite CLI unless the target has no lockfile. GuardDog and Scorecard emit SARIF when scanning the calling repository and their native text report for a third-party package, since they cannot emit SARIF in that mode. |
There was a problem hiding this comment.
P3: This sentence garbles the exception: "always emit SARIF, CVE Lite CLI unless..." needs a clause boundary so the lockfile qualification applies only to CVE Lite CLI. Reword to e.g. "Semgrep and Gitleaks always emit SARIF, and CVE Lite CLI does too, unless the target has no lockfile."
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scan-community-node/README.md, line 72:
<comment>This sentence garbles the exception: "always emit SARIF, CVE Lite CLI unless..." needs a clause boundary so the lockfile qualification applies only to CVE Lite CLI. Reword to e.g. "Semgrep and Gitleaks always emit SARIF, and CVE Lite CLI does too, unless the target has no lockfile."</comment>
<file context>
@@ -0,0 +1,76 @@
+
+Every scan writes a human-readable summary to the workflow run's step summary. Each scanner writes its report files into a `security-report/` directory in the workspace.
+
+Semgrep, Gitleaks and CVE Lite CLI always emit SARIF, CVE Lite CLI unless the target has no lockfile. GuardDog and Scorecard emit SARIF when scanning the calling repository and their native text report for a third-party package, since they cannot emit SARIF in that mode.
+
+## Testing
</file context>
| Semgrep, Gitleaks and CVE Lite CLI always emit SARIF, CVE Lite CLI unless the target has no lockfile. GuardDog and Scorecard emit SARIF when scanning the calling repository and their native text report for a third-party package, since they cannot emit SARIF in that mode. | |
| Semgrep and Gitleaks always emit SARIF, and CVE Lite CLI does too, unless the target has no lockfile. GuardDog and Scorecard emit SARIF when scanning the calling repository and their native text report for a third-party package, since they cannot emit SARIF in that mode. |
| scan: | ||
| uses: n8n-io/github-actions/.github/workflows/scan-community-node.yml@<sha> # v1.0.0 | ||
| with: | ||
| package: ${{ inputs.package }} |
There was a problem hiding this comment.
P3: The "minimal form" snippet references ${{ inputs.package }}, but the snippet as shown defines no input. In a caller workflow without workflow_dispatch or workflow_call inputs, ${{ inputs.package }} fails before the run starts with "Unrecognized named-value: 'inputs'". Only examples/ci-security-scan.yml defines that input; the minimal snippet omits the on: block, so a reader who copies it verbatim gets a broken workflow. Show the on: block with the input, or use a literal placeholder here and leave the inputs version to the example.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scan-community-node/README.md, line 45:
<comment>The "minimal form" snippet references `${{ inputs.package }}`, but the snippet as shown defines no input. In a caller workflow without `workflow_dispatch` or `workflow_call` inputs, `${{ inputs.package }}` fails before the run starts with "Unrecognized named-value: 'inputs'". Only `examples/ci-security-scan.yml` defines that input; the minimal snippet omits the `on:` block, so a reader who copies it verbatim gets a broken workflow. Show the `on:` block with the input, or use a literal placeholder here and leave the `inputs` version to the example.</comment>
<file context>
@@ -0,0 +1,76 @@
+ scan:
+ uses: n8n-io/github-actions/.github/workflows/scan-community-node.yml@<sha> # v1.0.0
+ with:
+ package: ${{ inputs.package }}
+ upload-sarif: true
+```
</file context>
| package: ${{ inputs.package }} | |
| package: <package-name> # leave empty to scan this repository (for the ${{ inputs.* }} form, see examples/ci-security-scan.yml) |
What changed and why
Ports the community node security scan from
n8n-io/scan-community-node-actioninto this repository as a releasable package, so public community node repositories can call it. That repository is private, and this one is already the org's home for shared CI.The entry workflow
.github/workflows/scan-community-node.ymlfans out to five scanners, each a reusable workflow of its own: GuardDog for malware, Semgrep for insecure code, OpenSSF Scorecard for repository posture, CVE Lite CLI for vulnerable dependencies and Gitleaks for secrets. Helper actions, the package README and an example caller live underscan-community-node/. Cross-file references use the$/self-repository prefix, so callers check nothing out. Every third-party action is pinned to a commit SHA and the Scorecard image to a digest, per this repository's rules.Every scanner writes into
security-report/, the summary action renders that directory into the step summary, and the SARIF upload to code scanning is opt-in throughupload-sarif. The called workflows declare no permissions and inherit the caller's grant.The release and CI workflows learn to treat a reusable workflow as a package: released under its file name, with sub-workflows named
<package>-*.ymland docs under<package>/. CI also checks that such a package has a README.ci-scan-community-node.ymlexercises both modes on pull requests that touch the package.Replaces #2, which predates most of the pipeline's current shape.