mohuclaw 1.0.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.
Files changed (92) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +64 -0
  3. package/bin/mohu-tui.js +73 -0
  4. package/bin/mohu-webui.js +67 -0
  5. package/dist/tui/tui.js +38733 -0
  6. package/dist/webui/index.html +1551 -0
  7. package/dist/webui/server.js +876 -0
  8. package/ioc/c2-ips.txt +25 -0
  9. package/ioc/file-hashes.txt +13 -0
  10. package/ioc/malicious-domains.txt +46 -0
  11. package/ioc/malicious-hashes.txt +5 -0
  12. package/ioc/malicious-publishers.txt +34 -0
  13. package/ioc/malicious-skill-patterns.txt +87 -0
  14. package/package.json +46 -0
  15. package/scripts/check/access_control.sh +183 -0
  16. package/scripts/check/credential_storage.sh +222 -0
  17. package/scripts/check/execution_sandbox.sh +502 -0
  18. package/scripts/check/memory_poisoning.sh +334 -0
  19. package/scripts/check/network_exposure.sh +479 -0
  20. package/scripts/check/resource_cost.sh +182 -0
  21. package/scripts/check/supply_chain.sh +553 -0
  22. package/scripts/repair/access_control/_common.sh +249 -0
  23. package/scripts/repair/access_control/check_1.sh +28 -0
  24. package/scripts/repair/access_control/check_2.sh +27 -0
  25. package/scripts/repair/access_control/check_3.sh +23 -0
  26. package/scripts/repair/access_control/check_4.sh +23 -0
  27. package/scripts/repair/access_control/check_5.sh +20 -0
  28. package/scripts/repair/credential_storage/_common.sh +277 -0
  29. package/scripts/repair/credential_storage/check_1.sh +47 -0
  30. package/scripts/repair/credential_storage/check_2.sh +35 -0
  31. package/scripts/repair/credential_storage/check_3.sh +53 -0
  32. package/scripts/repair/credential_storage/logs/security-scan.log +15 -0
  33. package/scripts/repair/execution_sandbox/_common.sh +302 -0
  34. package/scripts/repair/execution_sandbox/check_1.sh +67 -0
  35. package/scripts/repair/execution_sandbox/check_10.sh +23 -0
  36. package/scripts/repair/execution_sandbox/check_11.sh +34 -0
  37. package/scripts/repair/execution_sandbox/check_12.sh +38 -0
  38. package/scripts/repair/execution_sandbox/check_13.sh +29 -0
  39. package/scripts/repair/execution_sandbox/check_2.sh +46 -0
  40. package/scripts/repair/execution_sandbox/check_3.sh +37 -0
  41. package/scripts/repair/execution_sandbox/check_4.sh +23 -0
  42. package/scripts/repair/execution_sandbox/check_5.sh +28 -0
  43. package/scripts/repair/execution_sandbox/check_6.sh +17 -0
  44. package/scripts/repair/execution_sandbox/check_7.sh +17 -0
  45. package/scripts/repair/execution_sandbox/check_8.sh +17 -0
  46. package/scripts/repair/execution_sandbox/check_9.sh +17 -0
  47. package/scripts/repair/execution_sandbox/logs/security-scan.log +10 -0
  48. package/scripts/repair/memory_poisoning/_common.sh +336 -0
  49. package/scripts/repair/memory_poisoning/check_1.sh +51 -0
  50. package/scripts/repair/memory_poisoning/check_2.sh +26 -0
  51. package/scripts/repair/memory_poisoning/check_3.sh +24 -0
  52. package/scripts/repair/memory_poisoning/check_4.sh +27 -0
  53. package/scripts/repair/memory_poisoning/check_5.sh +20 -0
  54. package/scripts/repair/network_exposure/_common.sh +330 -0
  55. package/scripts/repair/network_exposure/check_1.sh +86 -0
  56. package/scripts/repair/network_exposure/check_10.sh +16 -0
  57. package/scripts/repair/network_exposure/check_11.sh +31 -0
  58. package/scripts/repair/network_exposure/check_12.sh +24 -0
  59. package/scripts/repair/network_exposure/check_2.sh +26 -0
  60. package/scripts/repair/network_exposure/check_3.sh +43 -0
  61. package/scripts/repair/network_exposure/check_4.sh +23 -0
  62. package/scripts/repair/network_exposure/check_5.sh +16 -0
  63. package/scripts/repair/network_exposure/check_6.sh +98 -0
  64. package/scripts/repair/network_exposure/check_7.sh +35 -0
  65. package/scripts/repair/network_exposure/check_8.sh +19 -0
  66. package/scripts/repair/network_exposure/check_9.sh +19 -0
  67. package/scripts/repair/resource_cost/_common.sh +303 -0
  68. package/scripts/repair/resource_cost/check_1.sh +16 -0
  69. package/scripts/repair/resource_cost/check_2.sh +16 -0
  70. package/scripts/repair/resource_cost/check_3.sh +23 -0
  71. package/scripts/repair/supply_chain/_common.sh +222 -0
  72. package/scripts/repair/supply_chain/check_1.sh +95 -0
  73. package/scripts/repair/supply_chain/check_10.sh +60 -0
  74. package/scripts/repair/supply_chain/check_11.sh +63 -0
  75. package/scripts/repair/supply_chain/check_12.sh +36 -0
  76. package/scripts/repair/supply_chain/check_13.sh +44 -0
  77. package/scripts/repair/supply_chain/check_14.sh +33 -0
  78. package/scripts/repair/supply_chain/check_15.sh +33 -0
  79. package/scripts/repair/supply_chain/check_16.sh +34 -0
  80. package/scripts/repair/supply_chain/check_17.sh +61 -0
  81. package/scripts/repair/supply_chain/check_18.sh +62 -0
  82. package/scripts/repair/supply_chain/check_2.sh +93 -0
  83. package/scripts/repair/supply_chain/check_3.sh +78 -0
  84. package/scripts/repair/supply_chain/check_4.sh +72 -0
  85. package/scripts/repair/supply_chain/check_5.sh +73 -0
  86. package/scripts/repair/supply_chain/check_6.sh +81 -0
  87. package/scripts/repair/supply_chain/check_7.sh +52 -0
  88. package/scripts/repair/supply_chain/check_8.sh +71 -0
  89. package/scripts/repair/supply_chain/check_9.sh +78 -0
  90. package/scripts/repair/supply_chain/logs/security-scan.log +77 -0
  91. package/scripts/scan.sh +228 -0
  92. package/webui/index.html +1551 -0
