Skip to content

Add the scan-community-node reusable workflow - #3

Closed
bennycode wants to merge 1 commit into
mainfrom
add-scan-community-node
Closed

bennycode wants to merge 1 commit into
mainfrom
add-scan-community-node

Conversation

@bennycode

@bennycode bennycode commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

What changed and why

Ports the community node security scan from n8n-io/scan-community-node-action into 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.yml fans 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 under scan-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 through upload-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>-*.yml and docs under <package>/. CI also checks that such a package has a README.

ci-scan-community-node.yml exercises both modes on pull requests that touch the package.

Replaces #2, which predates most of the pipeline's current shape.

Review in cubic

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>/.
@bennycode
bennycode marked this pull request as ready for review September 28, 2026 19:35

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Loading

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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/*'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
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 }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
package: ${{ inputs.package }}
package: <package-name> # leave empty to scan this repository (for the ${{ inputs.* }} form, see examples/ci-security-scan.yml)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant