mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
Swap all `runs-on: ubuntu-latest` to `runs-on: arc-runner-set` all jobs.
The ARM docker build job in docker.yml uses `${{ matrix.runner }}`
and is left untouched since the GKE runner pool is x86_64 only.
Runners are backed by ARC (Actions Runner Controller) on a GKE cluster
with a spot preemptible node pool that scales based on job demand.
Use the baked Electron dependencies for the desktop E2E job.
293 lines
11 KiB
YAML
293 lines
11 KiB
YAML
name: Supply Chain Audit
|
|
|
|
# Narrow, high-signal scanner. Only fires on critical indicators of supply
|
|
# chain attacks (e.g. the litellm-style payloads). Low-signal heuristics
|
|
# (plain base64, plain exec/eval, dependency/Dockerfile/workflow edits,
|
|
# Actions version unpinning, outbound POST/PUT) were intentionally
|
|
# removed — they fired on nearly every PR and trained reviewers to ignore
|
|
# the scanner. Keep this file's checks ruthlessly narrow: if you find
|
|
# yourself adding WARNING-tier patterns here again, make a separate
|
|
# advisory-only workflow instead.
|
|
#
|
|
# Path-gating is handled centrally by the ``ci.yml`` orchestrator's
|
|
# ``detect`` job. The orchestrator passes ``scan`` / ``deps`` booleans as
|
|
# inputs; this workflow's jobs gate on those inputs instead of re-computing
|
|
# the diff. MCP catalog review was previously here but has moved to
|
|
# ``review-labels.yml`` so it can be rerun independently.
|
|
#
|
|
# Outputs:
|
|
# review_status — JSON array of status objects consumed by the review
|
|
# comment assembler (scripts/ci/assemble_review_comment.py).
|
|
# critical_findings — "true" when the narrow critical-pattern scan found
|
|
# something. The review-label gate consumes this and
|
|
# owns the action-required result, so adding
|
|
# ``ci-reviewed`` can heal the run on rerun.
|
|
|
|
on:
|
|
workflow_call:
|
|
inputs:
|
|
event_name:
|
|
description: The event name from the calling orchestrator.
|
|
type: string
|
|
required: true
|
|
scan:
|
|
description: Whether supply-chain-relevant files changed.
|
|
type: boolean
|
|
required: true
|
|
deps:
|
|
description: Whether pyproject.toml changed.
|
|
type: boolean
|
|
required: true
|
|
outputs:
|
|
review_status:
|
|
description: JSON array of review status objects for the review comment assembler.
|
|
value: ${{ jobs.aggregate.outputs.review_status }}
|
|
critical_findings:
|
|
description: Whether the critical-pattern scan found a risk requiring maintainer review.
|
|
value: ${{ jobs.aggregate.outputs.critical_findings }}
|
|
|
|
permissions:
|
|
pull-requests: write
|
|
contents: read
|
|
|
|
jobs:
|
|
scan:
|
|
name: Scan PR for critical supply chain risks
|
|
if: inputs.scan
|
|
runs-on: arc-runner-set
|
|
timeout-minutes: 15
|
|
outputs:
|
|
review_status: ${{ steps.emit-status.outputs.review_status }}
|
|
critical_findings: ${{ steps.scan.outputs.found }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Scan diff for critical patterns
|
|
id: scan
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
CI_REVIEWED: ${{ contains(github.event.pull_request.labels.*.name, 'ci-reviewed') }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
BASE="${{ github.event.pull_request.base.sha }}"
|
|
HEAD="${{ github.event.pull_request.head.sha }}"
|
|
|
|
# Added lines only, excluding lockfiles.
|
|
# Three-point diff (base...head) diffs from the merge base to HEAD,
|
|
# so only changes introduced by this PR are included — not changes
|
|
# that landed on main after the PR branched off.
|
|
DIFF=$(git diff "$BASE"..."$HEAD" -- . ':!uv.lock' ':!*.lock' ':!package-lock.json' ':!yarn.lock' || true)
|
|
|
|
FINDINGS=""
|
|
|
|
# --- .pth files (auto-execute on Python startup) ---
|
|
# The exact mechanism used in the litellm supply chain attack:
|
|
# https://github.com/BerriAI/litellm/issues/24512
|
|
PTH_FILES=$(git diff --diff-filter=d --name-only "$BASE"..."$HEAD" | grep '\.pth$' || true)
|
|
if [ -n "$PTH_FILES" ]; then
|
|
FINDINGS="${FINDINGS}
|
|
### 🚨 CRITICAL: .pth file added or modified
|
|
Python \`.pth\` files in \`site-packages/\` execute automatically when the interpreter starts — no import required.
|
|
|
|
**Files:**
|
|
\`\`\`
|
|
${PTH_FILES}
|
|
\`\`\`
|
|
"
|
|
fi
|
|
|
|
# --- base64 decode + exec/eval on the same line (the litellm attack pattern) ---
|
|
B64_EXEC_HITS=$(echo "$DIFF" | grep -n '^+' | grep -iE 'base64\.(b64decode|decodebytes|urlsafe_b64decode)' | grep -iE 'exec\(|eval\(' | head -10 || true)
|
|
if [ -n "$B64_EXEC_HITS" ]; then
|
|
FINDINGS="${FINDINGS}
|
|
### 🚨 CRITICAL: base64 decode + exec/eval combo
|
|
Base64-decoded strings passed directly to exec/eval — the signature of hidden credential-stealing payloads.
|
|
|
|
**Matches:**
|
|
\`\`\`
|
|
${B64_EXEC_HITS}
|
|
\`\`\`
|
|
"
|
|
fi
|
|
|
|
# --- subprocess with encoded/obfuscated command argument ---
|
|
PROC_HITS=$(echo "$DIFF" | grep -n '^+' | grep -E 'subprocess\.(Popen|call|run)\s*\(' | grep -iE 'base64|\\x[0-9a-f]{2}|chr\(' | head -10 || true)
|
|
if [ -n "$PROC_HITS" ]; then
|
|
FINDINGS="${FINDINGS}
|
|
### 🚨 CRITICAL: subprocess with encoded/obfuscated command
|
|
Subprocess calls whose command strings are base64- or hex-encoded are a strong indicator of payload execution.
|
|
|
|
**Matches:**
|
|
\`\`\`
|
|
${PROC_HITS}
|
|
\`\`\`
|
|
"
|
|
fi
|
|
|
|
# --- Install-hook files (setup.py/sitecustomize/usercustomize/__init__.pth) ---
|
|
# These execute during pip install or interpreter startup.
|
|
# Anchored at repo root: only the top-level setup.py/setup.cfg run during
|
|
# `pip install`, and only top-level sitecustomize.py/usercustomize.py are
|
|
# auto-loaded by the interpreter via site.py. Any nested file with the
|
|
# same name (e.g. hermes_cli/setup.py — the CLI setup wizard) is unrelated
|
|
# and produced false positives that trained reviewers to ignore the scanner.
|
|
SETUP_HITS=$(git diff --diff-filter=d --name-only "$BASE"..."$HEAD" | grep -E '^(setup\.py|setup\.cfg|sitecustomize\.py|usercustomize\.py|__init__\.pth)$' || true)
|
|
# A maintainer-applied ci-reviewed label records the manual review
|
|
# required for intentional changes to an install hook. The scanner
|
|
# still blocks every unreviewed addition or modification.
|
|
if [ -n "$SETUP_HITS" ] && [ "$CI_REVIEWED" != "true" ]; then
|
|
FINDINGS="${FINDINGS}
|
|
### 🚨 CRITICAL: Install-hook file added or modified
|
|
These files can execute code during package installation or interpreter startup.
|
|
|
|
**Files:**
|
|
\`\`\`
|
|
${SETUP_HITS}
|
|
\`\`\`
|
|
"
|
|
fi
|
|
|
|
if [ -n "$FINDINGS" ]; then
|
|
echo "found=true" >> "$GITHUB_OUTPUT"
|
|
echo "$FINDINGS" > /tmp/findings.md
|
|
else
|
|
echo "found=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Emit review_status
|
|
id: emit-status
|
|
if: always()
|
|
env:
|
|
FOUND: ${{ steps.scan.outputs.found }}
|
|
run: |
|
|
python3 - <<'PYEOF'
|
|
import json, os
|
|
|
|
# The review-label gate renders and blocks critical findings. Keep
|
|
# this scan a fact-finder so adding ci-reviewed can rerun the gate
|
|
# without requiring the scanner itself to fail again.
|
|
status = []
|
|
|
|
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as f:
|
|
f.write(f"review_status={json.dumps(status)}\n")
|
|
PYEOF
|
|
|
|
|
|
dep-bounds:
|
|
name: Check PyPI dependency upper bounds
|
|
if: inputs.deps
|
|
runs-on: arc-runner-set
|
|
timeout-minutes: 15
|
|
outputs:
|
|
review_status: ${{ steps.emit-status.outputs.review_status }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Check for unbounded PyPI deps
|
|
id: bounds
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
BASE="${{ github.event.pull_request.base.sha }}"
|
|
HEAD="${{ github.event.pull_request.head.sha }}"
|
|
|
|
# Only check added lines in pyproject.toml
|
|
ADDED=$(git diff "$BASE"..."$HEAD" -- pyproject.toml | grep '^+' | grep -v '^+++' || true)
|
|
|
|
if [ -z "$ADDED" ]; then
|
|
echo "found=false" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
|
|
# Match PyPI dep specs that have >= and no < ceiling.
|
|
# Pattern: "package>=version" without a following ",<" bound.
|
|
# Excludes git+ URLs (which use commit SHAs) and comments.
|
|
UNBOUNDED=$(echo "$ADDED" | grep -oE '"[a-zA-Z0-9_-]+(\[[^\]]*\])?>=[ 0-9.]+"' | grep -v ',<' || true)
|
|
|
|
if [ -n "$UNBOUNDED" ]; then
|
|
echo "found=true" >> "$GITHUB_OUTPUT"
|
|
echo "$UNBOUNDED" > /tmp/unbounded.txt
|
|
else
|
|
echo "found=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Emit review_status
|
|
id: emit-status
|
|
if: always()
|
|
env:
|
|
FOUND: ${{ steps.bounds.outputs.found }}
|
|
run: |
|
|
python3 - <<'PYEOF'
|
|
import json, os
|
|
|
|
found = os.environ.get("FOUND", "") == "true"
|
|
|
|
if found:
|
|
with open("/tmp/unbounded.txt", encoding="utf-8") as f:
|
|
detail = f.read()
|
|
status = [{
|
|
"source": "supply chain",
|
|
"results": [{
|
|
"kind": "action_required",
|
|
"title": "Unbounded PyPI dependencies",
|
|
"summary": "This PR adds PyPI dependencies without upper bounds.",
|
|
"detail": detail,
|
|
"how_to_fix": 'Add a `<next_major` upper bound, e.g. `"package>=1.2.0,<2"`. See CONTRIBUTING.md dependency pinning policy.'
|
|
}]
|
|
}]
|
|
else:
|
|
status = []
|
|
|
|
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as f:
|
|
f.write(f"review_status={json.dumps(status)}\n")
|
|
PYEOF
|
|
|
|
- name: Fail on unbounded deps
|
|
if: steps.bounds.outputs.found == 'true'
|
|
run: |
|
|
echo "::error::PyPI dependencies without upper bounds detected. Add <next_major ceiling per CONTRIBUTING.md policy."
|
|
exit 1
|
|
|
|
aggregate:
|
|
name: Aggregate review statuses
|
|
needs: [scan, dep-bounds]
|
|
if: always()
|
|
runs-on: arc-runner-set
|
|
timeout-minutes: 15
|
|
outputs:
|
|
review_status: ${{ steps.merge.outputs.review_status }}
|
|
critical_findings: ${{ steps.merge.outputs.critical_findings }}
|
|
steps:
|
|
- name: Merge review statuses
|
|
id: merge
|
|
env:
|
|
SCAN_STATUS: ${{ needs.scan.outputs.review_status }}
|
|
DEP_STATUS: ${{ needs.dep-bounds.outputs.review_status }}
|
|
CRITICAL_FINDINGS: ${{ needs.scan.outputs.critical_findings }}
|
|
run: |
|
|
python3 - <<'PYEOF'
|
|
import json, os
|
|
|
|
merged = []
|
|
for key in ("SCAN_STATUS", "DEP_STATUS"):
|
|
raw = os.environ.get(key, "")
|
|
if not raw:
|
|
continue
|
|
try:
|
|
data = json.loads(raw)
|
|
except (json.JSONDecodeError, TypeError):
|
|
continue
|
|
if isinstance(data, list):
|
|
merged.extend(data)
|
|
|
|
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as f:
|
|
f.write(f"review_status={json.dumps(merged)}\n")
|
|
f.write("critical_findings=" + os.environ.get("CRITICAL_FINDINGS", "false") + "\n")
|
|
PYEOF
|