@wipcomputer/wip-ldm-os 0.4.85-alpha.3 → 0.4.85-alpha.4

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.
@@ -0,0 +1,205 @@
1
+ #!/usr/bin/env bash
2
+ # audit-logs.sh: F-007 pre-fix log audit for the VPS hosted-mcp surface.
3
+ #
4
+ # Read-only. No log mutation, no rotation, no clearing. Greps nginx and
5
+ # PM2 logs for bearer/ticket/api-key shaped values that may have leaked
6
+ # during the period when the WS upgrade accepted ?token=ck- and the
7
+ # phone app sent ck values in URLs.
8
+ #
9
+ # Output: per-source match count, plus up to N redacted sample lines per
10
+ # source so you can see which path / agent / time range is affected
11
+ # without ever printing the leaked secret in this terminal session.
12
+ #
13
+ # If any source shows a non-zero count, treat the matching ck values as
14
+ # already-leaked. Rotate them as part of the F-002 + F-005a deploy
15
+ # (see ai/product/bugs/security/2026-04-28--cc-mini--vps-hosted-mcp-audit.md).
16
+ #
17
+ # Usage on the VPS:
18
+ # bash audit-logs.sh
19
+ # bash audit-logs.sh --days 14 # widen log window for rotated logs
20
+ # bash audit-logs.sh --samples 20 # show more redacted samples
21
+ # bash audit-logs.sh --json # machine-readable output (counts only)
22
+ #
23
+ # Source: src/hosted-mcp/scripts/audit-logs.sh
24
+
25
+ set -euo pipefail
26
+
27
+ # ── Defaults ────────────────────────────────────────────────────────
28
+
29
+ DAYS=7
30
+ SAMPLES=10
31
+ JSON=0
32
+ NGINX_LOG_DIR=/var/log/nginx
33
+ PM2_APP=mcp-server
34
+
35
+ # ── Args ────────────────────────────────────────────────────────────
36
+
37
+ while [ $# -gt 0 ]; do
38
+ case "$1" in
39
+ --days) DAYS=$2; shift 2 ;;
40
+ --samples) SAMPLES=$2; shift 2 ;;
41
+ --json) JSON=1; shift 1 ;;
42
+ --nginx-log-dir) NGINX_LOG_DIR=$2; shift 2 ;;
43
+ --pm2-app) PM2_APP=$2; shift 2 ;;
44
+ -h|--help)
45
+ sed -n '2,30p' "$0"
46
+ exit 0
47
+ ;;
48
+ *) echo "Unknown arg: $1" >&2; exit 2 ;;
49
+ esac
50
+ done
51
+
52
+ # ── Patterns ────────────────────────────────────────────────────────
53
+ #
54
+ # Three families of leak we are looking for. Each is a single ERE that
55
+ # `grep -E` will accept. We match conservatively (\b, exact prefixes)
56
+ # so we do not over-flag random bytes that happen to resemble a key.
57
+
58
+ PAT_QUERY_TOKEN='[?&](token|ticket|api_key|access_token)=[^[:space:]&"]+'
59
+ PAT_CK_KEY='\bck-[A-Za-z0-9_-]{6,}\b'
60
+ PAT_AUTH_BEARER='Authorization:[[:space:]]*Bearer[[:space:]]+ck-[A-Za-z0-9_-]{6,}'
61
+
62
+ # ── Redaction (for sample output only) ──────────────────────────────
63
+ #
64
+ # Replace the matched value with a marker before we print the sample
65
+ # line. This way the audit's own output never reproduces the leaked
66
+ # secret in the operator's terminal/scrollback.
67
+
68
+ redact() {
69
+ sed -E \
70
+ -e 's/([?&](token|ticket|api_key|access_token)=)[^&" \t]+/\1[REDACTED]/g' \
71
+ -e 's/\bck-[A-Za-z0-9_-]+/[REDACTED ck-]/g'
72
+ }
73
+
74
+ # ── Source inventory ────────────────────────────────────────────────
75
+
76
+ declare -a SOURCES
77
+ declare -A SOURCE_LABEL
78
+
79
+ add_source() {
80
+ local label=$1
81
+ local path=$2
82
+ if [ -e "$path" ] || compgen -G "$path" > /dev/null; then
83
+ SOURCES+=("$path")
84
+ SOURCE_LABEL["$path"]=$label
85
+ fi
86
+ }
87
+
88
+ add_source "nginx access (current)" "${NGINX_LOG_DIR}/access.log"
89
+ add_source "nginx error (current)" "${NGINX_LOG_DIR}/error.log"
90
+ add_source "nginx wip.computer.access" "${NGINX_LOG_DIR}/wip.computer.access.log"
91
+ add_source "nginx wip.computer.error" "${NGINX_LOG_DIR}/wip.computer.error.log"
92
+
93
+ # ── Counters ────────────────────────────────────────────────────────
94
+
95
+ total_matches=0
96
+ declare -A counts
97
+
98
+ scan_one() {
99
+ local path=$1
100
+ local label=$2
101
+ local count
102
+ if [[ "$path" == *.gz ]]; then
103
+ count=$(sudo zgrep -E "$PAT_QUERY_TOKEN|$PAT_CK_KEY|$PAT_AUTH_BEARER" "$path" 2>/dev/null | wc -l | tr -d ' ')
104
+ else
105
+ count=$(sudo grep -E "$PAT_QUERY_TOKEN|$PAT_CK_KEY|$PAT_AUTH_BEARER" "$path" 2>/dev/null | wc -l | tr -d ' ')
106
+ fi
107
+ counts["$path"]=$count
108
+ total_matches=$(( total_matches + count ))
109
+ }
110
+
111
+ print_samples() {
112
+ local path=$1
113
+ local label=$2
114
+ local count=${counts["$path"]}
115
+ [ "$count" -eq 0 ] && return
116
+ echo
117
+ echo "── $label ($path)"
118
+ echo " matches: $count"
119
+ echo " sample (redacted, up to $SAMPLES lines):"
120
+ if [[ "$path" == *.gz ]]; then
121
+ sudo zgrep -E "$PAT_QUERY_TOKEN|$PAT_CK_KEY|$PAT_AUTH_BEARER" "$path" 2>/dev/null \
122
+ | head -n "$SAMPLES" | redact | sed 's/^/ /'
123
+ else
124
+ sudo grep -E "$PAT_QUERY_TOKEN|$PAT_CK_KEY|$PAT_AUTH_BEARER" "$path" 2>/dev/null \
125
+ | head -n "$SAMPLES" | redact | sed 's/^/ /'
126
+ fi
127
+ }
128
+
129
+ # ── Run nginx scan ──────────────────────────────────────────────────
130
+
131
+ if [ "$JSON" -ne 1 ]; then
132
+ echo "F-007 log audit"
133
+ echo "Days back (rotated): $DAYS"
134
+ echo "Samples per source: $SAMPLES"
135
+ echo "Nginx log dir: $NGINX_LOG_DIR"
136
+ echo
137
+ fi
138
+
139
+ # Current logs
140
+ for path in "${SOURCES[@]}"; do
141
+ scan_one "$path" "${SOURCE_LABEL[$path]}"
142
+ done
143
+
144
+ # Rotated logs within the window
145
+ shopt -s nullglob
146
+ declare -a ROTATED
147
+ for f in "${NGINX_LOG_DIR}"/*.gz "${NGINX_LOG_DIR}"/*.[0-9]*; do
148
+ if [ -f "$f" ]; then
149
+ if find "$f" -mtime "-${DAYS}" -print -quit | grep -q .; then
150
+ ROTATED+=("$f")
151
+ fi
152
+ fi
153
+ done
154
+
155
+ for path in "${ROTATED[@]}"; do
156
+ scan_one "$path" "rotated $(basename "$path")"
157
+ done
158
+
159
+ # ── PM2 ─────────────────────────────────────────────────────────────
160
+ #
161
+ # pm2 logs --nostream prints recent lines from the in-memory buffer.
162
+ # This is best-effort: it covers the current PM2 process lifetime, not
163
+ # rotated copies. PM2's persisted logs (if any) live under
164
+ # ~/.pm2/logs/<app>-out.log and ~/.pm2/logs/<app>-error.log.
165
+
166
+ PM2_OUT="$HOME/.pm2/logs/${PM2_APP}-out.log"
167
+ PM2_ERR="$HOME/.pm2/logs/${PM2_APP}-error.log"
168
+
169
+ if [ -e "$PM2_OUT" ]; then add_source "pm2 ${PM2_APP} out" "$PM2_OUT"; scan_one "$PM2_OUT" "pm2 out"; fi
170
+ if [ -e "$PM2_ERR" ]; then add_source "pm2 ${PM2_APP} error" "$PM2_ERR"; scan_one "$PM2_ERR" "pm2 error"; fi
171
+
172
+ # ── Output ──────────────────────────────────────────────────────────
173
+
174
+ if [ "$JSON" -eq 1 ]; then
175
+ printf '{"total_matches": %d, "by_source": {' "$total_matches"
176
+ first=1
177
+ for path in "${!counts[@]}"; do
178
+ [ "$first" -eq 1 ] && first=0 || printf ','
179
+ printf '"%s": %d' "$path" "${counts[$path]}"
180
+ done
181
+ printf '}}\n'
182
+ else
183
+ echo "── Summary"
184
+ for path in "${!counts[@]}"; do
185
+ label=${SOURCE_LABEL[$path]:-${path}}
186
+ printf " %-40s %s matches\n" "$label" "${counts[$path]}"
187
+ done
188
+ echo
189
+ echo "── Total matches: $total_matches"
190
+
191
+ for path in "${!counts[@]}"; do
192
+ print_samples "$path" "${SOURCE_LABEL[$path]:-${path}}"
193
+ done
194
+
195
+ echo
196
+ if [ "$total_matches" -gt 0 ]; then
197
+ echo "ACTION REQUIRED:"
198
+ echo " Treat any matched ck values as leaked. Rotate them as part of the"
199
+ echo " F-002 + F-005a deploy. After deploying redact-logs.conf, re-run"
200
+ echo " this script and confirm zero matches in the post-rotation window."
201
+ else
202
+ echo "OK: no leak signatures found in the scanned window."
203
+ echo " Re-run after every deploy that adds or moves auth surfaces."
204
+ fi
205
+ fi
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env bash
2
+ # verify-deploy.sh: Verify a hosted-mcp deploy manifest against the live
3
+ # VPS. Reads a manifest produced by deploy.sh, fetches the current
4
+ # sha256 of each declared destination on the remote, and compares.
5
+ #
6
+ # Per F-009 of the VPS hosted-mcp audit. Use after deploy.sh, after
7
+ # any apparent change to the live tree, or as part of the pre-dogfood
8
+ # smoke test.
9
+ #
10
+ # Usage:
11
+ # bash verify-deploy.sh wip.computer:/var/www/.../<timestamp>.json
12
+ # bash verify-deploy.sh /path/to/local/manifest.json --remote wip.computer
13
+ # bash verify-deploy.sh latest # use latest manifest on VPS
14
+ #
15
+ # Exits non-zero on any mismatch. Output lists each file as OK or DIFF.
16
+
17
+ set -euo pipefail
18
+
19
+ REMOTE="wip.computer"
20
+ MANIFEST_DIR="/var/www/wip.computer/deploy-manifests/hosted-mcp"
21
+ ARG=""
22
+
23
+ while [ $# -gt 0 ]; do
24
+ case "$1" in
25
+ --remote) REMOTE=$2; shift 2 ;;
26
+ -h|--help) sed -n '2,18p' "$0"; exit 0 ;;
27
+ *) ARG=$1; shift ;;
28
+ esac
29
+ done
30
+
31
+ if [ -z "$ARG" ]; then
32
+ echo "usage: bash verify-deploy.sh <manifest-path-or-latest>" >&2
33
+ exit 2
34
+ fi
35
+
36
+ # Resolve manifest path.
37
+ if [ "$ARG" = "latest" ]; then
38
+ MANIFEST_PATH=$(ssh "${REMOTE}" "ls -t ${MANIFEST_DIR}/*.json 2>/dev/null | head -1")
39
+ if [ -z "$MANIFEST_PATH" ]; then
40
+ echo "FATAL: no manifests found in ${REMOTE}:${MANIFEST_DIR}" >&2
41
+ exit 1
42
+ fi
43
+ echo "Latest manifest: ${MANIFEST_PATH}"
44
+ MANIFEST_JSON=$(ssh "${REMOTE}" "cat ${MANIFEST_PATH}")
45
+ elif [[ "$ARG" == *":"* ]]; then
46
+ # remote:path form
47
+ MANIFEST_REMOTE=${ARG%%:*}
48
+ MANIFEST_PATH=${ARG#*:}
49
+ MANIFEST_JSON=$(ssh "${MANIFEST_REMOTE}" "cat ${MANIFEST_PATH}")
50
+ elif [ -f "$ARG" ]; then
51
+ MANIFEST_PATH=$ARG
52
+ MANIFEST_JSON=$(cat "$ARG")
53
+ else
54
+ echo "FATAL: manifest not found: ${ARG}" >&2
55
+ exit 1
56
+ fi
57
+
58
+ # Parse files[] entries via python3.
59
+ if ! command -v python3 >/dev/null 2>&1; then
60
+ echo "FATAL: python3 required" >&2
61
+ exit 1
62
+ fi
63
+
64
+ ENTRIES=$(printf '%s' "$MANIFEST_JSON" | python3 -c '
65
+ import json,sys
66
+ m=json.load(sys.stdin)
67
+ for f in m.get("files",[]):
68
+ print(f.get("source","")+"\t"+f.get("destination","")+"\t"+f.get("sha256",""))
69
+ ')
70
+
71
+ if [ -z "$ENTRIES" ]; then
72
+ echo "FATAL: no files entries in manifest" >&2
73
+ exit 1
74
+ fi
75
+
76
+ echo "Manifest: ${MANIFEST_PATH}"
77
+ echo "Verifying ${REMOTE}:"
78
+ echo
79
+
80
+ OK=0
81
+ FAIL=0
82
+ while IFS=$'\t' read -r src dst expected; do
83
+ [ -z "$dst" ] && continue
84
+ # ssh -n redirects stdin from /dev/null so ssh does not consume the
85
+ # loop's heredoc and short-circuit after the first iteration.
86
+ remote_sha=$(ssh -n "${REMOTE}" "sudo sha256sum ${dst} 2>/dev/null" | awk '{print $1}' || echo "")
87
+ if [ -z "$remote_sha" ]; then
88
+ printf " MISSING %s\n" "$dst"
89
+ FAIL=$((FAIL+1))
90
+ elif [ "$remote_sha" = "$expected" ]; then
91
+ printf " OK %s\n" "$dst"
92
+ OK=$((OK+1))
93
+ else
94
+ printf " DIFF %s\n expected %s\n live %s\n" "$dst" "$expected" "$remote_sha"
95
+ FAIL=$((FAIL+1))
96
+ fi
97
+ done <<< "$ENTRIES"
98
+
99
+ echo
100
+ echo "Summary: ${OK} ok, ${FAIL} mismatched"
101
+ [ "$FAIL" -eq 0 ] && exit 0
102
+ exit 1