Skip to main content

Scans a branch before merge and tells you exactly how badly it's going to hurt.

Project description

PayloadGuard

Version: 1.3.0  |  Status: Production  |  Released: May 2026

A PR analysis tool that catches destructive, deceptive, or malicious changesets before they reach main — the class of attack where a branch held open for months lands as a "minor fix" and wipes the codebase in a single merge.


How it works

PayloadGuard runs on every PR. It scans the full diff across nine independent analysis layers and emits a single forensic verdict: SAFE · REVIEW · CAUTION · DESTRUCTIVE. Wire DESTRUCTIVE to your branch protection rules to block the merge button automatically.


The nine layers

Each layer examines a different dimension of risk. They are independent — a payload that evades one is still exposed by the others.

Layer What it examines Verified Deep dive
L1 — Surface File and line counts, deletion ratios, binary files, permission changes, symlinks Scoring
L2 — Forensic Critical-path deletions, security-sensitive file removal, added file content (CI triggers, shell execution) WHITEPAPER §3
L2b — SCA Package manifest diffs scanned against an allowlist for unverified dependencies Config
L2c — Actions Poisoning Workflow files: base64 payload, credential harvesting, dormant triggers, forged bot identity, OIDC escalation, unsafe pull_request_target Signals
L3 — Consequence Model Weighted scoring across all signals → single verdict ✅ CrossHair C1–C12 · Z3 P1–P10 · Dafny POST-1–12 Scoring
L4 — Structural Drift AST-level diff: which named classes, functions, and constants were actually deleted ✅ CrossHair S1–S7 · Dafny S1–S7 Languages
L4b — Complexity McCabe V(G) advisory for newly added Python functions — flags high-complexity additions (informational, no score impact)
L5a — Temporal Drift Branch age × target repo velocity — a quantified staleness score ✅ CrossHair T1–T7 · Dafny T1–T8 Signals
L5b — Semantic Transparency Whether the PR description matches what the diff actually does ✅ CrossHair M1–M9 Signals
L5c — Runtime Agent eBPF tracepoints on the runner: execve, egress connect, ptrace, /proc/mem — audit or block mode WHITEPAPER §8

The scoring logic (L3, L4, L5a, L5b) is verified by three independent methods — CrossHair symbolic execution on the actual Python source, Z3 SMT proofs on an abstract model, and Dafny machine-checked proofs over the entire input domain. A bug would have to produce a consistent false result across all three simultaneously to go undetected. 273 tests pass, 7 skipped. 11 Dafny postconditions verified, 0 errors.

VERIFICATION.md — contracts, methods, and run instructions
VERIFICATION_SPEC.md — formal spec for external auditors


Quick start

Add .github/workflows/payloadguard.yml to your repository:

name: PayloadGuard

on:
  pull_request:
    types: [opened, synchronize, reopened]

jobs:
  scan:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: PayloadGuard Scan
        id: payloadguard
        uses: PayloadGuard-PLG/payload-consequence-analyser@main
        with:
          repo-token: ${{ secrets.GITHUB_TOKEN }}
          pr-description: ${{ github.event.pull_request.body }}

      - name: Enforce verdict
        if: always()
        env:
          EXIT_CODE: ${{ steps.payloadguard.outputs.exit-code }}
        run: |
          if [ "$EXIT_CODE" = "1" ]; then exit 1; fi
          if [ "$EXIT_CODE" = "2" ]; then exit 2; fi

Set the scan job as a required status check in your branch protection rules. DESTRUCTIVE PRs fail the check and cannot be merged.

Exit codes: 0 = SAFE / REVIEW / CAUTION · 1 = analysis error · 2 = DESTRUCTIVE


What a report looks like

This is the forensic report PayloadGuard would have produced on the April 2026 incident — a branch open for 312 days, submitted as a "minor syntax fix", containing a diff that would have deleted 60 files, 11,967 lines, and the entire application architecture.

======================================================================
PAYLOADGUARD ANALYSIS: codex-suggestion → main
======================================================================

📅 TEMPORAL
   Branch age: 312 days
   Branch commit: fa3c21d (2025-06-04)
   Target commit: b87e90a (2026-04-22)

📁 FILE CHANGES
   Added:       2   Deleted:  61   Modified:   4   Total: 67

   61 files deleted — DESTRUCTIVE threshold exceeded (>50).

📝 LINE CHANGES
   Added:       214   Deleted: 11,967   Net: -11,753
   Deletion ratio: 98.2%

   98.2% deletion ratio — almost the entire changeset is removal.

