Skip to content

runloopai/lisan-al-gaib-action

Repository files navigation

Lisan al-Gaib

A GitHub Action that acts as a supply-chain security gate by failing if newly added or updated packages were published less than a configurable number of days ago.

Supported ecosystems

Ecosystem Lockfiles Registry
npm pnpm-lock.yaml, package-lock.json, yarn.lock, bun.lock npm registry
python uv.lock, *.py.lock (script lockfiles), pylock.toml (PEP 751) PyPI
rust MODULE.bazel with crate.spec() crates.io
java MODULE.bazel with maven.install() + JSON lock files Maven Central / custom repos
bazel MODULE.bazel.lock Bazel Central Registry (BCR)
actions .github/workflows/*.yml, action.yml GitHub API
multitool multitool.hub() lockfiles via MODULE.bazel Archive Last-Modified headers

Quick start

- uses: runloopai/lisan-al-gaib-action@main
  with:
    ecosystems: npm

Supported event types

The action auto-detects the base ref to diff against based on the GitHub event:

Event Base ref
pull_request / pull_request_target PR base SHA
push payload.before SHA
merge_group Merge group base SHA
release target_commitish
schedule, workflow_dispatch, workflow_call, workflow_run HEAD~1

Falls back to HEAD~1, then origin/main, then the empty tree (initial commit) if the resolved ref doesn't exist.

You can always override with the base-ref input.

Inputs

Input Required Default Description
ecosystems Yes Comma-separated list: npm, python, rust, java, bazel, actions, multitool
min-age-days No 14 Minimum days since publication to pass
warn-age-days No 21 Age threshold for warnings (between min and warn = warning, above = pass)
base-ref No auto-detect Git ref to diff against
node-lockfiles No auto-detect Newline-separated glob patterns for Node.js lockfiles
python-lockfiles No auto-detect Newline-separated glob patterns for Python lockfiles
module-bazel No MODULE.bazel Path to root MODULE.bazel (for rust/java/bazel/multitool ecosystems)
workflow-files No auto-detect Newline-separated glob patterns for workflow files (for actions ecosystem)
strict-third-party No false Fail (instead of warn) on archive overrides without Last-Modified and third-party branch-pinned actions
bypass-keyword No "" If the PR body contains this string on a line by itself, failures are downgraded to warnings
check-all-on-new-workflow No true Check all packages (not just changed) when the workflow file is newly added
github-token No ${{ github.token }} GitHub token for API queries (actions and bazel ecosystems)
npm-registry-url No https://registry.npmjs.org npm registry URL
pypi-registry-url No https://pypi.org PyPI registry URL
crates-registry-url No https://crates.io crates.io registry URL
maven-registry-url No https://repo1.maven.org/maven2 Maven Central registry URL
target-licenses No auto SPDX license(s) your project is distributed under. Deps must be compatible with the target. Supports per-ecosystem YAML map, special aliases (open-source, open-source-no-strong-copyleft, open-source-no-relinkable-copyleft, open-source-no-network-copyleft), or auto to detect from package.json/LICENSE (falls back to open-source-no-relinkable-copyleft if detection fails). Empty string disables license checking.
allowed-licenses No "" Deprecated — use target-licenses. Ignored when target-licenses is set.
age-overrides No "" YAML map of ecosystem → list of package names to skip age checking for specific packages
license-overrides No "" YAML map of ecosystem → package → SPDX license or "ignore" to override or skip license checking
license-heuristics No false When true, infer licenses from LICENSE/README file text using heuristic matching. When false, only use registry metadata and GitHub API.
bcr-url No https://bcr.bazel.build Bazel Central Registry URL

Outputs

Output Description
total-checked Number of packages checked
total-failures Number of packages that failed the age gate
total-warnings Number of packages in the warning zone
license-violations Number of packages with incompatible licenses

Examples

Check npm dependencies on PRs

name: Dependency Check
on: pull_request

permissions:
  contents: read

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: runloopai/lisan-al-gaib-action@main
        with:
          ecosystems: npm

Multiple ecosystems with custom thresholds

- uses: runloopai/lisan-al-gaib-action@main
  with:
    ecosystems: npm,python,rust,java
    min-age-days: "7"
    warn-age-days: "14"

Scheduled scan

name: Weekly Dependency Scan
on:
  schedule:
    - cron: "0 9 * * 1"

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2
      - uses: runloopai/lisan-al-gaib-action@main
        with:
          ecosystems: npm,python

Monorepo with multiple lockfiles

- uses: runloopai/lisan-al-gaib-action@main
  with:
    ecosystems: npm
    node-lockfiles: |
      apps/*/pnpm-lock.yaml
      packages/*/package-lock.json

Check GitHub Actions versions

- uses: runloopai/lisan-al-gaib-action@main
  with:
    ecosystems: actions

Actions pinned to a branch (e.g. @main) are skipped. Actions pinned to a tag (e.g. @v4) or commit SHA are checked against the GitHub API for their publish/commit date.

Check Bazel module dependencies

- uses: runloopai/lisan-al-gaib-action@main
  with:
    ecosystems: bazel

Parses MODULE.bazel.lock for resolved module versions and queries the Bazel Central Registry. Handles overrides from MODULE.bazel:

  • git_override: checks the commit/tag/branch date via GitHub API
  • archive_override: checks the archive URL's Last-Modified header
  • local_path_override: skipped
  • single_version_override / multiple_version_override: checked against BCR with the overridden version

License compliance

- uses: runloopai/lisan-al-gaib-action@main
  with:
    ecosystems: npm
    # Auto-detect your project's license from package.json or LICENSE file.
    # Falls back to open-source-no-relinkable-copyleft if detection fails.
    target-licenses: auto

    # Or specify your project's license explicitly
    # target-licenses: "MIT"

    # Per-ecosystem targets (YAML map)
    # target-licenses: |
    #   "*": Apache-2.0
    #   rust: Apache-2.0, MIT
    #   npm: MIT

    # Special aliases:
    # target-licenses: "open-source"                           # any OSI-approved license
    # target-licenses: "open-source-no-relinkable-copyleft"    # OSI minus LGPL/GPL/AGPL (allows MPL, CDDL, EPL)
    # target-licenses: "open-source-no-strong-copyleft"        # OSI minus GPL/AGPL (allows LGPL, MPL, CDDL, EPL)
    # target-licenses: "open-source-no-network-copyleft"       # OSI minus AGPL

    # Disable license checking
    # target-licenses: ""

For every analyzed dependency, the action fetches the license from the package registry (npm, PyPI, crates.io, Maven POM, GitHub API, BCR metadata) and checks directional compatibility — whether the dependency's license allows incorporation into a project under your target license. This uses a full SPDX compatibility matrix (permissive → copyleft flow, GPL version compatibility, weak copyleft rules, etc.). Incompatible licenses produce error annotations and fail the check.

License and age overrides

- uses: runloopai/lisan-al-gaib-action@main
  with:
    ecosystems: npm,python
    age-overrides: |
      npm:
        - some-legacy-package
      python:
        - internal-tool
    license-overrides: |
      npm:
        custom-pkg: MIT
      actions:
        owner/repo: ignore

When license violations or unknown licenses are detected, the action suggests a license-overrides block as a colored git diff of your workflow file.

Check Bazel multitool binaries

- uses: runloopai/lisan-al-gaib-action@main
  with:
    ecosystems: multitool

Parses multitool.hub() calls from MODULE.bazel (following include() statements), finds the referenced lockfiles, and diffs HEAD vs base to detect changed tool binaries. Each binary's publish date is checked via the Last-Modified header of its download URL.

Custom registry URL

- uses: runloopai/lisan-al-gaib-action@main
  with:
    ecosystems: npm
    npm-registry-url: "https://npm.pkg.github.com"

Remediation

When violations are detected, the action suggests package manager-level settings to prevent installing young packages:

Package manager Config file Setting
pnpm pnpm-workspace.yaml minimumReleaseAge: 20160 (minutes)
yarn .yarnrc.yml npmMinimalAgeGate: "14d"
bun bunfig.toml [install] minimumReleaseAge = 1209600 (seconds)
uv pyproject.toml or uv.toml [tool.uv] exclude-newer = "14 days"

How it works

  1. Resolve base ref from the GitHub event context (PR base, push before, etc.)
  2. Detect changed lockfiles by diffing HEAD against the base ref
  3. Parse lockfiles using structured parsers (lockparse for npm/pnpm/yarn/bun, smol-toml for Python, web-tree-sitter for Bazel/Starlark)
  4. Compare HEAD vs base lockfile contents to find new or version-changed packages
  5. Query registries for each changed package's publish date
  6. Report results as GitHub annotations (errors/warnings) and a job summary table

For Rust and Java ecosystems, the action parses MODULE.bazel using a tree-sitter Starlark grammar, resolving recursive include() statements to find all crate.spec() and maven.install() blocks.

For the Bazel ecosystem, it parses MODULE.bazel.lock (JSON) to find resolved module versions and extracts override directives (git_override, archive_override, etc.) from MODULE.bazel files.

For the Actions ecosystem, it parses workflow YAML files for uses: directives, determines whether each ref is a tag or commit SHA (branches are skipped), and queries the GitHub API for the associated date.

For the Multitool ecosystem, it finds multitool.hub() calls in MODULE.bazel, reads the referenced lockfiles (JSON), and checks each binary's download URL for a Last-Modified header.

Bypass for emergency fixes

If you need to merge a PR with a dependency that fails the age gate (e.g., a critical 0-day vulnerability fix), set the bypass-keyword input:

- uses: runloopai/lisan-al-gaib-action@main
  with:
    ecosystems: npm
    bypass-keyword: "DEPENDENCY-AGE-BYPASS"

The bypass is detected from any of the following (whichever matches first):

  1. PR body — include the keyword on its own line:
    This PR updates lodash to fix CVE-2025-XXXX.
    
    DEPENDENCY-AGE-BYPASS
    
  2. PR label — add a label named exactly DEPENDENCY-AGE-BYPASS to the PR
  3. Commit message — include the keyword on its own line in the HEAD commit message (useful for push, workflow_dispatch, and other non-PR events)

The action will still report the failures as warnings but will not fail the check.

Note: If using label-based bypass, add labeled and unlabeled to the pull_request event types so the workflow re-runs when labels change:

on:
  pull_request:
    types: [opened, reopened, synchronize, edited, labeled, unlabeled]

The edited type ensures the workflow re-runs when the PR body is changed to add the keyword.

Running locally

You can run the check on a local repository without GitHub Actions:

# Install dependencies
pnpm install

# Compare against remote default branch (e.g., origin/main)
pnpm local -- --ecosystems npm

# Compare dirty (uncommitted) changes against HEAD
pnpm local -- --diff --ecosystems npm

# Compare against a specific ref
pnpm local -- --base-ref origin/release-2.0 --ecosystems npm,python

# Check ALL dependencies (not just changed)
pnpm local -- --all --ecosystems npm

# With custom thresholds
pnpm local -- --ecosystems npm --min-age-days 7 --warn-age-days 14

CLI options

Option Description
--ecosystems <list> Comma-separated ecosystems (default: npm)
--base-ref <ref> Git ref to diff against (default: remote default branch)
--diff Compare working tree against HEAD
--all Check all dependencies (uses empty tree as base)
--min-age-days <n> Minimum age in days (default: 14)
--warn-age-days <n> Warning threshold in days (default: 21)
--github-token <t> GitHub token (default: $GITHUB_TOKEN env var)

You can also run directly with Node after building:

pnpm build
node out/cli.js --ecosystems npm --diff

Etymology

Lisan al-Gaib (لسان الغيب, "Voice from the Outer World") is a messianic figure in Frank Herbert's Dune universe — the prophesied leader who would deliver the Fremen from oppression. In the story, the Lisan al-Gaib defeats Shai-Hulud, the colossal sandworms that dominate the deserts of Arrakis.

The name is fitting for this action because Shai-Hulud is also the name given to a series of devastating npm supply chain attacks that began in September 2025. The Shai-Hulud worm compromised over 500 npm packages — collectively downloaded 132 million times per month — by hijacking maintainer credentials and automatically republishing all of a victim's packages with malicious payloads. Like its namesake sandworm, the malware burrowed through the ecosystem, using TruffleHog to scan for secrets (GitHub tokens, AWS/GCP/Azure keys) and self-replicating across every package owned by a compromised maintainer. A second wave in November 2025 impacted tens of thousands of GitHub repositories.

Just as the Lisan al-Gaib tamed the sandworms, this action guards your repository against the supply chain threats that Shai-Hulud exploited — by ensuring that newly published packages have had time to be vetted by the community before they enter your dependency tree.

About

checks to make sure that any new dependencies have baked for at least two weeks.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors