npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

workflow-sentinel

v1.1.1

Published

Harden GitHub Actions: flags unpinned actions, broad or missing permissions, pull_request_target pwn requests, script injection, leaked secrets, persisted credentials, self-hosted PR runners, curl|bash, deprecated commands and undigested images, with a fi

Readme

workflow-sentinel

npm version CI Node License: MIT

Catch insecure GitHub Actions workflows before they're merged.

workflow-sentinel reads your .github/workflows/*.yml and action.yml files and flags the patterns behind most real-world Actions compromises: mutable action tags, over-privileged tokens, pull_request_target "pwn requests", script injection, leaked secrets and more. Every finding includes a file and line number, a severity and a concrete fix. --fix pins your actions to commit SHAs for you.

CLI, library and GitHub Action. Node.js 18+, one small runtime dependency (yaml).

Rules

| Rule | Severity | What it catches | |---|---|---| | dangerous-checkout | critical | pull_request_target / workflow_run that checks out the PR head (ref: ${{ github.event.pull_request.head.sha }}, github.head_ref, refs/pull/…, gh pr checkout) | | unpinned-action | high | uses: with a tag or branch instead of a 40-char commit SHA (steps, reusable workflows, composite actions) | | broad-permissions | high | permissions: write-all at workflow or job level. Top-level write scopes shared by several jobs are reported as low | | script-injection | high (critical under pull_request_target) | ${{ github.event.issue.title }}, …pull_request.body, github.head_ref, comment bodies, commit messages and similar values pasted into run: or actions/github-script. ${{ inputs.* }} in composite actions is reported as medium | | secrets-untrusted | high | Secrets used in a job that runs checked-out PR code. Echoing ${{ secrets.* }} is reported as medium | | self-hosted-pr | high | runs-on: self-hosted in a workflow triggered by pull requests or comments | | missing-permissions | medium | No top-level permissions: and jobs without their own | | persist-credentials | medium | actions/checkout without persist-credentials: false | | secrets-inherit | medium | Reusable workflow called with secrets: inherit | | curl-pipe-shell | medium | curl … \| bash, wget -O- … \| sh, bash <(curl …), sh -c "$(curl …)" | | deprecated-command | medium (high for set-env/add-path) | ::set-output, ::save-state, ::set-env, ::add-path | | docker-no-digest | medium | docker:// actions, container:, services: and Docker action.yml images without @sha256: | | cache-poisoning | high | Untrusted github.event.* / branch names in actions/cache keys or paths (including via tainted env) | | artipacked | medium | upload-artifact after checkout without persist-credentials: false, broad artifact paths, risky download-artifact in privileged workflows | | invalid-expression | medium | Mistyped ${{ }} contexts (github.events), properties unavailable for your on: triggers, unknown functions | | invalid-schema | low | Unknown workflow/job/step keys (e.g. job: instead of jobs:), likely typos in action inputs |

Install and run

npx workflow-sentinel                      # scan the current repository
npx workflow-sentinel .github/workflows/release.yml --fail-on medium
npx workflow-sentinel --trusted-owner actions --trusted-owner github
GH_TOKEN=... npx workflow-sentinel --fix   # pin every action to its current commit SHA (# v4 comment for Dependabot)

Sample output

workflow-sentinel — 3 file(s) scanned

.github/workflows/ci.yml
     1  MEDIUM   No top-level `permissions:`; job "test" runs with the default GITHUB_TOKEN scopes [missing-permissions]
        ↳ fix: Without a `permissions:` block the GITHUB_TOKEN gets the repository default, which may be read/write. Add `permissions: { contents: read }` at the top level and widen per job only where needed.
     9  HIGH     actions/checkout@v4 is pinned to "v4" (mutable tag), not a commit SHA [unpinned-action]
        ↳ fix: Pin it: `uses: actions/checkout@<40-char-sha> # v4` (or run `workflow-sentinel --fix`). Tags and branches are mutable; a compromised upstream can retag.
     9  MEDIUM   actions/checkout@v4 without `persist-credentials: false` in job "test" [persist-credentials]
        ↳ fix: Add to the step: `with: { persist-credentials: false }` (keep it only if a later step must `git push`).
...
9 high, 5 medium
✖ Failing (fail-on: high).

Options

| Option | Description | |---|---| | [paths…] | Files or directories to scan (default: .github/workflows plus every action.yml under the current directory, skipping node_modules, dist and similar folders) | | --fail-on <sev> | critical, high (default), medium, low, info or none | | --config <file> | Config file path | | --trusted-owner <o> | Allow tags for an owner (actions) or a repo (github/codeql-action). Repeatable | | --fix / --fix-dry-run | Resolve each tag or branch to its commit SHA through the GitHub API (GH_TOKEN or GITHUB_TOKEN) and rewrite uses: as owner/repo@<sha> # v4 (Dependabot-compatible SHA comments) | | --json [file], --sarif <file>, --markdown <file> | Reports | | --no-fixes, --no-color | Text output tweaks |

Exit codes: 0 passed · 1 findings at or above --fail-on · 2 usage, config or runtime error.

Configuration

Put the config in .workflow-sentinel.json (or a "workflow-sentinel" key in package.json):

{
  "failOn": "high",
  "trustedOwners": ["actions", "github/codeql-action"],
  "rules": { "persist-credentials": "low", "curl-pipe-shell": "off" },
  "ignore": [".github/workflows/legacy-*.yml"],
  "allow": [
    "unpinned-action:my-org/*",
    { "rule": "persist-credentials", "file": ".github/workflows/release.yml", "reason": "pushes tags" },
    { "rule": "self-hosted-pr", "expires": "2026-12-31", "reason": "migrating runners" }
  ]
}
  • rules sets a severity per rule, or off.
  • allow matches on any combination of rule, file (glob) and target (an action like actions/*, or an image). "rule:target" is a shorthand. An entry stops applying after its expires date.
  • Inline: add # workflow-sentinel-ignore [rule …] at the end of a line, or on its own line just above it.

Suppressed findings are counted in the summary and listed under suppressed in the JSON.

GitHub Action

name: Workflow security
on: [push, pull_request]
permissions:
  contents: read
  security-events: write
jobs:
  workflow-sentinel:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
        with: { persist-credentials: false }
      - uses: MrLouisWright/workflow-sentinel@v1
        with:
          fail-on: high
          sarif: workflow-sentinel.sarif
      - if: always()
        uses: github/codeql-action/upload-sarif@v3
        with: { sarif_file: workflow-sentinel.sarif }

Inputs: paths, fail-on, config, trusted-owners (space-separated), sarif, json, job-summary (default true) and args.

Library

ESM and CommonJS (require) are both supported. TypeScript declarations are generated from source at build time.

import { scan, scanText, formatSarif } from 'workflow-sentinel';
const { findings } = scan({ cwd: '.', config: { trustedOwners: ['actions'] } });
const { scanText, validateExpression } = require('workflow-sentinel');

Tests live in test/ and run on every push in .github/workflows/ci.yml (including import + require checks). Releases publish to npm with --provenance.

Limitations

This is a static, pattern-based linter. It doesn't follow data across jobs or into the scripts your workflows call, and it can't know which of your self-hosted runners are ephemeral. Treat a clean result as a good baseline, not a guarantee.

License

MIT © Louis Wright