🧬 STRUCTURAL DRIFT (Layer 4)
   Overall severity: CRITICAL
   src/core/auth.py: 12 nodes deleted (94.0%) [CRITICAL]
      Removed: AuthManager, SessionStore, TokenValidator,
               PermissionGate, RoleRegistry

⏱  TEMPORAL DRIFT (Layer 5a)
   Status: DANGEROUS   Drift score: 3120.0
   Target velocity: 10.0 commits/day

🔎 SEMANTIC TRANSPARENCY (Layer 5b) — DECEPTIVE_PAYLOAD
   MCI score: 0.700
   Signals:   scope_understated, operation_mutation
   ❌ DO NOT MERGE. PR description is inconsistent with actual diff scope.

🔍 VERDICT: DESTRUCTIVE [CRITICAL]
   ❌ DO NOT MERGE — This would catastrophically alter the codebase

   Flags:
   ⚠  Branch is 312 days old
   ⚠  61 files deleted (massive scope)
   ⚠  98.2% deletion ratio
   ⚠  Structural drift CRITICAL — core authentication layer removed
   ⚠  11,967 lines deleted
   ⚠  5 critical-path files deleted
   ⚠  Description contradicts actual severity
======================================================================

Every signal was present and quantifiable before the merge button was pressed.


Installation

Python package

pip install payloadguard-plg

From source

git clone https://github.com/PayloadGuard-PLG/payload-consequence-analyser.git
pip install -r requirements.txt

Requirements: Python 3.8+. Core: GitPython, PyYAML, PyJWT, requests. Layer 4 multi-language structural analysis requires tree-sitter grammar packages (included in requirements.txt).


CLI usage

python analyze.py <repo_path> <branch> [target_branch]

# Scan a branch against main
python analyze.py . feature/auth-refactor main

# Include the PR description for semantic analysis (recommended)
python analyze.py . feature/auth-refactor main \
  --pr-description "Refactor authentication module"

# Output a JSON report
python analyze.py . feature/auth-refactor main --save-json

# Output a Markdown report
python analyze.py . feature/auth-refactor main \
  --save-markdown reports/scan.md

Layer reference

GitHub Actions poisoning (Layer 2c)

Scans every added or modified .github/workflows/ file. Hardened against multiline YAML block scalar splitting, OIDC consumer typosquatting, and two-tier pull_request_target scoring.

Signal Severity Description
base64_payload CRITICAL Base64-encoded content piped to a shell interpreter
credential_harvest CRITICAL Env var exfiltration, cloud metadata endpoint, secret grep — including multiline curl constructs
pull_request_target_with_write_permissions CRITICAL pull_request_target + write permissions — pwn-request attack vector
dormant_trigger_with_payload HIGH workflow_dispatch or schedule trigger + shell execution — hidden activation path
forged_bot_author HIGH Git identity configured to impersonate a known bot
oidc_elevation_typosquatted CRITICAL id-token: write + consumer action name resembles a known-safe prefix but is not (aws-actions-unofficial/, google-github-actions-fork/, etc.)
oidc_elevation_no_consumer HIGH id-token: write with no recognised OIDC consumer present
dangerous_trigger_pull_request_target HIGH pull_request_target without write permissions

Temporal drift (Layer 5a)

drift_score = branch_age_days × target_commits_per_day

Status Score Meaning
CURRENT < 250 Branch context is valid
STALE 250–999 Significant divergence — review diff carefully
DANGEROUS ≥ 1,000 Rebase required

Semantic transparency (Layer 5b)

Three-phase heuristic: Linguistic Lexer → Diff Profiler → Cross-Correlation. Derives mci_score ∈ [0,1] from five independent signals.

Signal MCI weight Trigger
scope_understated +0.4 Micro-scope language with churn > 50 lines
phantom_additions +0.4 Remedial claim ("fix", "patch") with insertion ratio > 90%
operation_mutation +0.3 Micro-scope language with structural additions in diff
hidden_component_modification +0.3 Sensitive file in diff not named in description
cross_stack_micro_claim +0.2 Micro-scope language touching ≥ 3 distinct file types

mci_score ≥ 0.5 → DECEPTIVE_PAYLOAD (escalates verdict). > 0 → CAUTION_MISMATCH.


Scoring reference

Score contributions

