GitHub - marshal-hq/marshal: Behavioral security monitoring for JVM dependencies. Catches malicious package updates before CVEs exist

6 min read Original article ↗

Behavioral supply-chain security for JVM dependencies.

Marshal watches how packages change on Maven Central and scores every update on a 0–100 risk scale. A maintainer swap, a dropped GPG signature, a sudden jump in dependency count: these show up the day a version is published, long before a CVE exists. Risky updates fail your PR check with a clear reason. It's built for Java teams on Maven or Gradle that auto-merge dependency updates and want a way to catch the bad ones before they hit the build.

PR comment showing Marshal findings

Marshal demo

javax.activation:activation scores ORANGE 55/100: GPG signature dropped between versions.

Quick start

Requires Java 21 or later to run the CLI. The GitHub Action bundles its own runtime, so there's nothing to install on the Action path.

Add this to your repo at .github/workflows/marshal.yml:

name: Marshal

on:
  pull_request:
    paths: ['**/pom.xml', '**/build.gradle', '**/build.gradle.kts']

jobs:
  marshal:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0   # required: the PR base is built and diffed
      # Gradle projects only: provide the build toolchain for ./gradlew:
      # - uses: actions/setup-java@v4
      #   with: { distribution: temurin, java-version: '17' }
      - name: Marshal scan
        uses: marshal-hq/marshal/marshal-action@v0.1.0
        with:
          path: '.'        # a build file or project dir (Maven or Gradle)
          threshold: red
          fail-on: fail

Or run the CLI directly:

# Download
curl -fsSL https://github.com/marshal-hq/marshal/releases/download/v0.1.0/marshal-cli-0.1.0.jar \
  -o marshal.jar

# Scan a Maven project
java -jar marshal.jar scan --pom pom.xml

# Scan a Gradle project (Marshal runs your ./gradlew)
java -jar marshal.jar scan --build-file build.gradle.kts

Using Marshal

On a pull request (recommended). Add the workflow above. On every PR that touches a pom.xml or build.gradle(.kts), Marshal detects the build tool, scans the dependency changes, and posts a comment with any findings. Set fail-on: fail with threshold: red to block merges on critical findings, or set fail-on: never to comment without blocking. Safe updates pass silently.

A failing check flags a risky update, but on its own it will not stop a merge. GitHub only enforces a check once you require it. To make Marshal blocking, turn on "Require status checks to pass" in your branch protection rules and select the Marshal check.

If Marshal cannot resolve your dependencies, for example because the build fails, the check fails rather than passing silently. A scanner that cannot analyze your project should not report it as clean.

Locally, before you commit. Scan any project from the command line. Point Marshal at a Maven pom or a Gradle build file:

java -jar marshal.jar scan --pom pom.xml             # Maven
java -jar marshal.jar scan --build-file build.gradle.kts   # Gradle

marshal diff compares two states and reports only the new risk, which is what the PR check runs. It works the same way for both build tools:

java -jar marshal.jar diff --base old/build.gradle.kts --head build.gradle.kts

Pick an output format depending on where the result goes:

java -jar marshal.jar scan --pom pom.xml                 # human-readable terminal output
java -jar marshal.jar scan --pom pom.xml --output json   # for CI and scripting
java -jar marshal.jar scan --pom pom.xml --output md      # markdown, e.g. for PR comments

Marshal exits with a non-zero code when a finding reaches your fail-on threshold, so it drops into any CI pipeline, not just GitHub Actions.

Build tool support

Marshal supports JVM projects built with Maven or Gradle: Spring Boot, Spring, and plain JVM libraries. Android (AGP) and Kotlin Multiplatform are not supported in v1, because they use build configurations Marshal does not yet read.

For Gradle, Marshal runs your project's Gradle wrapper, so it works with the Gradle version your project already uses. The resolver is verified against Gradle 8.5 and later. Gradle 7.x is not yet covered by our tests.

What it catches

Signal What it means
Signature dropped Package was GPG-signed in prior releases, now it's not
Missing signature This release has no GPG signature
New maintainer Different signing key or publisher account from prior version
Dependency explosion Dependency count grew more than 3× in one release
Major version jump Version jumped by more than 2 major versions
Repo URL changed Source repository URL is different from prior version
Yanked version Package was yanked or retracted after publication

Each update gets a 0–100 risk score. Risky findings show up as a PR comment with the evidence and a recommendation.

Real-world coverage

Five historical supply-chain attacks are in the test suite as replay fixtures, each reconstructed from metadata that was visible at the time:

event-stream (npm, 2018): A new maintainer published a version with a malicious dependency (flatmap-stream) and no GPG signature. The NEW_MAINTAINER and DEPENDENCY_EXPLOSION signals fire, scoring ORANGE at 60 points.

ua-parser-js (npm, 2021): Account takeover. The attacker published from a different key. NEW_MAINTAINER and SIGNATURE_DROPPED fire, scoring RED.

node-ipc protestware (npm, 2022): The legitimate maintainer added a destructive payload, dependency count ballooned, and the version was later yanked. SIGNATURE_DROPPED, DEPENDENCY_EXPLOSION, and YANKED_VERSION fire.

PyTorch-nightly dependency confusion (2022): A malicious package was published to PyPI before the legitimate one. First publish with no signature and later yanked: MISSING_SIGNATURE and YANKED_VERSION score YELLOW.

XZ Utils (2024): Slow social engineering over two years. Marshal would have caught the initial maintainer handoff (NEW_MAINTAINER, partial signal), but the attacker spent months earning trust before introducing the backdoor, which is exactly the kind of thing static behavioral signals miss. Worth being honest about: this is a limit of the approach, not a case the tool handles.

How it works

  1. WATCH: Marshal fetches version history and metadata for every dependency in your project from Maven Central.
  2. ANALYZE: Each new version is scored against 7 behavioral signals: maintainer changes, signature drops, dependency explosions, and more.
  3. BLOCK: Risky updates fail your PR check with a clear reason. Safe updates pass silently.

What Marshal is not

Marshal is not a CVE scanner. It doesn't look up known vulnerabilities, and it isn't trying to. Snyk, Dependabot, and OWASP Dependency-Check already handle that. What they can't see is the malicious update that nobody has reported yet, because there's no CVE on day zero. That's the gap Marshal fills. Keep your CVE scanner. Add Marshal next to it.

Configuration

Place marshal.yml at your project root to customize behavior:

rules:
  disabled: []
  overrides: {}

thresholds:
  fail-on: red      # red | orange | yellow
  warn-on: orange

allowlist:
  packages: []
    # - "org.springframework:*"

notifications:
  slack:
    webhook: ${MARSHAL_SLACK_WEBHOOK}
    min-level: red

Full reference: examples/marshal.yml. Rules can be disabled individually. Allowlisted packages are skipped entirely. Slack alerts fire when findings reach or exceed min-level.

Status

Marshal is in early development (v0.1.0). The detection engine covers Maven Central with 7 behavioral rules. Maven and Gradle are both supported now, with npm and PyPI after that.

What works:

  • CLI scanning of Maven (pom.xml) and Gradle (build.gradle, build.gradle.kts) projects
  • GitHub Action with PR comments and build tool auto-detection
  • Risk scoring with configurable thresholds
  • Slack alerts on critical findings

What's next:

  • Transitive dependency resolution (currently direct deps only)
  • npm ecosystem support
  • PyPI ecosystem support
  • Hosted continuous watching (SaaS)

License

Apache 2.0. See LICENSE.

Links