@goplus/agentguard 1.0.14 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -81,7 +81,7 @@ exports.TROJAN_RULES = [
81
81
  {
82
82
  id: 'SOCIAL_ENGINEERING',
83
83
  description: 'Detects social engineering pressure language in skill instructions',
84
- severity: 'medium',
84
+ severity: 'high',
85
85
  file_patterns: ['*.md'],
86
86
  patterns: [
87
87
  /CRITICAL\s+REQUIREMENT/i,
@@ -1 +1 @@
1
- {"version":3,"file":"trojan.js","sourceRoot":"","sources":["../../../src/scanner/rules/trojan.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"trojan.js","sourceRoot":"","sources":["../../../src/scanner/rules/trojan.ts"],"names":[],"mappings":";;;AAEA;;GAEG;AACU,QAAA,YAAY,GAAe;IACtC;QACE,EAAE,EAAE,qBAAqB;QACzB,WAAW,EAAE,iFAAiF;QAC9F,QAAQ,EAAE,UAAU;QACpB,aAAa,EAAE,CAAC,MAAM,CAAC;QACvB,QAAQ,EAAE;YACR,gDAAgD;YAChD,qDAAqD;YACrD,gDAAgD;YAChD,oCAAoC;YACpC,8BAA8B;YAC9B,wCAAwC;YACxC,qCAAqC;YACrC,eAAe;SAChB;QACD,SAAS,EAAE,CAAC,OAAe,EAAE,EAAE;YAC7B,uEAAuE;YACvE,MAAM,WAAW,GAAG,8DAA8D,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACjG,MAAM,WAAW,GAAG,kBAAkB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACrD,MAAM,UAAU,GAAG,4CAA4C,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC9E,MAAM,OAAO,GAAG,CAAC,WAAW,EAAE,WAAW,EAAE,UAAU,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC;YAC9E,OAAO,OAAO,IAAI,CAAC,CAAC;QACtB,CAAC;KACF;IACD;QACE,EAAE,EAAE,sBAAsB;QAC1B,WAAW,EAAE,wDAAwD;QACrE,QAAQ,EAAE,MAAM;QAChB,aAAa,EAAE,CAAC,GAAG,CAAC;QACpB,QAAQ,EAAE;YACR,uBAAuB;YACvB,kBAAkB;YAClB,kBAAkB;YAClB,cAAc;YACd,gBAAgB;YAChB,eAAe;YACf,kBAAkB;YAClB,eAAe;SAChB;KACF;IACD;QACE,EAAE,EAAE,eAAe;QACnB,WAAW,EAAE,uCAAuC;QACpD,QAAQ,EAAE,QAAQ;QAClB,aAAa,EAAE,CAAC,GAAG,CAAC;QACpB,QAAQ,EAAE;YACR,gEAAgE;YAChE,0CAA0C;SAC3C;QACD,SAAS,EAAE,CAAC,OAAe,EAAE,KAAuB,EAAE,EAAE;YACtD,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC;YAChC,MAAM,KAAK,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YACxC,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,GAAG,CAAC;gBAAE,OAAO,KAAK,CAAC;YAC3C,+BAA+B;YAC/B,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG;gBAAE,OAAO,KAAK,CAAC,CAAE,WAAW;YAChD,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC;gBAAE,OAAO,KAAK,CAAC,CAAI,UAAU;YAC/C,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,EAAE;gBAAE,OAAO,KAAK,CAAC,CAAG,WAAW;YAChD,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE;gBAAE,OAAO,KAAK,CAAC,CAAC,gBAAgB;YACxF,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG;gBAAE,OAAO,KAAK,CAAC,CAAC,cAAc;YACtE,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG;gBAAE,OAAO,KAAK,CAAC,CAAC,aAAa;YACrE,+DAA+D;YAC/D,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC;gBAAE,OAAO,KAAK,CAAC;YACrE,OAAO,IAAI,CAAC;QACd,CAAC;KACF;IACD;QACE,EAAE,EAAE,oBAAoB;QACxB,WAAW,EAAE,oEAAoE;QACjF,QAAQ,EAAE,MAAM;QAChB,aAAa,EAAE,CAAC,MAAM,CAAC;QACvB,QAAQ,EAAE;YACR,yBAAyB;YACzB,8BAA8B;YAC9B,8CAA8C;YAC9C,sDAAsD;YACtD,qDAAqD;YACrD,+BAA+B;SAChC;QACD,SAAS,EAAE,CAAC,OAAe,EAAE,EAAE;YAC7B,mEAAmE;YACnE,OAAO,qDAAqD,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC7E,CAAC;KACF;CACF,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goplus/agentguard",
3
- "version": "1.0.14",
3
+ "version": "1.1.1",
4
4
  "description": "GoPlus AgentGuard — Security guard for AI agents. Blocks dangerous commands, prevents data leaks, protects secrets. 20 detection rules, runtime action evaluation, trust registry.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: agentguard
3
- description: GoPlus AgentGuard — AI agent security guard. Run /agentguard checkup for a full security health check: scans all installed skills, checks credentials, permissions, and network exposure, then delivers an HTML report directly to you. Also use for scanning third-party code, blocking dangerous commands, preventing data leaks, evaluating action safety, and running daily security patrols.
3
+ description: GoPlus AgentGuard — AI agent security guard. Run /agentguard checkup for a full security health check, scans all installed skills, checks credentials, permissions, and network exposure, then delivers an HTML report directly to you. Also use for scanning third-party code, blocking dangerous commands, preventing data leaks, evaluating action safety, and running daily security patrols.
4
4
  license: MIT
5
5
  compatibility: Requires Node.js 18+. Optional GoPlus API credentials for enhanced Web3 simulation.
6
6
  metadata:
@@ -8,7 +8,7 @@ metadata:
8
8
  version: "1.1"
9
9
  optional_env: "GOPLUS_API_KEY, GOPLUS_API_SECRET (for Web3 transaction simulation only)"
10
10
  user-invocable: true
11
- allowed-tools: Read, Grep, Glob, Bash(node *trust-cli.ts *) Bash(node *action-cli.ts *) Bash(*checkup-report.js) Bash(echo *checkup-report.js) Bash(cat *checkup-report.js) Bash(openclaw *) Bash(ss *) Bash(lsof *) Bash(ufw *) Bash(iptables *) Bash(crontab *) Bash(systemctl list-timers *) Bash(find *) Bash(stat *) Bash(env) Bash(sha256sum *) Bash(node *) Bash(cd *)
11
+ allowed-tools: Read, Write, Grep, Glob, Bash(node *trust-cli.ts *) Bash(node *action-cli.ts *) Bash(*checkup-report.js) Bash(echo *checkup-report.js) Bash(cat *checkup-report.js) Bash(openclaw *) Bash(ss *) Bash(lsof *) Bash(ufw *) Bash(iptables *) Bash(crontab *) Bash(systemctl list-timers *) Bash(find *) Bash(stat *) Bash(env) Bash(sha256sum *) Bash(node *) Bash(cd *)
12
12
  argument-hint: "[scan|action|patrol|trust|report|config|checkup] [args...]"
13
13
  ---
14
14
 
@@ -89,7 +89,7 @@ For each rule, use Grep to search the relevant file types. Record every match wi
89
89
  | 21 | TROJAN_DISTRIBUTION | CRITICAL | md | Trojanized binary download + password + execute |
90
90
  | 22 | SUSPICIOUS_PASTE_URL | HIGH | all | URLs to paste sites (pastebin, glot.io, etc.) |
91
91
  | 23 | SUSPICIOUS_IP | MEDIUM | all | Hardcoded public IPv4 addresses |
92
- | 24 | SOCIAL_ENGINEERING | MEDIUM | md | Pressure language + execution instructions |
92
+ | 24 | SOCIAL_ENGINEERING | HIGH | md | Pressure language + execution instructions |
93
93
 
94
94
  ### Risk Level Calculation
95
95
 
@@ -284,7 +284,7 @@ Detect tampered or unregistered skill packages by comparing file hashes against
284
284
  Scan workspace files for leaked secrets using AgentGuard's own detection patterns.
285
285
 
286
286
  **Steps**:
287
- 1. Use Grep to scan `$OC/workspace/` (especially `memory/` and `logs/`) with patterns from:
287
+ 1. Use Grep to scan `$OC/workspace/` **recursively, covering all agent subdirectories** (e.g. all `workspace-agent-*/` directories, not just the current agent's workspace) with patterns from:
288
288
  - scan-rules.md Rule 7 (PRIVATE_KEY_PATTERN): `0x[a-fA-F0-9]{64}` in quotes
289
289
  - scan-rules.md Rule 8 (MNEMONIC_PATTERN): BIP-39 word sequences, `seed_phrase`, `mnemonic`
290
290
  - scan-rules.md Rule 5 (READ_SSH_KEYS): SSH key file references in workspace
@@ -610,89 +610,129 @@ Run a comprehensive agent health checkup across 6 security dimensions. Generates
610
610
 
611
611
  ### Step 1: Data Collection
612
612
 
613
+ **IMPORTANT: You MUST run ALL 7 checks below — not just the skill scan. The checkup covers 5 security dimensions, not just code scanning. Do NOT skip checks 2–7.**
614
+
613
615
  Run these checks in parallel where possible. These are **universal agent security checks** — they apply to any Claude Code or OpenClaw environment, regardless of whether AgentGuard is installed.
614
616
 
615
- 1. **Discover & scan installed skills**: Glob `~/.claude/skills/*/SKILL.md` and `~/.openclaw/skills/*/SKILL.md`. For each discovered skill, **run `/agentguard scan <skill_path>`** using the scan subcommand logic (24 detection rules). Collect the scan results (risk level, findings count, risk tags) for each skill.
616
- 2. **Credential file permissions**: `stat` on `~/.ssh/`, `~/.gnupg/`, and if OpenClaw: `stat` on `$OC/openclaw.json`, `$OC/devices/paired.json`
617
- 3. **Sensitive credential scan (DLP)**: Use Grep to scan workspace memory/logs directories for leaked secrets:
618
- - Private keys: `0x[a-fA-F0-9]{64}`, `-----BEGIN.*PRIVATE KEY-----`
619
- - Mnemonics: sequences of 12+ BIP-39 words, `seed_phrase`, `mnemonic`
620
- - API keys/tokens: `AKIA[0-9A-Z]{16}`, `gh[pousr]_[A-Za-z0-9_]{36}`, plaintext passwords
621
- 4. **Network exposure**: Run `lsof -i -P -n 2>/dev/null | grep LISTEN` or `ss -tlnp 2>/dev/null` to check for dangerous open ports (Redis 6379, Docker API 2375, MySQL 3306, MongoDB 27017 on 0.0.0.0)
622
- 5. **Scheduled tasks audit**: Check `crontab -l 2>/dev/null` for suspicious entries containing `curl|bash`, `wget|sh`, or accessing `~/.ssh/`
623
- 6. **Environment variable exposure**: Run `env` and check for sensitive variable names (`PRIVATE_KEY`, `MNEMONIC`, `SECRET`, `PASSWORD`) detect presence only, mask values
624
- 7. **Runtime protection check**: Check if security hooks exist in `~/.claude/settings.json`, check for audit logs at `~/.agentguard/audit.jsonl`
617
+ 1. **[REQUIRED] Discover & scan installed skills** (→ feeds Dimension 1: Code Safety): Glob ALL of the following paths for `*/SKILL.md`:
618
+ - `~/.claude/skills/*/SKILL.md`
619
+ - `~/.openclaw/skills/*/SKILL.md`
620
+ - `~/.openclaw/workspace/skills/*/SKILL.md`
621
+ - `~/.qclaw/skills/*/SKILL.md`
622
+ - `~/.qclaw/workspace/skills/*/SKILL.md`
623
+
624
+ For **every** discovered skill, **run `/agentguard scan <skill_path>`** using the scan subcommand logic (24 detection rules). Do NOT skip any skill regardless of how many are found. Collect the scan results (risk level, findings count, risk tags) for each skill.
625
+ 2. **[REQUIRED] Credential file permissions** (→ feeds Dimension 2: Credential Safety): Platform-aware checkbehavior differs by OS:
626
+ - **macOS/Linux**: Run `stat -f '%Lp' <path> 2>/dev/null || stat -c '%a' <path> 2>/dev/null` on `~/.ssh/`, `~/.gnupg/`, and if OpenClaw: on `$OC/openclaw.json`, `$OC/devices/paired.json`. **If the command returns empty output, the directory does not exist — treat as N/A (award full points), do NOT flag as a failure.**
627
+ - **Windows**: `stat` is not available. Use `icacls <path>` to check ACLs instead. If the directory does not exist, treat as N/A (award full points). If it exists, check that the ACL grants access only to the current user (no `Everyone`, `Users`, or `Authenticated Users` with write/read access). Flag as FAIL only if the directory exists AND the ACL is overly permissive.
628
+ 3. **[REQUIRED] Sensitive credential scan / DLP** (→ feeds Dimension 2: Credential Safety): Use Grep to scan **all** agent workspace directories for leaked secrets. This MUST cover the entire workspace root, not just the current agent's directory:
629
+ - For OpenClaw / QClaw: scan `~/.openclaw/workspace/` and `~/.qclaw/workspace/` recursively — this includes **all** `workspace-agent-*/` subdirectories, not just the current agent's workspace
630
+ - For Claude Code: scan `~/.claude/` recursively
631
+ - Patterns to detect:
632
+ - Private keys: `0x[a-fA-F0-9]{64}`, `-----BEGIN.*PRIVATE KEY-----`
633
+ - Mnemonics: sequences of 12+ BIP-39 words, `seed_phrase`, `mnemonic`
634
+ - API keys/tokens: `AKIA[0-9A-Z]{16}`, `gh[pousr]_[A-Za-z0-9_]{36}`, plaintext passwords
635
+ - **Important**: Use the workspace *root* directory as the scan target (e.g. `~/.qclaw/workspace/`), not a specific agent subdirectory. All sibling `workspace-agent-*` directories must be included.
636
+ 4. **[REQUIRED] Network exposure** (→ feeds Dimension 3: Network & System): Run `lsof -i -P -n 2>/dev/null | grep LISTEN` or `ss -tlnp 2>/dev/null` to check for dangerous open ports (Redis 6379, Docker API 2375, MySQL 3306, MongoDB 27017 on 0.0.0.0)
637
+ 5. **[REQUIRED] Scheduled tasks audit** (→ feeds Dimension 3: Network & System): Check `crontab -l 2>/dev/null` for suspicious entries containing `curl|bash`, `wget|sh`, or accessing `~/.ssh/`
638
+ 6. **[REQUIRED] Environment variable exposure** (→ feeds Dimension 3: Network & System): Run `env` and check for sensitive variable names (`PRIVATE_KEY`, `MNEMONIC`, `SECRET`, `PASSWORD`) — detect presence only, mask values
639
+ 7. **[REQUIRED] Runtime protection check** (→ feeds Dimension 4: Runtime Protection): Check if security hooks exist in `~/.claude/settings.json` or `~/.openclaw/openclaw.json`, check for audit logs at `~/.agentguard/audit.jsonl`
625
640
 
626
641
  ### Step 2: Score Calculation
627
642
 
628
- Checklist-based scoring across 6 security dimensions. **Every failed check = 1 finding with severity and description.**
643
+ **Additive scoring**: Each dimension starts at **0**. For each check that **passes**, add the listed points. Maximum is 100 per dimension. **Every failed check = 1 finding with severity and description.**
629
644
 
630
645
  #### Dimension 1: Skill & Code Safety (weight: 25%)
631
646
 
632
- Uses AgentGuard's 24-rule scan engine (`/agentguard scan`) to audit each installed skill.
647
+ Uses AgentGuard's 24-rule scan engine (`/agentguard scan`) to audit each installed skill. Start at base 100 and **deduct** for findings:
648
+
649
+ - Base score: **100**
650
+ - Each CRITICAL finding: **−15**
651
+ - Each HIGH finding: **−8**
652
+ - Each MEDIUM finding: **−3**
653
+ - Floor at **0** (never negative)
633
654
 
634
- | Check | Score | If failed finding |
635
- |-------|-------|---------------------|
636
- | All skills scanned with risk level LOW | +40 | For each skill with findings, add per-finding: "<rule_id> in <skill>:<file>:<line>" with its severity |
637
- | No CRITICAL scan findings across all skills | +30 | "CRITICAL: <rule_id> detected in <skill>" (CRITICAL) |
638
- | No HIGH scan findings across all skills | +30 | "HIGH: <rule_id> detected in <skill>" (HIGH) |
655
+ For each finding, add: `"<rule_id> in <skill>:<file>:<line>"` with its severity.
639
656
 
640
- Deductions from base 100: each CRITICAL finding −15, HIGH −8, MEDIUM −3. Floor at 0.
657
+ **False-positive suppression**: When the scanned skill is `agentguard` itself (skill path contains `agentguard`), suppress `READ_ENV_SECRETS` findings — AgentGuard reads environment variables as part of its own configuration detection, which is expected behaviour and not a security risk. Do not deduct points or list these as findings in the report.
641
658
 
642
- If no skills installed: score = 70, add finding: "No third-party skills installed — no code to audit" (LOW).
659
+ If no skills installed: score = **70**, add finding: "No third-party skills installed — no code to audit" (LOW).
643
660
 
644
661
  #### Dimension 2: Credential & Secret Safety (weight: 25%)
645
662
 
646
- Checks for leaked credentials and permission hygiene.
663
+ Checks for leaked credentials and permission hygiene. Start at **0**, add points for each check that **passes** (total possible = 100):
647
664
 
648
- | Check | Score | If failed → finding |
649
- |-------|-------|---------------------|
650
- | `~/.ssh/` permissions are 700 or stricter | +25 | "~/.ssh/ permissions too open (<actual>) — should be 700" (HIGH) |
651
- | `~/.gnupg/` permissions are 700 or stricter | +15 | "~/.gnupg/ permissions too open (<actual>) — should be 700" (MEDIUM) |
652
- | No private keys (hex 0x..64, PEM) found in skill code or workspace | +25 | "Plaintext private key found in <location>" (CRITICAL) |
653
- | No mnemonic phrases found in skill code or workspace | +20 | "Plaintext mnemonic found in <location>" (CRITICAL) |
654
- | No API keys/tokens (AWS AKIA.., GitHub gh*_) found in skill code | +15 | "API key/token found in <location>" (HIGH) |
665
+ | Check | Points if PASS | If FAIL → finding |
666
+ |-------|---------------|-------------------|
667
+ | `~/.ssh/` permissions are 700 or stricter | **+25** | "~/.ssh/ permissions too open (<actual>) — should be 700" (HIGH) |
668
+ | `~/.gnupg/` permissions are 700 or stricter | **+15** | "~/.gnupg/ permissions too open (<actual>) — should be 700" (MEDIUM) |
669
+
670
+ **Permission check rules (to avoid false positives):**
671
+ - **Directory does not exist** (stat/icacls returns empty or "file not found"): Treat as N/A award the points. A missing `~/.ssh/` or `~/.gnupg/` is not a security risk.
672
+ - **Windows**: Use `icacls` instead of `stat`. Award full points if directory doesn't exist. Flag as FAIL only if directory exists AND ACL grants access to `Everyone`, `Users`, or `Authenticated Users`.
673
+ - **macOS/Linux**: Flag as FAIL only when the directory exists AND stat returns a numeric value AND that value is greater than 700.
674
+ | No private keys (hex 0x..64, PEM) found in skill code or workspace | **+25** | "Plaintext private key found in <location>" (CRITICAL) |
675
+ | No mnemonic phrases found in skill code or workspace | **+20** | "Plaintext mnemonic found in <location>" (CRITICAL) |
676
+ | No API keys/tokens (AWS AKIA.., GitHub gh*_) found in skill code | **+15** | "API key/token found in <location>" (HIGH) |
655
677
 
656
678
  #### Dimension 3: Network & System Exposure (weight: 20%)
657
679
 
658
- Checks for dangerous network exposure and system-level risks.
680
+ Checks for dangerous network exposure and system-level risks. Start at **0**, add points for each check that **passes** (total possible = 100):
681
+
682
+ | Check | Points if PASS | If FAIL → finding |
683
+ |-------|---------------|-------------------|
684
+ | No high-risk ports exposed on 0.0.0.0 (Redis/Docker/MySQL/MongoDB) | **+35** | "Dangerous port exposed: <service> on 0.0.0.0:<port>" (HIGH) |
685
+ | No suspicious cron jobs (curl\|bash, wget\|sh, accessing ~/.ssh/) | **+30** | "Suspicious cron job: <command>" (HIGH) |
686
+ | No sensitive env vars with dangerous names (PRIVATE_KEY, MNEMONIC) | **+20** | "Sensitive env var exposed: <name>" (MEDIUM) |
687
+ | OpenClaw config files have proper permissions (600) if applicable | **+15** | "OpenClaw config <file> permissions too open" (MEDIUM) |
659
688
 
660
- | Check | Score | If failedfinding |
661
- |-------|-------|---------------------|
662
- | No high-risk ports exposed on 0.0.0.0 (Redis/Docker/MySQL/MongoDB) | +35 | "Dangerous port exposed: <service> on 0.0.0.0:<port>" (HIGH) |
663
- | No suspicious cron jobs (curl\|bash, wget\|sh, accessing ~/.ssh/) | +30 | "Suspicious cron job: <command>" (HIGH) |
664
- | No sensitive env vars with dangerous names (PRIVATE_KEY, MNEMONIC) | +20 | "Sensitive env var exposed: <name>" (MEDIUM) |
665
- | OpenClaw config files have proper permissions (600) if applicable | +15 | "OpenClaw config <file> permissions too open" (MEDIUM) |
689
+ **Example**: If no dangerous ports (+35), no suspicious cron (+30), but env var `PRIVATE_KEY` found (+0), and not OpenClaw (+15 skip, give points) score = 35 + 30 + 0 + 15 = **80**.
666
690
 
667
691
  #### Dimension 4: Runtime Protection (weight: 15%)
668
692
 
669
- Checks whether the agent has active security monitoring.
693
+ Checks whether the agent has active security monitoring. Start at **0**, add points for each check that **passes** (total possible = 100):
670
694
 
671
- | Check | Score | If failed → finding |
672
- |-------|-------|---------------------|
673
- | Security hooks/guards installed (AgentGuard, custom hooks, etc.) | +40 | "No security hooks installed — actions are unmonitored" (HIGH) |
674
- | Security audit log exists with recent events | +30 | "No security audit log — no threat history available" (MEDIUM) |
675
- | Skills have been security-scanned at least once | +30 | "Installed skills have never been security-scanned" (MEDIUM) |
695
+ | Check | Points if PASS | If FAIL → finding |
696
+ |-------|---------------|-------------------|
697
+ | Security hooks/guards installed (AgentGuard, custom hooks, etc.) | **+40** | "No security hooks installed — actions are unmonitored" (HIGH) |
698
+ | Security audit log exists with recent events | **+30** | "No security audit log — no threat history available" (MEDIUM) |
699
+ | Skills have been security-scanned at least once | **+30** | "Installed skills have never been security-scanned" (MEDIUM) |
676
700
 
677
701
  #### Dimension 5: Web3 Safety (weight: 15% if applicable)
678
702
 
679
- Only if Web3 usage is detected (env vars like `GOPLUS_API_KEY`, `CHAIN_ID`, `RPC_URL`, or web3-related skills installed). Otherwise `{ "score": null, "na": true }`.
703
+ Only if Web3 usage is detected (env vars like `GOPLUS_API_KEY`, `CHAIN_ID`, `RPC_URL`, or web3-related skills installed). Otherwise `{ "score": null, "na": true }`. Start at **0**, add points for each check that **passes** (total possible = 100):
704
+
705
+ | Check | Points if PASS | If FAIL → finding |
706
+ |-------|---------------|-------------------|
707
+ | No wallet-draining patterns (approve+transferFrom) in skill code | **+40** | "Wallet-draining pattern detected in <skill>" (CRITICAL) |
708
+ | No unlimited token approval patterns in skill code | **+30** | "Unlimited approval pattern detected in <skill>" (HIGH) |
709
+ | Transaction security API configured (GoPlus or equivalent) | **+30** | "No transaction security API — Web3 calls are unverified" (MEDIUM) |
710
+
711
+ #### Composite Score Calculation
712
+
713
+ Calculate the weighted average of all applicable dimensions:
714
+
715
+ ```
716
+ composite_score = (code_safety × 0.25) + (credential_safety × 0.25) + (network_exposure × 0.20) + (runtime_protection × 0.15) + (web3_safety × 0.15)
717
+ ```
718
+
719
+ If Web3 Safety is N/A, redistribute its 15% weight proportionally across the other 4 dimensions:
720
+ ```
721
+ composite_score = (code_safety × 0.294) + (credential_safety × 0.294) + (network_exposure × 0.235) + (runtime_protection × 0.176)
722
+ ```
680
723
 
681
- | Check | Score | If failed → finding |
682
- |-------|-------|---------------------|
683
- | No wallet-draining patterns (approve+transferFrom) in skill code | +40 | "Wallet-draining pattern detected in <skill>" (CRITICAL) |
684
- | No unlimited token approval patterns in skill code | +30 | "Unlimited approval pattern detected in <skill>" (HIGH) |
685
- | Transaction security API configured (GoPlus or equivalent) | +30 | "No transaction security API — Web3 calls are unverified" (MEDIUM) |
724
+ Round to the nearest integer.
686
725
 
687
- #### Composite Score
726
+ **Tier assignment (MUST use these exact thresholds):**
688
727
 
689
- Weighted average of all applicable dimensions. If Web3 Safety is N/A, redistribute its 15% weight proportionally.
728
+ | Score Range | Tier | Label |
729
+ |-------------|------|-------|
730
+ | **90–100** | **S** | JACKED |
731
+ | **70–89** | **A** | Healthy |
732
+ | **50–69** | **B** | Tired |
733
+ | **0–49** | **F** | Critical |
690
734
 
691
- Determine tier:
692
- - 90–100 → Tier **S** (JACKED)
693
- - 70–89 → Tier **A** (Healthy)
694
- - 50–69 → Tier **B** (Tired)
695
- - 0–49 → Tier **F** (Critical)
735
+ **Example**: code_safety=100, credential_safety=80, network_exposure=85, runtime_protection=30, web3=N/A → composite = (100×0.294)+(80×0.294)+(85×0.235)+(30×0.176) = 29.4+23.5+20.0+5.3 = **78** → Tier **A** (Healthy).
696
736
 
697
737
  ### Step 3: Generate Analysis Report
698
738
 
@@ -709,6 +749,18 @@ This report goes into the `"analysis"` field of the JSON output.
709
749
 
710
750
  Also generate a list of actionable recommendations as `{ "severity": "...", "text": "..." }` objects for the structured view.
711
751
 
752
+ ### Pre-Step-4 Validation
753
+
754
+ **Before assembling the JSON, verify you have collected data for ALL 5 dimensions:**
755
+
756
+ - [ ] `code_safety` — from Step 1 check 1 (skill scanning)
757
+ - [ ] `credential_safety` — from Step 1 checks 2 + 3 (permissions + DLP)
758
+ - [ ] `network_exposure` — from Step 1 checks 4 + 5 + 6 (ports + cron + env vars)
759
+ - [ ] `runtime_protection` — from Step 1 check 7 (hooks + audit log)
760
+ - [ ] `web3_safety` — from Step 2 (only if Web3 detected, otherwise `{ "score": null, "na": true }`)
761
+
762
+ **If any dimension is missing data, go back and run the missing checks. Do NOT submit a report with only code_safety filled in.**
763
+
712
764
  ### Step 4: Generate Report
713
765
 
714
766
  Assemble the results into a JSON object and pipe it to the report generator:
@@ -734,16 +786,21 @@ Assemble the results into a JSON object and pipe it to the report generator:
734
786
  }
735
787
  ```
736
788
 
737
- Execute (remember to `cd` into the skill directory first see "Resolving Script Paths" above):
789
+ Execute the report generator. **Use the `--file` method for cross-platform compatibility** (the `echo | pipe` method fails on Windows due to shell quoting differences):
790
+
791
+ 1. First, write the JSON to a temporary file using the Write tool (e.g. `/tmp/agentguard-checkup-data.json`)
792
+ 2. Then run (remember to `cd` into the skill directory first — see "Resolving Script Paths" above):
738
793
  ```bash
739
- cd <skill_directory> && echo '<json>' | node scripts/checkup-report.js
794
+ cd <skill_directory> && node scripts/checkup-report.js --file /tmp/agentguard-checkup-data.json
740
795
  ```
741
796
 
742
797
  The script outputs the HTML file path to stdout (e.g. `/tmp/agentguard-checkup-1234567890.html`). Capture this path — you will need it for delivery in Step 6.
743
798
 
744
- ### Step 5: Terminal Summary
799
+ > **Note**: The script also supports stdin pipe (`echo '<json>' | node scripts/checkup-report.js`) but this may fail on Windows cmd.exe where single quotes are not string delimiters. Always prefer `--file`.
800
+
801
+ ### Step 5: Terminal Summary (REQUIRED)
745
802
 
746
- After the report generates, output a brief summary in the terminal:
803
+ **You MUST output this summary after the report generates.** This is the primary output the user sees. Do NOT skip this step — always show the score, dimension table, and report path:
747
804
 
748
805
  ```
749
806
  ## 🦞 GoPlus AgentGuard Health Checkup
@@ -763,7 +820,27 @@ After the report generates, output a brief summary in the terminal:
763
820
  **Full visual report**: <path> (opened in browser)
764
821
 
765
822
  💡 Top recommendation: <first recommendation text>
823
+
824
+ ### Next Steps
825
+ (Only include this section if there are HIGH or CRITICAL findings.)
826
+
827
+ List each HIGH or CRITICAL finding as a plain-language suggestion — no commands, no JSON, no technical details. One sentence per item. Ask the user to confirm if they'd like help with any of them.
828
+
829
+ Format:
766
830
  ```
831
+ ⚠️ A few things need your attention:
832
+ 1. 🔴 <plain description of critical issue and why it matters>
833
+ 2. 🟠 <plain description of high issue and why it matters>
834
+ ...
835
+
836
+ Reply with the number(s) you'd like help with and I'll walk you through it.
837
+ ```
838
+
839
+ Examples of plain-language descriptions:
840
+ - No hooks: "Security monitoring isn't active — AgentGuard can't block threats in real-time until hooks are configured."
841
+ - Unregistered skills: "10 installed skills haven't been security-reviewed — they're running with no trust level assigned."
842
+ - SSH permissions: "Your SSH key folder has loose permissions — other processes on this machine could potentially read your private keys."
843
+ - Plaintext credential: "A private key or API token was found in plain text in a file — it should be removed and rotated."
767
844
 
768
845
  ### Step 6: Deliver the Report to the User
769
846
 
@@ -75,6 +75,7 @@ Detailed commands, patterns, and thresholds for the 8 patrol checks. This docume
75
75
 
76
76
  ### Permission Checks
77
77
 
78
+ **macOS/Linux:**
78
79
  ```bash
79
80
  # SSH directory — should be 700
80
81
  stat -f "%Lp" ~/.ssh/ 2>/dev/null || stat -c "%a" ~/.ssh/ 2>/dev/null
@@ -82,10 +83,19 @@ stat -f "%Lp" ~/.ssh/ 2>/dev/null || stat -c "%a" ~/.ssh/ 2>/dev/null
82
83
  stat -f "%Lp" ~/.gnupg/ 2>/dev/null || stat -c "%a" ~/.gnupg/ 2>/dev/null
83
84
  ```
84
85
 
86
+ **Windows (use icacls instead of stat):**
87
+ ```powershell
88
+ icacls $env:USERPROFILE\.ssh 2>$null
89
+ icacls $env:USERPROFILE\.gnupg 2>$null
90
+ ```
91
+
85
92
  | Condition | Severity |
86
93
  |-----------|----------|
87
- | `~/.ssh/` permissions > 700 | HIGH |
88
- | `~/.gnupg/` permissions > 700 | MEDIUM |
94
+ | macOS/Linux: `~/.ssh/` exists AND permissions > 700 | HIGH |
95
+ | macOS/Linux: `~/.gnupg/` exists AND permissions > 700 | MEDIUM |
96
+ | Windows: `~/.ssh/` exists AND ACL grants access to Everyone/Users/Authenticated Users | HIGH |
97
+ | Windows: `~/.gnupg/` exists AND ACL grants access to Everyone/Users/Authenticated Users | MEDIUM |
98
+ | Directory does not exist (stat/icacls returns empty) | N/A — not a finding |
89
99
 
90
100
  ---
91
101
 
@@ -292,7 +292,7 @@ Detects trojanized binary distribution patterns. Flags when 2+ of the following
292
292
  - Version-like patterns (`x.0.0.0`)
293
293
  - Values > 255 in any octet
294
294
 
295
- ## Rule 24: SOCIAL_ENGINEERING (MEDIUM)
295
+ ## Rule 24: SOCIAL_ENGINEERING (HIGH)
296
296
  **Files**: `*.md`
297
297
 
298
298
  | Pattern | Description |
@@ -15,9 +15,17 @@
15
15
  import { writeFileSync, readFileSync, existsSync } from 'node:fs';
16
16
  import { join, dirname } from 'node:path';
17
17
  import { tmpdir, homedir } from 'node:os';
18
- import { exec } from 'node:child_process';
18
+ import { exec, spawn } from 'node:child_process';
19
19
  import { fileURLToPath } from 'node:url';
20
20
 
21
+ const DIM_META = {
22
+ code_safety: { icon: 'find_in_page', name: 'Skill & Code Safety', zh: '技能与代码安全' },
23
+ credential_safety: { icon: 'key', name: 'Credential & Secrets', zh: '凭证与密钥安全' },
24
+ network_exposure: { icon: 'lan', name: 'Network & System', zh: '网络与系统暴露' },
25
+ runtime_protection: { icon: 'shield', name: 'Runtime Protection', zh: '运行时防护' },
26
+ web3_safety: { icon: 'token', name: 'Web3 Safety', zh: 'Web3 安全' },
27
+ };
28
+
21
29
  // Try to load favicon from agentguard-server or fallback
22
30
  const __dirname = dirname(fileURLToPath(import.meta.url));
23
31
  let faviconB64 = '';
@@ -29,13 +37,27 @@ for (const p of iconPaths) {
29
37
  if (existsSync(p)) { faviconB64 = readFileSync(p).toString('base64'); break; }
30
38
  }
31
39
 
32
- let input = '';
33
- process.stdin.setEncoding('utf8');
34
- process.stdin.on('data', (chunk) => { input += chunk; });
35
- process.stdin.on('end', () => {
36
- try { generateReport(JSON.parse(input)); }
37
- catch (err) { process.stderr.write(`Error: ${err.message}\n`); process.exit(1); }
38
- });
40
+ // Support --file <path> argument to read JSON from file (cross-platform friendly)
41
+ const fileArgIdx = process.argv.indexOf('--file');
42
+ if (fileArgIdx !== -1 && process.argv[fileArgIdx + 1]) {
43
+ const filePath = process.argv[fileArgIdx + 1];
44
+ try {
45
+ const content = readFileSync(filePath, 'utf8');
46
+ generateReport(JSON.parse(content));
47
+ } catch (err) {
48
+ process.stderr.write(`Error reading ${filePath}: ${err.message}\n`);
49
+ process.exit(1);
50
+ }
51
+ } else {
52
+ // Fallback: read JSON from stdin (pipe)
53
+ let input = '';
54
+ process.stdin.setEncoding('utf8');
55
+ process.stdin.on('data', (chunk) => { input += chunk; });
56
+ process.stdin.on('end', () => {
57
+ try { generateReport(JSON.parse(input)); }
58
+ catch (err) { process.stderr.write(`Error: ${err.message}\n`); process.exit(1); }
59
+ });
60
+ }
39
61
 
40
62
  // ---------------------------------------------------------------------------
41
63
  // Helpers
@@ -73,13 +95,7 @@ function getTier(score) {
73
95
  ])};
74
96
  }
75
97
 
76
- const DIM_META = {
77
- code_safety: { icon: 'find_in_page', name: 'Skill & Code Safety', zh: '技能与代码安全' },
78
- credential_safety: { icon: 'key', name: 'Credential & Secrets', zh: '凭证与密钥安全' },
79
- network_exposure: { icon: 'lan', name: 'Network & System', zh: '网络与系统暴露' },
80
- runtime_protection: { icon: 'shield', name: 'Runtime Protection', zh: '运行时防护' },
81
- web3_safety: { icon: 'token', name: 'Web3 Safety', zh: 'Web3 安全' },
82
- };
98
+
83
99
 
84
100
  function esc(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
85
101
 
@@ -730,7 +746,8 @@ function generateReport(data) {
730
746
  autoRecs.push({ severity: 'LOW', text: 'Enable auto-scan on session start: export AGENTGUARD_AUTO_SCAN=1', zh: '启用会话启动时自动扫描:export AGENTGUARD_AUTO_SCAN=1' });
731
747
 
732
748
  // Merge: user recs first, then auto recs (dedup by text similarity)
733
- const allRecs = [...recommendations];
749
+ // Guard against malformed entries where text may be undefined (#37)
750
+ const allRecs = recommendations.filter(r => r && typeof r.text === 'string');
734
751
  for (const ar of autoRecs) {
735
752
  if (!allRecs.some(r => r.text.toLowerCase().includes(ar.text.slice(0, 30).toLowerCase()))) {
736
753
  allRecs.push(ar);
@@ -747,11 +764,10 @@ function generateReport(data) {
747
764
  const sc = sevColor(sev);
748
765
  const zhText = r.zh || r.text;
749
766
  return `
750
- <div class="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-[#262a31]/30 transition-colors group">
767
+ <div class="flex items-center gap-3 px-4 py-3 rounded-lg">
751
768
  <span class="w-5 text-xs font-headline font-bold text-[#849588]/60">${i+1}</span>
752
769
  <span class="px-2 py-0.5 rounded text-[9px] font-bold uppercase tracking-wide text-white shrink-0" style="background:${sc}">${sev}</span>
753
770
  <span class="text-sm text-[#b9cbbd] leading-snug rec-text" data-en="${esc(r.text)}" data-zh="${esc(zhText)}">${esc(r.text)}</span>
754
- <span class="material-symbols-outlined text-sm text-[#849588]/0 group-hover:text-[#849588]/50 transition-colors ml-auto shrink-0">chevron_right</span>
755
771
  </div>`;
756
772
  }).join('')}</div>`
757
773
  : '<div class="text-center py-12 text-[#849588]" data-i18n="no_recs">No recommendations.</div>';
@@ -759,12 +775,7 @@ function generateReport(data) {
759
775
  // ── AI Analysis report ──
760
776
  const analysisText = data.analysis || '';
761
777
  const analysisHtml = analysisText
762
- ? `<div class="relative group">
763
- <div class="bg-[#0a0e14] border border-[#3a4a3f]/10 rounded-xl p-5 text-sm text-[#b9cbbd] leading-relaxed whitespace-pre-line" id="analysisText">${esc(analysisText)}</div>
764
- <button onclick="copyReport()" id="copyBtn" class="absolute top-3 right-3 flex items-center gap-1.5 px-3 py-1.5 bg-[#262a31] border border-[#3a4a3f]/30 rounded-lg text-[11px] font-semibold text-[#849588] hover:text-[#dfe2eb] hover:border-[#849588]/50 transition-all opacity-0 group-hover:opacity-100">
765
- <span class="material-symbols-outlined text-sm" id="copyIcon">content_copy</span><span id="copyLabel" data-i18n="copy_report">Copy Report</span>
766
- </button>
767
- </div>`
778
+ ? `<div class="bg-[#0a0e14] border border-[#3a4a3f]/10 rounded-xl p-5 text-sm text-[#b9cbbd] leading-relaxed whitespace-pre-line" id="analysisText">${esc(analysisText)}</div>`
768
779
  : '';
769
780
 
770
781
  // ── Health status label ──
@@ -919,6 +930,9 @@ body{background:#0a0e14;color:#dfe2eb;font-family:'Inter',sans-serif}
919
930
  <p class="text-[10px] font-label uppercase tracking-[0.2em] text-[#849588] mb-1" data-i18n="sec_analysis">Security Analysis</p>
920
931
  <h1 class="text-2xl font-headline font-bold text-[#f5fff5] tracking-tight flex items-center gap-3">
921
932
  <span class="material-symbols-outlined" style="color:${tier.color}">analytics</span><span data-i18n="diag_report">Diagnostic Report</span>
933
+ <button onclick="copyReport()" id="copyBtn" class="flex items-center gap-1 px-2 py-1 bg-[#262a31] border border-[#3a4a3f]/30 rounded-lg text-[11px] font-semibold text-[#849588] hover:text-[#dfe2eb] hover:border-[#849588]/50 transition-all ml-1">
934
+ <span class="material-symbols-outlined text-sm" id="copyIcon">content_copy</span><span id="copyLabel" class="hidden sm:inline" data-i18n="copy_report">Copy Report</span>
935
+ </button>
922
936
  </h1>
923
937
  </div>
924
938
  <div class="bg-[#262a31] border border-[#3a4a3f]/15 rounded-lg px-4 py-2 flex items-center gap-2">
@@ -1036,7 +1050,7 @@ body{background:#0a0e14;color:#dfe2eb;font-family:'Inter',sans-serif}
1036
1050
  i18n.zh.quote=quotes_zh['${tier.grade}']||quotes_zh.B;
1037
1051
  i18n.en.quote='"${tier.quote.replace(/'/g,"\\'")}\"';
1038
1052
 
1039
- let curLang='en';
1053
+ let curLang=(${JSON.stringify(data.analysis||'')}).match(/[\u4e00-\u9fff]/) ? 'zh' : 'en';
1040
1054
  window.toggleLang=function(){
1041
1055
  curLang=curLang==='en'?'zh':'en';
1042
1056
  document.getElementById('langLabel').textContent=curLang==='en'?'中文':'EN';
@@ -1054,6 +1068,22 @@ body{background:#0a0e14;color:#dfe2eb;font-family:'Inter',sans-serif}
1054
1068
  });
1055
1069
  };
1056
1070
 
1071
+ // Apply initial language on load (auto-detect Chinese from analysis content)
1072
+ if(curLang==='zh'){
1073
+ document.getElementById('langLabel').textContent='EN';
1074
+ document.querySelectorAll('[data-i18n]').forEach(el=>{
1075
+ const key=el.getAttribute('data-i18n');
1076
+ if(key==='findings_range'){
1077
+ const range=el.getAttribute('data-range'),total=el.getAttribute('data-total');
1078
+ el.textContent='发现 — '+range+' / 共 '+total;
1079
+ } else if(i18n.zh[key]!=null)el.textContent=i18n.zh[key];
1080
+ });
1081
+ document.querySelectorAll('.finding-dim,.finding-text,.clean-dims,.rec-text').forEach(el=>{
1082
+ const t=el.getAttribute('data-zh');
1083
+ if(t)el.textContent=t;
1084
+ });
1085
+ }
1086
+
1057
1087
  // Dimension data for share card (must be before shareReport)
1058
1088
  const _dims=${JSON.stringify(Object.fromEntries(Object.entries(DIM_META).map(([k])=>[k,dimensions[k]||{score:null,na:false}])))};
1059
1089
 
@@ -1334,11 +1364,35 @@ body{background:#0a0e14;color:#dfe2eb;font-family:'Inter',sans-serif}
1334
1364
 
1335
1365
  const outPath = join(tmpdir(), `agentguard-checkup-${Date.now()}.html`);
1336
1366
  writeFileSync(outPath, html, 'utf8');
1337
- console.log(outPath);
1338
1367
 
1339
- const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
1340
- exec(`${cmd} "${outPath}"`, (err) => {
1341
- if (err) process.stderr.write(`Could not open browser: ${err.message}\n`);
1368
+ // Skip browser open for headless/bot environments (Qclaw, OpenClaw, CI)
1369
+ const isHeadless = process.env.OPENCLAW_STATE_DIR || process.env.QCLAW || process.env.CI;
1370
+
1371
+ // Flush stdout before doing anything else — on Windows/Linux in non-TTY/pipe
1372
+ // mode, console.log() is non-blocking and process.exit() can terminate before
1373
+ // the buffer is flushed, causing the caller (Claude) to receive an empty path.
1374
+ // Flush stdout before doing anything else — on Windows/Linux in non-TTY/pipe
1375
+ // mode, console.log() is non-blocking and process.exit() can terminate before
1376
+ // the buffer is flushed, causing the caller (Claude) to receive an empty path.
1377
+ process.stdout.write(outPath + '\n', () => {
1378
+ if (!isHeadless) {
1379
+ if (process.platform === 'win32') {
1380
+ // Use PowerShell Start-Process to open the file via Shell Execute API,
1381
+ // bypassing cmd.exe entirely — cmd /c start creates a visible intermediate
1382
+ // window whose title is the file path, which is the UX bug in #23.
1383
+ spawn('powershell', [
1384
+ '-NoProfile', '-WindowStyle', 'Hidden', '-Command',
1385
+ `Start-Process '${outPath.replace(/'/g, "''")}'`,
1386
+ ], { detached: true, stdio: 'ignore', windowsHide: true }).unref();
1387
+ } else {
1388
+ const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open';
1389
+ exec(`${cmd} "${outPath}"`, (err) => {
1390
+ if (err) process.stderr.write(`Could not open browser: ${err.message}\n`);
1391
+ });
1392
+ }
1393
+ }
1394
+ // Hard exit after 3s — guards against exec child process hanging and
1395
+ // blocking Node from exiting naturally (e.g. xdg-open on misconfigured Linux).
1396
+ setTimeout(() => process.exit(0), 3000).unref();
1342
1397
  });
1343
- setTimeout(() => process.exit(0), 2000);
1344
1398
  }