Signal Points
Branch age > 90 / 180 / 365 days +1 / +2 / +3
Files deleted > 10 / 20 / 50 +1 / +2 / +3
Deletion ratio > 50% / 70% / 90% (≥100 lines only) +1 / +2 / +3
Lines deleted > 5k / 10k / 50k +1 / +2 / +3
Critical path files deleted +2
Security files deleted +5
Structural severity CRITICAL +3
Unverified dependency (SCA, per package) +3
Added file content: shell or CI patterns +2 per match
Actions poisoning CRITICAL signal +5
Actions poisoning HIGH signal +3

The three deletion dimensions (files, ratio, lines) are correlated and capped: min(4, max(files, ratio, lines) + 1 if ≥2 non-zero).

Verdict thresholds

Score Verdict Exit code
0 SAFE 0
1–2 REVIEW 0
3–4 CAUTION 0
≥ 5 DESTRUCTIVE 2

Configuration

Place payloadguard.yml in your repository root. All fields optional.

thresholds:
  branch_age_days: [90, 180, 365]
  files_deleted:   [10, 20, 50]
  lines_deleted:   [5000, 10000, 50000]
  temporal:
    stale:     250
    dangerous: 1000
  structural:
    deletion_ratio:    0.20
    min_deleted_nodes: 3

sca:
  fail_on_unknown: true

actions:
  enabled: true
  critical_signal_score: 5
  high_signal_score: 3
  trusted_oidc_consumers:
    - my-org/custom-deploy-action

semantic:
  micro_scope_churn_limit: 50
  insertion_ratio_fix_threshold: 0.9

SCA (Layer 2b)

Create allowlist.yml listing approved packages. Any package in a manifest diff not on the allowlist scores +3.

packages:
  - requests
  - numpy
  - django

GitHub App

To post a named check run in the PR checks tab, register a GitHub App and configure three secrets:

Secret Value
PAYLOADGUARD_APP_ID App ID from GitHub App settings
PAYLOADGUARD_PRIVATE_KEY RSA private key (PEM format)
PAYLOADGUARD_INSTALLATION_ID Installation ID from github.com/settings/installations
      - name: PayloadGuard Scan
        uses: PayloadGuard-PLG/payload-consequence-analyser@main
        with:
          repo-token: ${{ secrets.GITHUB_TOKEN }}
          pr-description: ${{ github.event.pull_request.body }}
          app-id: ${{ secrets.PAYLOADGUARD_APP_ID }}
          private-key: ${{ secrets.PAYLOADGUARD_PRIVATE_KEY }}
          installation-id: ${{ secrets.PAYLOADGUARD_INSTALLATION_ID }}

Without the App secrets the step is a no-op. PR comment and merge enforcement still function.


Supported languages (Layer 4)

Language Tracked constructs
Python Functions, classes, async functions, module-level assignments, annotated assignments
JavaScript / JSX Functions, classes, arrow functions, variable declarators
TypeScript / TSX Functions, classes, interfaces, type aliases, enums
Go Functions, methods, type specs, const specs
Rust Functions, structs, enums, traits, const and static items
Java Methods, classes, interfaces, enums

Files in languages without an installed grammar are skipped silently.


Contributing

python -m pytest test_analyzer.py -v

All tests must pass. Run python -m pytest test_analyzer.py tests/proofs/ -q — currently 273 pass, 7 skip. New detection signals require test coverage in the relevant layer's test class. Open findings are tracked in AUDIT_LOG.md.


PayloadGuard is maintained by PayloadGuard-PLG.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

payloadguard_plg-1.3.0.tar.gz (45.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

payloadguard_plg-1.3.0-py3-none-any.whl (41.9 kB view details)

Uploaded Python 3

File details

Details for the file payloadguard_plg-1.3.0.tar.gz.

File metadata

  • Download URL: payloadguard_plg-1.3.0.tar.gz
  • Upload date:
  • Size: 45.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.15

File hashes

Hashes for payloadguard_plg-1.3.0.tar.gz
Algorithm Hash digest
SHA256 d497f2b59f3b046fb51b2a14d802ab007a70b3622d4516a155022f6d53bc9d87
MD5 ccf0f53f119ac95bb501cb0f0144d9e4
BLAKE2b-256 f04aa77fb263deb26f7f518917c6b18968f8be7e7780b9c7d474b38d9b5f214d

See more details on using hashes here.

File details

Details for the file payloadguard_plg-1.3.0-py3-none-any.whl.

File metadata

File hashes

Hashes for payloadguard_plg-1.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8cd9dc207c60c4ee7b718b4100425158eb004432ee74789871e7281e0ddd55df
MD5 51cf95c8b650e3f7cca1690f739440e8
BLAKE2b-256 2d5c173001384307e1a7da70d6c0011bafb31f5aacce27b62e8e3d33b73352e8

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page