@@ -0,0 +1,35 @@
1
+ #!/bin/bash
2
+ # CHECK 7 (origin 20): mDNS/Bonjour Exposure
3
+ # Usage: ./check_7.sh
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
6
+ source "$SCRIPT_DIR/_common.sh"
7
+
8
+ # Guidance
9
+ cat <<EOF
10
+ RECOMMENDED ACTIONS:
11
+ 1. Disable mDNS broadcasting:
12
+ openclaw config set discovery.mdns.mode off
13
+
14
+ 2. Or set to minimal (name only, no paths):
15
+ openclaw config set discovery.mdns.mode minimal
16
+
17
+ 3. Verify mDNS is no longer broadcasting:
18
+ avahi-browse -a
19
+ # Should show no openclaw entries
20
+
21
+ 4. Disable avahi-daemon entirely (if not needed):
22
+ sudo systemctl stop avahi-daemon
23
+ sudo systemctl disable avahi-daemon
24
+
25
+ 5. Or restrict avahi to localhost only (/etc/avahi/avahi-daemon.conf):
26
+ [server]
27
+ allow-interfaces=lo
28
+ deny-interfaces=eth0,wlan0
29
+
30
+ Then restart:
31
+ sudo systemctl restart avahi-daemon
32
+
33
+ EOF
34
+
35
+ exit 0
@@ -0,0 +1,19 @@
1
+ #!/bin/bash
2
+ # CHECK 8 (origin 34): SSRF Protection
3
+ # Usage: ./check_8.sh
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
6
+ source "$SCRIPT_DIR/_common.sh"
7
+
8
+ # Guidance
9
+ cat <<EOF
10
+ RECOMMENDED ACTIONS:
11
+ 1. Update OpenClaw:
12
+ openclaw update
13
+
14
+ 2. Audit cron webhook targets for internal endpoints:
15
+ openclaw config get cron
16
+
17
+ EOF
18
+
19
+ exit 0
@@ -0,0 +1,19 @@
1
+ #!/bin/bash
2
+ # CHECK 9 (origin 36): ACP Permission Auto-Approval
3
+ # Usage: ./check_9.sh
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
6
+ source "$SCRIPT_DIR/_common.sh"
7
+
8
+ # Guidance
9
+ cat <<EOF
10
+ RECOMMENDED ACTIONS:
11
+ 1. Update OpenClaw:
12
+ openclaw update
13
+
14
+ 2. Disable ACP auto-approve until patched:
15
+ openclaw config set acp.autoApprove false
16
+
17
+ EOF
18
+
19
+ exit 0
@@ -0,0 +1,303 @@
1
+ #!/bin/bash
2
+ # Exit codes: 0=SECURE, 1=WARNINGS, 2=COMPROMISED
3
+
4
+ set -uo pipefail
5
+
6
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
7
+ # PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
8
+ # IOC_DIR="$PROJECT_DIR/ioc"
9
+ PROJECT_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)"
10
+ IOC_DIR="$PROJECT_DIR/ioc"
11
+ SELF_DIR_NAME="$(basename "$PROJECT_DIR")"
12
+
13
+ # Default OpenClaw paths
14
+ # OPENCLAW_DIR="${OPENCLAW_HOME:-$HOME/.openclaw}"
15
+ OPENCLAW_DIR="${OPENCLAW_HOME:-$HOME/test/openclaw-1}"
16
+ SKILLS_DIR="$OPENCLAW_DIR/workspace/skills"
17
+ WORKSPACE_DIR="$OPENCLAW_DIR/workspace"
18
+ # LOG_DIR="$OPENCLAW_DIR/logs"
19
+ LOG_DIR="logs"
20
+ LOG_FILE="$LOG_DIR/security-scan.log"
21
+ TIMESTAMP="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
22
+
23
+ export PATH="$HOME/.local/bin:/opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"
24
+
25
+ # Overall counters
26
+ CRITICAL=0
27
+ WARNINGS=0
28
+ CLEAN=0
29
+
30
+ # Per-category counters
31
+ CATEGORY_NAME=""
32
+ CATEGORY_TOTAL_CHECKS=0
33
+ CATEGORY_CRITICAL=0
34
+ CATEGORY_WARNINGS=0
35
+ CATEGORY_CLEAN=0
36
+
37
+ mkdir -p "$LOG_DIR"
38
+
39
+ log() {
40
+ echo "$1" | tee -a "$LOG_FILE"
41
+ }
42
+
43
+ header() {
44
+ # Usage: header <new_index> <message>
45
+ log ""
46
+ log "[$1/$CATEGORY_TOTAL_CHECKS] $2"
47
+ }
48
+
49
+ result_clean() {
50
+ log "CLEAN: $1"
51
+ CLEAN=$((CLEAN + 1))
52
+ CATEGORY_CLEAN=$((CATEGORY_CLEAN + 1))
53
+ }
54
+
55
+ result_warn() {
56
+ log "WARNING: $1"
57
+ WARNINGS=$((WARNINGS + 1))
58
+ CATEGORY_WARNINGS=$((CATEGORY_WARNINGS + 1))
59
+ }
60
+
61
+ result_critical() {
62
+ log "CRITICAL: $1"
63
+ CRITICAL=$((CRITICAL + 1))
64
+ CATEGORY_CRITICAL=$((CATEGORY_CRITICAL + 1))
65
+ }
66
+
67
+ category_start() {
68
+ # Usage: category_start <name> <total_checks>
69
+ CATEGORY_NAME="$1"
70
+ CATEGORY_TOTAL_CHECKS="$2"
71
+ CATEGORY_CRITICAL=0
72
+ CATEGORY_WARNINGS=0
73
+ CATEGORY_CLEAN=0
74
+
75
+ log ""
76
+ log "----------------------------------------"
77
+ log "CATEGORY START: $CATEGORY_NAME ($CATEGORY_TOTAL_CHECKS checks)"
78
+ log "----------------------------------------"
79
+ }
80
+
81
+ category_end() {
82
+ log ""
83
+ log "CATEGORY SUMMARY: $CATEGORY_NAME"
84
+ log " critical: $CATEGORY_CRITICAL"
85
+ log " warning : $CATEGORY_WARNINGS"
86
+ log " clean : $CATEGORY_CLEAN"
87
+ log "----------------------------------------"
88
+ }
89
+
90
+ # Use timeout if available (macOS may only have gtimeout via coreutils)
91
+ TIMEOUT_BIN=""
92
+ if command -v timeout >/dev/null 2>&1; then
93
+ TIMEOUT_BIN="timeout"
94
+ elif command -v gtimeout >/dev/null 2>&1; then
95
+ TIMEOUT_BIN="gtimeout"
96
+ fi
97
+
98
+ run_with_timeout() {
99
+ local secs="$1"
100
+ shift
101
+
102
+ if [ -n "$TIMEOUT_BIN" ]; then
103
+ "$TIMEOUT_BIN" "$secs" "$@"
104
+ elif command -v python3 >/dev/null 2>&1; then
105
+ python3 - "$secs" "$@" <<'PY'
106
+ import subprocess
107
+ import sys
108
+
109
+ def main():
110
+ try:
111
+ secs = float(sys.argv[1])
112
+ except Exception:
113
+ secs = 0
114
+
115
+ cmd = sys.argv[2:]
116
+ if not cmd:
117
+ sys.exit(1)
118
+
119
+ try:
120
+ proc = subprocess.Popen(cmd)
121
+ try:
122
+ proc.wait(timeout=secs if secs > 0 else None)
123
+ except subprocess.TimeoutExpired:
124
+ proc.kill()
125
+ proc.wait()
126
+ sys.exit(124)
127
+ sys.exit(proc.returncode if proc.returncode is not None else 0)
128
+ except FileNotFoundError:
129
+ sys.exit(127)
130
+
131
+ if __name__ == "__main__":
132
+ main()
133
+ PY
134
+ else
135
+ "$@"
136
+ fi
137
+ }
138
+
139
+ # Load IOC data
140
+ load_ips() {
141
+ if [ -f "$IOC_DIR/c2-ips.txt" ]; then
142
+ grep -v '^#' "$IOC_DIR/c2-ips.txt" | grep -v '^$' | cut -d'|' -f1
143
+ else
144
+ echo "91.92.242"
145
+ fi
146
+ }
147
+
148
+ load_domains() {
149
+ if [ -f "$IOC_DIR/malicious-domains.txt" ]; then
150
+ grep -v '^#' "$IOC_DIR/malicious-domains.txt" | grep -v '^$' | cut -d'|' -f1
151
+ else
152
+ echo "webhook.site"
153
+ fi
154
+ }
155
+
156
+ sha256_file() {
157
+ # Cross-platform SHA-256 for a file
158
+ local target="$1"
159
+ [ -f "$target" ] || return 1
160
+
161
+ if command -v sha256sum >/dev/null 2>&1; then
162
+ sha256sum "$target" 2>/dev/null | awk '{print tolower($1)}'
163
+ elif command -v shasum >/dev/null 2>&1; then
164
+ shasum -a 256 "$target" 2>/dev/null | awk '{print tolower($1)}'
165
+ elif command -v openssl >/dev/null 2>&1; then
166
+ openssl dgst -sha256 "$target" 2>/dev/null | sed -E 's/^.*= //' | tr '[:upper:]' '[:lower:]'
167
+ else
168
+ return 1
169
+ fi
170
+ }
171
+
172
+ load_hash_iocs() {
173
+ # Expected format: <sha256>|<campaign>
174
+ # Similar to c2-ips.txt storage style
175
+ if [ -f "$IOC_DIR/malicious-hashes.txt" ]; then
176
+ grep -v '^#' "$IOC_DIR/malicious-hashes.txt" | grep -v '^$' || true
177
+ fi
178
+ }
179
+
180
+ lookup_malicious_hash_campaign() {
181
+ local needle
182
+ needle="$(printf "%s" "${1:-}" | tr '[:upper:]' '[:lower:]')"
183
+ [ -n "$needle" ] || return 1
184
+
185
+ while IFS='|' read -r hash_val campaign rest; do
186
+ hash_val="$(printf "%s" "$hash_val" | tr '[:upper:]' '[:lower:]')"
187
+ if [ "$hash_val" = "$needle" ]; then
188
+ printf "%s\n" "${campaign:-unknown}"
189
+ return 0
190
+ fi
191
+ done < <(load_hash_iocs) # ← 进程替换,不产生子shell
192
+
193
+ return 1
194
+ }
195
+
196
+ extract_github_account_age_days() {
197
+ # Best-effort local metadata extraction only.
198
+ # We cannot reliably infer GitHub account age from filesystem alone unless
199
+ # the skill metadata explicitly records it.
200
+ local skill_dir="$1"
201
+ local val=""
202
+
203
+ for meta in "$skill_dir/package.json" "$skill_dir/config.json" "$skill_dir/SKILL.md"; do
204
+ [ -f "$meta" ] || continue
205
+
206
+ # JSON-ish keys
207
+ val="$(grep -iEo '"(githubAccountAge|github_account_age|accountAgeDays|githubAccountAgeDays)"[[:space:]]*:[[:space:]]*[0-9]+' "$meta" 2>/dev/null | head -1 | grep -oE '[0-9]+' || true)"
208
+ if [ -n "$val" ]; then
209
+ printf "%s\n" "$val"
210
+ return 0
211
+ fi
212
+
213
+ # YAML / markdown frontmatter-ish keys
214
+ val="$(grep -iE '^(githubAccountAge|github_account_age|accountAgeDays|githubAccountAgeDays)[[:space:]]*:[[:space:]]*[0-9]+' "$meta" 2>/dev/null | head -1 | grep -oE '[0-9]+' || true)"
215
+ if [ -n "$val" ]; then
216
+ printf "%s\n" "$val"
217
+ return 0
218
+ fi
219
+ done
220
+
221
+ return 1
222
+ }
223
+
224
+
225
+ OPENCLAW_PRESENT=false
226
+ if command -v openclaw >/dev/null 2>&1; then
227
+ OPENCLAW_PRESENT=true
228
+ fi
229
+
230
+ OC_VERSION="unknown"
231
+ PARSED_OC_VERSION=""
232
+ OC_MAJOR=""
233
+ OC_MINOR=""
234
+ OC_PATCH=""
235
+
236
+ if [ "$OPENCLAW_PRESENT" = true ]; then
237
+ OC_VERSION="$(run_with_timeout 5 openclaw --version 2>/dev/null || echo "unknown")"
238
+ PARSED_OC_VERSION="$(echo "$OC_VERSION" | grep -oE '[0-9]{4}\.[0-9]+\.[0-9]+' | head -1 || true)"
239
+
240
+ if [ -n "$PARSED_OC_VERSION" ]; then
241
+ OC_MAJOR="$(echo "$PARSED_OC_VERSION" | cut -d'.' -f1)"
242
+ OC_MINOR="$(echo "$PARSED_OC_VERSION" | cut -d'.' -f2)"
243
+ OC_PATCH="$(echo "$PARSED_OC_VERSION" | cut -d'.' -f3)"
244
+ fi
245
+ fi
246
+
247
+ version_ge() {
248
+
249
+ local t_major="$1"
250
+ local t_minor="$2"
251
+ local t_patch="$3"
252
+
253
+ if [ -z "$PARSED_OC_VERSION" ]; then
254
+ return 1
255
+ fi
256
+
257
+ if [ "$OC_MAJOR" -gt "$t_major" ] 2>/dev/null; then
258
+ return 0
259
+ fi
260
+ if [ "$OC_MAJOR" -lt "$t_major" ] 2>/dev/null; then
261
+ return 1
262
+ fi
263
+
264
+ if [ "$OC_MINOR" -gt "$t_minor" ] 2>/dev/null; then
265
+ return 0
266
+ fi
267
+ if [ "$OC_MINOR" -lt "$t_minor" ] 2>/dev/null; then
268
+ return 1
269
+ fi
270
+
271
+ if [ "$OC_PATCH" -ge "$t_patch" ] 2>/dev/null; then
272
+ return 0
273
+ fi
274
+
275
+ return 1
276
+ }
277
+
278
+ version_lt() {
279
+
280
+ if [ -z "$PARSED_OC_VERSION" ]; then
281
+ return 1
282
+ fi
283
+
284
+ if version_ge "$1" "$2" "$3"; then
285
+ return 1
286
+ fi
287
+
288
+ return 0
289
+ }
290
+
291
+ # ------------------------------------------------------------
292
+ # Cron frequency check
293
+ # ------------------------------------------------------------
294
+
295
+ has_high_freq_cron() {
296
+
297
+ grep -Eq '^[[:space:]]*(\*[[:space:]]+\*[[:space:]]+\*[[:space:]]+\*[[:space:]]+\*|\*/[1-4][[:space:]]+\*[[:space:]]+\*[[:space:]]+\*[[:space:]]+\*)' "$1" 2>/dev/null
298
+
299
+ }
300
+
301
+ TOTAL_TOKENS=0
302
+ TOTAL_COST=0
303
+ HOURLY_COST=0
@@ -0,0 +1,16 @@
1
+ #!/bin/bash
2
+ # CHECK 1: Webhook DoS
3
+ # Usage: ./check_1.sh
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
6
+ source "$SCRIPT_DIR/_common.sh"
7
+
8
+ # Guidance
9
+ cat <<EOF
10
+ RECOMMENDED ACTIONS:
11
+ 1. Update OpenClaw:
12
+ openclaw update
13
+
14
+ EOF
15
+
16
+ exit 0
@@ -0,0 +1,16 @@
1
+ #!/bin/bash
2
+ # CHECK 2: fetchWithGuard DoS
3
+ # Usage: ./check_2.sh
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
6
+ source "$SCRIPT_DIR/_common.sh"
7
+
8
+ # Guidance
9
+ cat <<EOF
10
+ RECOMMENDED ACTIONS:
11
+ 1. Update OpenClaw:
12
+ openclaw update
13
+
14
+ EOF
15
+
16
+ exit 0
@@ -0,0 +1,23 @@
1
+ #!/bin/bash
2
+ # CHECK 3: High frequency cron
3
+ # Usage: ./check_3.sh
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
6
+ source "$SCRIPT_DIR/_common.sh"
7
+
8
+ # Guidance
9
+ cat <<EOF
10
+ RECOMMENDED ACTIONS:
11
+ 1. Review current crontab schedules:
12
+ crontab -l | grep -iE "openclaw|clawdbot|moltbot"
13
+
14
+ 2. Edit and reduce trigger frequency (no more than once per 5 minutes):
15
+ crontab -e
16
+ # Change */1 to */5 or higher
17
+
18
+ 3. Consider switching to event-driven triggers instead of fixed schedules:
19
+ openclaw config set scheduler.mode event-driven
20
+
21
+ EOF
22
+
23
+ exit 0
@@ -0,0 +1,222 @@
1
+ #!/bin/bash
2
+ # Exit codes: 0=SECURE, 1=WARNINGS, 2=COMPROMISED
3
+
4
+ set -uo pipefail
5
+
6
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
7
+ # PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
8
+ # IOC_DIR="$PROJECT_DIR/ioc"
9
+ PROJECT_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)"
10
+ IOC_DIR="$PROJECT_DIR/ioc"
11
+ SELF_DIR_NAME="$(basename "$PROJECT_DIR")"
12
+
13
+ # Default OpenClaw paths
14
+ # OPENCLAW_DIR="${OPENCLAW_HOME:-$HOME/.openclaw}"
15
+ OPENCLAW_DIR="${OPENCLAW_HOME:-$HOME/test/openclaw-1}"
16
+ SKILLS_DIR="$OPENCLAW_DIR/workspace/skills"
17
+ WORKSPACE_DIR="$OPENCLAW_DIR/workspace"
18
+ # LOG_DIR="$OPENCLAW_DIR/logs"
19
+ LOG_DIR="logs"
20
+ LOG_FILE="$LOG_DIR/security-scan.log"
21
+ TIMESTAMP="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
22
+
23
+ export PATH="$HOME/.local/bin:/opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"
24
+
25
+ # Overall counters
26
+ CRITICAL=0
27
+ WARNINGS=0
28
+ CLEAN=0
29
+
30
+ # Per-category counters
31
+ CATEGORY_NAME=""
32
+ CATEGORY_TOTAL_CHECKS=0
33
+ CATEGORY_CRITICAL=0
34
+ CATEGORY_WARNINGS=0
35
+ CATEGORY_CLEAN=0
36
+
37
+ mkdir -p "$LOG_DIR"
38
+
39
+ log() {
40
+ echo "$1" | tee -a "$LOG_FILE"
41
+ }
42
+
43
+ header() {
44
+ # Usage: header <new_index> <message>
45
+ log ""
46
+ log "[$1/$CATEGORY_TOTAL_CHECKS] $2"
47
+ }
48
+
49
+ result_clean() {
50
+ log "CLEAN: $1"
51
+ CLEAN=$((CLEAN + 1))
52
+ CATEGORY_CLEAN=$((CATEGORY_CLEAN + 1))
53
+ }
54
+
55
+ result_warn() {
56
+ log "WARNING: $1"
57
+ WARNINGS=$((WARNINGS + 1))
58
+ CATEGORY_WARNINGS=$((CATEGORY_WARNINGS + 1))
59
+ }
60
+
61
+ result_critical() {
62
+ log "CRITICAL: $1"
63
+ CRITICAL=$((CRITICAL + 1))
64
+ CATEGORY_CRITICAL=$((CATEGORY_CRITICAL + 1))
65
+ }
66
+
67
+ category_start() {
68
+ # Usage: category_start <name> <total_checks>
69
+ CATEGORY_NAME="$1"
70
+ CATEGORY_TOTAL_CHECKS="$2"
71
+ CATEGORY_CRITICAL=0
72
+ CATEGORY_WARNINGS=0
73
+ CATEGORY_CLEAN=0
74
+
75
+ log ""
76
+ log "----------------------------------------"
77
+ log "CATEGORY START: $CATEGORY_NAME ($CATEGORY_TOTAL_CHECKS checks)"
78
+ log "----------------------------------------"
79
+ }
80
+
81
+ category_end() {
82
+ log ""
83
+ log "CATEGORY SUMMARY: $CATEGORY_NAME"
84
+ log " critical: $CATEGORY_CRITICAL"
85
+ log " warning : $CATEGORY_WARNINGS"
86
+ log " clean : $CATEGORY_CLEAN"
87
+ log "----------------------------------------"
88
+ }
89
+
90
+ # Use timeout if available (macOS may only have gtimeout via coreutils)
91
+ TIMEOUT_BIN=""
92
+ if command -v timeout >/dev/null 2>&1; then
93
+ TIMEOUT_BIN="timeout"
94
+ elif command -v gtimeout >/dev/null 2>&1; then
95
+ TIMEOUT_BIN="gtimeout"
96
+ fi
97
+
98
+ run_with_timeout() {
99
+ local secs="$1"
100
+ shift
101
+
102
+ if [ -n "$TIMEOUT_BIN" ]; then
103
+ "$TIMEOUT_BIN" "$secs" "$@"
104
+ elif command -v python3 >/dev/null 2>&1; then
105
+ python3 - "$secs" "$@" <<'PY'
106
+ import subprocess
107
+ import sys
108
+
109
+ def main():
110
+ try:
111
+ secs = float(sys.argv[1])
112
+ except Exception:
113
+ secs = 0
114
+
115
+ cmd = sys.argv[2:]
116
+ if not cmd:
117
+ sys.exit(1)
118
+
119
+ try:
120
+ proc = subprocess.Popen(cmd)
121
+ try:
122
+ proc.wait(timeout=secs if secs > 0 else None)
123
+ except subprocess.TimeoutExpired:
124
+ proc.kill()
125
+ proc.wait()
126
+ sys.exit(124)
127
+ sys.exit(proc.returncode if proc.returncode is not None else 0)
128
+ except FileNotFoundError:
129
+ sys.exit(127)
130
+
131
+ if __name__ == "__main__":
132
+ main()
133
+ PY
134
+ else
135
+ "$@"
136
+ fi
137
+ }
138
+
139
+ # Load IOC data
140
+ load_ips() {
141
+ if [ -f "$IOC_DIR/c2-ips.txt" ]; then
142
+ grep -v '^#' "$IOC_DIR/c2-ips.txt" | grep -v '^$' | cut -d'|' -f1
143
+ else
144
+ echo "91.92.242"
145
+ fi
146
+ }
147
+
148
+ load_domains() {
149
+ if [ -f "$IOC_DIR/malicious-domains.txt" ]; then
150
+ grep -v '^#' "$IOC_DIR/malicious-domains.txt" | grep -v '^$' | cut -d'|' -f1
151
+ else
152
+ echo "webhook.site"
153
+ fi
154
+ }
155
+
156
+ sha256_file() {
157
+ # Cross-platform SHA-256 for a file
158
+ local target="$1"
159
+ [ -f "$target" ] || return 1
160
+
161
+ if command -v sha256sum >/dev/null 2>&1; then
162
+ sha256sum "$target" 2>/dev/null | awk '{print tolower($1)}'
163
+ elif command -v shasum >/dev/null 2>&1; then
164
+ shasum -a 256 "$target" 2>/dev/null | awk '{print tolower($1)}'
165
+ elif command -v openssl >/dev/null 2>&1; then
166
+ openssl dgst -sha256 "$target" 2>/dev/null | sed -E 's/^.*= //' | tr '[:upper:]' '[:lower:]'
167
+ else
168
+ return 1
169
+ fi
170
+ }
171
+
172
+ load_hash_iocs() {
173
+ # Expected format: <sha256>|<campaign>
174
+ # Similar to c2-ips.txt storage style
175
+ if [ -f "$IOC_DIR/malicious-hashes.txt" ]; then
176
+ grep -v '^#' "$IOC_DIR/malicious-hashes.txt" | grep -v '^$' || true
177
+ fi
178
+ }
179
+
180
+ lookup_malicious_hash_campaign() {
181
+ local needle
182
+ needle="$(printf "%s" "${1:-}" | tr '[:upper:]' '[:lower:]')"
183
+ [ -n "$needle" ] || return 1
184
+
185
+ while IFS='|' read -r hash_val campaign rest; do
186
+ hash_val="$(printf "%s" "$hash_val" | tr '[:upper:]' '[:lower:]')"
187
+ if [ "$hash_val" = "$needle" ]; then
188
+ printf "%s\n" "${campaign:-unknown}"
189
+ return 0
190
+ fi
191
+ done < <(load_hash_iocs) # ← 进程替换,不产生子shell
192
+
193
+ return 1
194
+ }
195
+
196
+ extract_github_account_age_days() {
197
+ # Best-effort local metadata extraction only.
198
+ # We cannot reliably infer GitHub account age from filesystem alone unless
199
+ # the skill metadata explicitly records it.
200
+ local skill_dir="$1"
201
+ local val=""
202
+
203
+ for meta in "$skill_dir/package.json" "$skill_dir/config.json" "$skill_dir/SKILL.md"; do
204
+ [ -f "$meta" ] || continue
205
+
206
+ # JSON-ish keys
207
+ val="$(grep -iEo '"(githubAccountAge|github_account_age|accountAgeDays|githubAccountAgeDays)"[[:space:]]*:[[:space:]]*[0-9]+' "$meta" 2>/dev/null | head -1 | grep -oE '[0-9]+' || true)"
208
+ if [ -n "$val" ]; then
209
+ printf "%s\n" "$val"
210
+ return 0
211
+ fi
212
+
213
+ # YAML / markdown frontmatter-ish keys
214
+ val="$(grep -iE '^(githubAccountAge|github_account_age|accountAgeDays|githubAccountAgeDays)[[:space:]]*:[[:space:]]*[0-9]+' "$meta" 2>/dev/null | head -1 | grep -oE '[0-9]+' || true)"
215
+ if [ -n "$val" ]; then
216
+ printf "%s\n" "$val"
217
+ return 0
218
+ fi
219
+ done
220
+
221
+ return 1
222
+ }