文件内容
HEARTBEAT.md
# ClawSec Suite — Heartbeat Check
Run this periodically (cron/systemd/CI/agent scheduler). It assumes POSIX shell, `curl`, and `jq`.
## Goals
1. Check whether `clawsec-suite` has an update available.
2. Poll the advisory feed.
3. Report new advisories, highlight affected installed skills, and require approval before removal actions.
---
## Configuration
```bash
INSTALL_ROOT="${INSTALL_ROOT:-$HOME/.openclaw/skills}"
SUITE_DIR="$INSTALL_ROOT/clawsec-suite"
GITHUB_RELEASES_API="${GITHUB_RELEASES_API:-https://api.github.com/repos/prompt-security/clawsec/releases?per_page=100}"
RELEASE_DOWNLOAD_BASE_URL="${RELEASE_DOWNLOAD_BASE_URL:-https://github.com/prompt-security/clawsec/releases/download}"
FEED_URL="${CLAWSEC_FEED_URL:-https://clawsec.prompt.security/advisories/feed.json}"
STATE_FILE="${CLAWSEC_SUITE_STATE_FILE:-$HOME/.openclaw/clawsec-suite-feed-state.json}"
MIN_FEED_INTERVAL_SECONDS="${MIN_FEED_INTERVAL_SECONDS:-300}"
```
---
## Step 0 — Basic sanity
```bash
set -euo pipefail
test -d "$SUITE_DIR"
test -f "$SUITE_DIR/skill.json"
echo "=== ClawSec Suite Heartbeat ==="
echo "When: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "Suite: $SUITE_DIR"
```
---
## Step 1 — Check suite version updates
```bash
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
INSTALLED_VER="$(jq -r '.version // ""' "$SUITE_DIR/skill.json" 2>/dev/null || true)"
LATEST_TAG=""
LATEST_VER=""
if curl -fsSLo "$TMP/releases.json" "$GITHUB_RELEASES_API"; then
LATEST_TAG="$(jq -r '[.[] | select(.tag_name | startswith("clawsec-suite-v"))][0].tag_name // ""' "$TMP/releases.json" 2>/dev/null || true)"
fi
if [ -n "$LATEST_TAG" ]; then
if curl -fsSLo "$TMP/remote-skill.json" "$RELEASE_DOWNLOAD_BASE_URL/$LATEST_TAG/skill.json"; then
LATEST_VER="$(jq -r '.version // ""' "$TMP/remote-skill.json" 2>/dev/null || true)"
fi
fi
echo "Installed suite: ${INSTALLED_VER:-unknown}"
echo "Latest suite: ${LATEST_VER:-unknown}"
if [ -z "$LATEST_VER" ]; then
echo "WARNING: Could not determine latest suite version from release metadata."
elif [ "$LATEST_VER" != "$INSTALLED_VER" ]; then
echo "UPDATE AVAILABLE: clawsec-suite ${INSTALLED_VER:-unknown} -> $LATEST_VER"
else
echo "Suite appears up to date."
fi
```
---
## Step 2 — Initialize advisory state
```bash
mkdir -p "$(dirname "$STATE_FILE")"
if [ ! -f "$STATE_FILE" ]; then
echo '{"schema_version":"1.0","known_advisories":[],"last_feed_check":null,"last_feed_updated":null}' > "$STATE_FILE"
chmod 600 "$STATE_FILE"
fi
if ! jq -e '.schema_version and .known_advisories' "$STATE_FILE" >/dev/null 2>&1; then
echo "WARNING: Invalid state file, resetting: $STATE_FILE"
cp "$STATE_FILE" "${STATE_FILE}.bak.$(date -u +%Y%m%d%H%M%S)" 2>/dev/null || true
echo '{"schema_version":"1.0","known_advisories":[],"last_feed_check":null,"last_feed_updated":null}' > "$STATE_FILE"
chmod 600 "$STATE_FILE"
fi
```
---
## Step 3 — Advisory feed check (embedded clawsec-feed)
```bash
now_epoch="$(date -u +%s)"
last_check="$(jq -r '.last_feed_check // "1970-01-01T00:00:00Z"' "$STATE_FILE")"
last_epoch="$(date -u -d "$last_check" +%s 2>/dev/null || date -u -j -f "%Y-%m-%dT%H:%M:%SZ" "$last_check" +%s 2>/dev/null || echo 0)"
if [ $((now_epoch - last_epoch)) -lt "$MIN_FEED_INTERVAL_SECONDS" ]; then
echo "Feed check skipped (rate limit: ${MIN_FEED_INTERVAL_SECONDS}s)."
else
FEED_TMP="$TMP/feed.json"
FEED_SOURCE="$FEED_URL"
if ! curl -fsSLo "$FEED_TMP" "$FEED_URL"; then
if [ -f "$SUITE_DIR/advisories/feed.json" ]; then
cp "$SUITE_DIR/advisories/feed.json" "$FEED_TMP"
FEED_SOURCE="$SUITE_DIR/advisories/feed.json (local fallback)"
echo "WARNING: Remote feed unavailable, using local fallback."
else
echo "ERROR: Remote feed unavailable and no local fallback feed found."
exit 1
fi
fi
if ! jq -e '.version and (.advisories | type == "array")' "$FEED_TMP" >/dev/null 2>&1; then
echo "ERROR: Advisory feed has invalid format."
exit 1
fi
echo "Feed source: $FEED_SOURCE"
echo "Feed updated: $(jq -r '.updated // "unknown"' "$FEED_TMP")"
NEW_IDS_FILE="$TMP/new_ids.txt"
jq -r --argfile state "$STATE_FILE" '($state.known_advisories // []) as $known | [.advisories[]?.id | select(. != null and ($known | index(.) | not))] | .[]?' "$FEED_TMP" > "$NEW_IDS_FILE"
if [ -s "$NEW_IDS_FILE" ]; then
echo "New advisories:"
while IFS= read -r id; do
[ -z "$id" ] && continue
jq -r --arg id "$id" '.advisories[] | select(.id == $id) | "- [\(.severity | ascii_upcase)] \(.id): \(.title)"' "$FEED_TMP"
jq -r --arg id "$id" '.advisories[] | select(.id == $id) | " Exploitability: \(.exploitability_score // "unknown" | ascii_upcase)"' "$FEED_TMP"
jq -r --arg id "$id" '.advisories[] | select(.id == $id) | " Action: \(.action // "Review advisory details")"' "$FEED_TMP"
done < "$NEW_IDS_FILE"
else
echo "FEED_OK - no new advisories"
fi
echo "Affected installed skills (if any):"
found_affected=0
removal_recommended=0
for skill_path in "$INSTALL_ROOT"/*; do
[ -d "$skill_path" ] || continue
skill_name="$(basename "$skill_path")"
skill_hits="$(jq -r --arg skill_prefix "${skill_name}@" '
[.advisories[]
| select(any(.affected[]?; startswith($skill_prefix)))
| "- [\(.severity | ascii_upcase)] \(.id): \(.title)\n Action: \(.action // "Review advisory details")"
] | .[]?
' "$FEED_TMP")"
if [ -n "$skill_hits" ]; then
found_affected=1
echo "- $skill_name is referenced by advisory feed entries"
printf "%s\n" "$skill_hits"
if jq -e --arg skill_prefix "${skill_name}@" '
any(
.advisories[];
any(.affected[]?; startswith($skill_prefix))
and (
((.type // "" | ascii_downcase) == "malicious_skill")
or ((.title // "" | ascii_downcase | test("malicious|exfiltrat|backdoor|trojan|stealer")))
or ((.description // "" | ascii_downcase | test("malicious|exfiltrat|backdoor|trojan|stealer")))
or ((.action // "" | ascii_downcase | test("remove|uninstall|disable|do not use|quarantine")))
)
)
' "$FEED_TMP" >/dev/null 2>&1; then
removal_recommended=1
fi
fi
done
if [ "$found_affected" -eq 0 ]; then
echo "- none"
fi
if [ "$removal_recommended" -eq 1 ]; then
echo "Approval required: ask the user for explicit approval before removing any skill."
echo "Double-confirmation policy: install request is first intent; require a second explicit confirmation with advisory context."
fi
# Persist state
current_utc="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
state_tmp="$TMP/state.json"
jq --arg t "$current_utc" --arg updated "$(jq -r '.updated // ""' "$FEED_TMP")" --argfile feed "$FEED_TMP" '
.last_feed_check = $t
| .last_feed_updated = (if $updated == "" then .last_feed_updated else $updated end)
| .known_advisories = ((.known_advisories // []) + [$feed.advisories[]?.id] | map(select(. != null)) | unique)
' "$STATE_FILE" > "$state_tmp"
mv "$state_tmp" "$STATE_FILE"
chmod 600 "$STATE_FILE"
fi
```
---
## Output Summary
Heartbeat output should include:
- suite version status,
- advisory feed status,
- new advisory list (if any) with exploitability scores,
- installed skills that appear in advisory `affected` lists,
- and a double-confirmation reminder before risky install/remove actions.
### Exploitability-Based Prioritization
When alerting on advisories, prioritize by **exploitability score** in addition to severity:
- `high` exploitability: Trivially or easily exploitable with public tooling, immediate action required
- `medium` exploitability: Exploitable with specific conditions, standard priority
- `low` exploitability: Difficult to exploit or theoretical, low priority
**Priority Rule**: A HIGH severity + HIGH exploitability CVE should be treated more urgently than a CRITICAL severity + LOW exploitability CVE.
If your runtime sends alerts, treat `high` exploitability advisories affecting installed skills as immediate notifications, regardless of severity rating.