From 801ab17d2ad0c97a3b62fa39ef1dc967d8e16e10 Mon Sep 17 00:00:00 2001 From: Robin Thellend Date: Mon, 26 Oct 2026 09:25:17 -0700 Subject: [PATCH] Add update-and-audit-deps skill and update-go-version.sh script - Add scripts/update-go-version.sh to update the Go toolchain in Dockerfile, .goversion, and go/go.mod, upgrade both js/wasm and docker-tagged Go dependencies, and update CHANGELOG.md. - Add .agents/skills/update-and-audit-deps (SKILL.md and scripts/diff-go-deps.sh) adapted for sshterm and sshterm-server to extract and audit Go module and xterm npm package diffs before building or running tests. --- .agents/skills/update-and-audit-deps/SKILL.md | 99 ++++++++++ .../scripts/diff-go-deps.sh | 182 ++++++++++++++++++ scripts/update-go-version.sh | 45 +++++ 3 files changed, 326 insertions(+) create mode 100644 .agents/skills/update-and-audit-deps/SKILL.md create mode 100755 .agents/skills/update-and-audit-deps/scripts/diff-go-deps.sh create mode 100755 scripts/update-go-version.sh diff --git a/.agents/skills/update-and-audit-deps/SKILL.md b/.agents/skills/update-and-audit-deps/SKILL.md new file mode 100644 index 0000000..12a619d --- /dev/null +++ b/.agents/skills/update-and-audit-deps/SKILL.md @@ -0,0 +1,99 @@ +--- +name: update-and-audit-deps +description: >- + Runs scripts/update-go-version.sh (and optionally updates xterm npm + dependencies) to update the Go toolchain, Go module dependencies, and + frontend npm packages, then audits all upstream dependency changes for + supply-chain attacks, security vulnerabilities, regressions, or API contract + hazards that are exploitable in sshterm or sshterm-server. Use when the user + asks to update Go or npm/xterm dependencies, run update-go-version.sh, or + audit updated upstream libraries. +--- + +# Update and Audit Go & NPM Dependencies + +## Overview + +This skill updates the Go version and upstream module dependencies using [`scripts/update-go-version.sh`](../../../scripts/update-go-version.sh) (and optionally frontend npm dependencies in `xterm/`), extracts the exact source code diffs between the old and new versions from `GOMODCACHE` and npm registry tarballs, and audits every change for malicious/suspicious code, security vulnerabilities, or subtle behavioral changes before any build, bundle, or test command is executed. + +> [!CAUTION] +> **CRITICAL: Never run `go build`, `go test`, `go run`, `go generate`, `./build.sh`, `./go/build.sh`, `./xterm/update.sh`, `npx rollup`, or `npm` without `--ignore-scripts` BEFORE completing the full source diff audit in Step 3, and NEVER run them if suspicious or malicious code is detected in any dependency.** +> Compiling or testing untrusted Go dependency updates can execute attacker-controlled code on the host (via `init()` functions, test binaries, CGo, or compiler directives). Running npm without `--ignore-scripts` or bundling with `npx rollup` can execute attacker-controlled JavaScript on the host (via `preinstall`/`install`/`postinstall` hooks or bundler plugins). If suspicious code is found during Step 3, stop immediately, do not build or run tests, and report the finding to the user. + +## Workflow + +### 1. Check Working Tree & Update Dependencies + +1. Check `git status -s` in the repository root: + - Confirm what baseline git ref (`BASE_REF`, typically `HEAD`) holds the pre-update `go/go.mod` and `xterm/package-lock.json`. + - If `go/go.mod` has not yet been updated in the working tree, run: + ```bash + ./scripts/update-go-version.sh + ``` + - If npm/xterm dependencies in `xterm/` should also be updated (and have not yet been updated in the working tree), update `xterm/package-lock.json` using `--package-lock-only --ignore-scripts` (do **not** run `./xterm/update.sh` or `npx rollup` before Step 4): + ```bash + (cd xterm && npm update --package-lock-only --ignore-scripts) + ``` +2. Inspect `git diff Dockerfile .goversion go/go.mod CHANGELOG.md xterm/package.json xterm/package-lock.json` to see which Go version, Go modules, and npm packages were updated. + - Note: `scripts/update-go-version.sh` prepends a new `## next` block to `CHANGELOG.md` when the Go version or Go modules change. If `CHANGELOG.md` already had a `## next` section, merge the duplicate `## next` heading cleanly. + +### 2. Generate Upstream Source Diffs + +Run the helper script [`scripts/diff-go-deps.sh`](./scripts/diff-go-deps.sh) against the pre-update git ref (`HEAD` if changes are uncommitted, or `HEAD~1` if the update was already committed): + +```bash +./.agents/skills/update-and-audit-deps/scripts/diff-go-deps.sh HEAD /tmp/sshterm-dep-audit +``` + +- The script downloads both the previous and updated versions of each changed Go module in `go/go.mod` into `GOMODCACHE` and unpacks both the previous and updated tarballs of each changed npm package in `xterm/package-lock.json` without executing any scripts, locates every `.go` file in `go/` that imports each changed Go module, and writes: + - `/tmp/sshterm-dep-audit/summary.md`: Overview of all changed Go modules and npm packages, their unpacked source directories, importing files in this repo, install lifecycle script warnings, and diff line counts. + - `/tmp/sshterm-dep-audit/.diff`: Unified diff of non-test Go source files between the old and new versions. + - `/tmp/sshterm-dep-audit/npm_.diff`: Unified diff of unpacked npm package files between the old and new versions. +- Read `/tmp/sshterm-dep-audit/summary.md` first. + +### 3. Audit Upstream Changes and `sshterm` / `sshterm-server` Call Sites + +For each upgraded Go module or npm package (delegate to parallel `research` subagents if multiple large diffs exist): + +1. **Read the upstream diff** (`/tmp/sshterm-dep-audit/.diff` or `/tmp/sshterm-dep-audit/npm_.diff`) and the surrounding upstream code in `GOMODCACHE` or `/tmp/sshterm-dep-audit/npm/`. +2. **Read every call site** in `sshterm` (`go/internal/app/`, `go/internal/webauthnsk/`, `go/internal/terminal/`, `go/internal/websocket/`, `go/internal/jsutil/`, `go/internal/indexeddb/`, `go/internal/zmodem/`, `go/internal/shellwords/`, `go/internal/termsafe/`, `docroot/ssh.mjs`, `xterm/`) and `sshterm-server` (`go/sshterm-server/`) that interacts with the upgraded dependency. +3. **Evaluate against supply-chain threats and the `sshterm` / `sshterm-server` threat model**: + - **Supply-Chain & Suspicious Code Inspection (Must Pass Before Any Build/Test)**: + - **Go modules**: Check for newly added or modified `init()` functions, `os/exec` commands, `syscall` / `syscall/js` / `unsafe` / `reflect` abuse, `//go:linkname` or `//go:cgo_*` directives, CGo/assembly files, obfuscated strings or encoded payloads (base64/hex blobs), unexpected outbound network calls (`net`, `net/http`), environment variable access (`os.Getenv`, `os.Environ`), or unexpected filesystem reads/writes. + - **NPM packages**: Check for `preinstall` / `install` / `postinstall` / `prepare` scripts in `package.json`, `child_process` / `fs` / `net` / `http` / `https` / `fetch` / `WebSocket` / `eval` / `Function` calls, obfuscated or minified payloads, prototype pollution, DOM / `IndexedDB` / `localStorage` exfiltration (e.g., reading encrypted/decrypted SSH keys or database state in the browser), or Rollup/Babel plugin changes that inject unauthorized code into `docroot/xterm.mjs`. + - **If any suspicious, backdoored, or unexplained code is detected: STOP IMMEDIATELY. Do NOT proceed to Step 4 (`./build.sh`, `./xterm/update.sh`, `go test`, or `go build`).** Alert the user with the exact file path and line numbers. + - **Parsing, Terminal Emulation & Input Validation**: + - Can malformed network or peer input from an untrusted SSH/SFTP server or WebSocket peer (SSH handshake/kex/channel/agent packets in `golang.org/x/crypto/ssh`, SFTP packet headers/attributes/paths/symlinks in `github.com/pkg/sftp`, WebSocket frames in `github.com/gorilla/websocket`, CBOR/COSE WebAuthn attestation/assertion structures in `github.com/fxamacker/cbor/v2`, CLI flags/arguments in `github.com/urfave/cli/v2`, or ANSI/CSI/OSC/DCS escape sequences in `@xterm/xterm` and `golang.org/x/term`) trigger panics, uncaught JS exceptions (such as `RangeError` wedging `WriteBuffer` and deadlocking WASM writes), slice bounds errors, nil-pointer dereferences, or command/flag/path injection? + - Does any `@xterm/xterm` or `@xterm/addon-fit` change break or bypass the terminal hardening in `docroot/ssh.mjs` (`applyXtermWorkarounds` via `term._core.registerCsiHandler`, `csiRepGuard`, `csiLoopGuard`, `sanitizeTitle`, or capture-phase paste handler stripping `\x1b` and `\x9b`) or `go/internal/termsafe`? + - In `sshterm-server`, does any change affect `ParseEndpoint`, `checkOrigin`, WebSocket upgrade handling, static file serving, or Content-Security-Policy enforcement? + - **Cryptographic, SSH, Certificate & WebAuthn Verification**: + - Are SSH host key verification, host/user certificate checks (`checkCertificate`, `ValidAfter`/`ValidBefore`, `CertTimeInfinity`, CA domain/principal matching in `caTrustedFor`, rejection of `ssh-rsa` SHA-1 and `ssh-dss` signatures), RSA SHA-2 (`rsa-sha2-256` / `rsa-sha2-512`) signing in `dynSigner` and `agent.ExtendedAgent` (`SignWithFlags`), OpenSSH private key encryption, IndexedDB backup encryption (`pbkdf2` + `nacl/secretbox`), and WebAuthn `ecdsa-sk` COSE/CBOR validation and AES-GCM AAD key wrapping still strictly enforced? + - **Resource Exhaustion, Memory/Callback Leaks & DoS**: + - Can an untrusted SSH/SFTP server, ZMODEM sender, or WebSocket client cause unbounded memory allocation (e.g., pre-allocating buffers from untrusted length fields in `sftp` or `ssh`, or synchronous main-thread loops in `@xterm/xterm`)? + - Are WebSocket connections, SFTP streams/service-worker downloads, `syscall/js` `js.Func` callbacks, and `sshterm-server` TCP proxy connections (`HandshakeTimeout`, `tcpDeadlineWriter`, ping/pong watchdog) properly closed and released on early close, context cancellation, or error paths? + +### 4. Build, Test, and Report Findings + +> [!IMPORTANT] +> **Only run the build, bundle, and test commands below if Step 3 confirmed that no suspicious or malicious code was introduced in any dependency diff.** If suspicious code was detected, skip Step 4.1 entirely and report the suspicious code immediately. + +1. Run the bundle, build, vet, and test checks (only after all diffs pass inspection): + ```bash + # If xterm/package.json or xterm/package-lock.json changed, rebuild docroot/xterm.mjs: + ./xterm/update.sh + + # Build ssh.wasm, tests.wasm, and sshterm-server: + ./build.sh + + # Run go vet (WASM target) and unit tests: + (cd go && GOTOOLCHAIN="go$(cat ../.goversion)" GOOS=js GOARCH=wasm go vet ./...) + (cd go && GOTOOLCHAIN="go$(cat ../.goversion)" go test -count=1 ./...) + ``` + - If WASM, browser, or `@xterm/xterm` integration behavior was affected and Docker is available, also run `./tests/run-headless-tests.sh`. +2. Present a concise, triaged report of the audit: + - **Summary Table of Upgraded Dependencies**: Module/package name, ecosystem (Go / npm), old -> new version, and audit status. + - **Triaged Findings (Critical / High / Medium / Low)**: + - Exact upstream file and line numbers in `GOMODCACHE` or unpacked npm tarball + - Exact `sshterm` / `sshterm-server` call-site file and line numbers + - Exploitability preconditions and impact + - Recommended fix location (`sshterm`/`sshterm-server`, sibling `github.com/c2FmZQ/*` repository, or upstream third-party library) diff --git a/.agents/skills/update-and-audit-deps/scripts/diff-go-deps.sh b/.agents/skills/update-and-audit-deps/scripts/diff-go-deps.sh new file mode 100755 index 0000000..605bdf3 --- /dev/null +++ b/.agents/skills/update-and-audit-deps/scripts/diff-go-deps.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +# +# Compare go/go.mod and xterm/package-lock.json against a baseline git ref +# (default: HEAD) and generate source code diffs from GOMODCACHE and npm +# tarballs for all upgraded dependencies, along with their import call sites +# in sshterm and sshterm-server. +# +# Usage: +# ./diff-go-deps.sh [base-ref] [output-dir] + +set -euo pipefail + +BASE_REF="${1:-HEAD}" +OUT_DIR="${2:-$(mktemp -d /tmp/sshterm-dep-audit.XXXXXX)}" +mkdir -p "${OUT_DIR}" + +REPO_ROOT="$(git rev-parse --show-toplevel)" +cd "${REPO_ROOT}" + +if [[ -f "${REPO_ROOT}/.goversion" ]]; then + export GOTOOLCHAIN="go$(cat "${REPO_ROOT}/.goversion")" +fi + +parse_gomod() { + awk ' + /^require \(/ { in_req=1; next } + in_req && /^\)/ { in_req=0; next } + /^require [^ (]+ [^ (]+/ { print $2, $3; next } + in_req && NF >= 2 && $1 !~ /^\/\// { print $1, $2 } + ' +} + +OLD_MODS="${OUT_DIR}/old_mods.txt" +NEW_MODS="${OUT_DIR}/new_mods.txt" +SUMMARY="${OUT_DIR}/summary.md" + +git show "${BASE_REF}:go/go.mod" | parse_gomod | sort -k1,1 > "${OLD_MODS}" +parse_gomod < go/go.mod | sort -k1,1 > "${NEW_MODS}" + +echo "# Upgraded Dependencies (${BASE_REF} -> working tree)" > "${SUMMARY}" +echo "" >> "${SUMMARY}" +echo "Output directory: \`${OUT_DIR}\`" >> "${SUMMARY}" +echo "" >> "${SUMMARY}" +echo "## Go Modules (\`go/go.mod\`)" >> "${SUMMARY}" +echo "" >> "${SUMMARY}" + +UPGRADED_GO=0 + +while read -r mod old_ver new_ver; do + if [[ "${old_ver}" == "${new_ver}" ]]; then + continue + fi + UPGRADED_GO=$((UPGRADED_GO + 1)) + safe_name="$(echo "${mod}" | tr '/.' '__')" + diff_file="${OUT_DIR}/${safe_name}.diff" + + echo "### ${mod}: ${old_ver} -> ${new_ver}" >> "${SUMMARY}" + + old_dir="" + new_dir="" + if [[ "${old_ver}" != "(added)" ]]; then + old_dir="$(cd "${REPO_ROOT}/go" && go mod download -json "${mod}@${old_ver}" 2>/dev/null | jq -r '.Dir // empty')" + fi + if [[ "${new_ver}" != "(removed)" ]]; then + new_dir="$(cd "${REPO_ROOT}/go" && go mod download -json "${mod}@${new_ver}" 2>/dev/null | jq -r '.Dir // empty')" + fi + + echo "- Old Dir: \`${old_dir:-N/A}\`" >> "${SUMMARY}" + echo "- New Dir: \`${new_dir:-N/A}\`" >> "${SUMMARY}" + + # Find files in sshterm / sshterm-server that import this module or its subpackages. + echo "- Imported in repository files:" >> "${SUMMARY}" + matches="$(grep -rnE --include='*.go' "\"${mod}(\"|/)" go/ || true)" + if [[ -n "${matches}" ]]; then + echo "${matches}" | sed 's/^/ - `/' | sed 's/$/`/' >> "${SUMMARY}" + else + echo " - (transitive / indirect dependency)" >> "${SUMMARY}" + fi + + if [[ -n "${old_dir}" && -n "${new_dir}" && -d "${old_dir}" && -d "${new_dir}" ]]; then + diff -ruN \ + --exclude='*_test.go' \ + --exclude='.git*' \ + "${old_dir}" "${new_dir}" > "${diff_file}" || true + lines="$(wc -l < "${diff_file}")" + echo "- Non-test Go source diff: \`${diff_file}\` (${lines} lines)" >> "${SUMMARY}" + fi + echo "" >> "${SUMMARY}" +done < <( + join -a1 -a2 -e '(none)' -o '0,1.2,2.2' "${OLD_MODS}" "${NEW_MODS}" | + awk '$2 != $3 { + old = ($2 == "(none)" ? "(added)" : $2) + new = ($3 == "(none)" ? "(removed)" : $3) + print $1, old, new + }' +) + +if [[ "${UPGRADED_GO}" -eq 0 ]]; then + echo "No Go module version changes detected in \`go/go.mod\` relative to \`${BASE_REF}\`." >> "${SUMMARY}" + echo "" >> "${SUMMARY}" +fi + +echo "## NPM Packages (\`xterm/package-lock.json\`)" >> "${SUMMARY}" +echo "" >> "${SUMMARY}" + +UPGRADED_NPM=0 + +if git cat-file -e "${BASE_REF}:xterm/package-lock.json" 2>/dev/null && [[ -f xterm/package-lock.json ]]; then + while IFS=$'\t' read -r pkg_key pkg_name old_ver new_ver old_resolved new_resolved dep_tier; do + if [[ -z "${pkg_key}" ]]; then + continue + fi + UPGRADED_NPM=$((UPGRADED_NPM + 1)) + safe_name="$(echo "${pkg_key}" | tr '/@.' '___')" + diff_file="${OUT_DIR}/npm_${safe_name}.diff" + pkg_work_dir="${OUT_DIR}/npm/${safe_name}" + old_dir="" + new_dir="" + + echo "### ${pkg_name} (\`${pkg_key}\`, ${dep_tier}): ${old_ver} -> ${new_ver}" >> "${SUMMARY}" + + if [[ "${old_ver}" != "(added)" && "${old_resolved}" != "-" && -n "${old_resolved}" ]]; then + old_dir="${pkg_work_dir}/old" + mkdir -p "${old_dir}" + curl -fsSL "${old_resolved}" | tar -xz -C "${old_dir}" --strip-components=1 + fi + if [[ "${new_ver}" != "(removed)" && "${new_resolved}" != "-" && -n "${new_resolved}" ]]; then + new_dir="${pkg_work_dir}/new" + mkdir -p "${new_dir}" + curl -fsSL "${new_resolved}" | tar -xz -C "${new_dir}" --strip-components=1 + fi + + echo "- Old Dir: \`${old_dir:-N/A}\`" >> "${SUMMARY}" + echo "- New Dir: \`${new_dir:-N/A}\`" >> "${SUMMARY}" + + if [[ -n "${new_dir}" && -f "${new_dir}/package.json" ]]; then + lifecycle_scripts="$(jq -r '.scripts // {} | to_entries[] | select(.key | test("^(pre|post)?install$|^prepare$")) | "\(.key): \(.value)"' "${new_dir}/package.json" 2>/dev/null || true)" + if [[ -n "${lifecycle_scripts}" ]]; then + echo "- **WARNING - Install lifecycle scripts in \`package.json\`**:" >> "${SUMMARY}" + echo "${lifecycle_scripts}" | sed 's/^/ - `/' | sed 's/$/`/' >> "${SUMMARY}" + fi + fi + + if [[ -n "${old_dir}" && -n "${new_dir}" && -d "${old_dir}" && -d "${new_dir}" ]]; then + diff -ruN \ + --exclude='*.map' \ + --exclude='.git*' \ + "${old_dir}" "${new_dir}" > "${diff_file}" || true + lines="$(wc -l < "${diff_file}")" + echo "- Source diff: \`${diff_file}\` (${lines} lines)" >> "${SUMMARY}" + fi + echo "" >> "${SUMMARY}" + done < <( + jq -rn \ + --slurpfile old <(git show "${BASE_REF}:xterm/package-lock.json") \ + --slurpfile new xterm/package-lock.json ' + ($old[0].packages // {}) as $op | + ($new[0].packages // {}) as $np | + ([($op | keys[]), ($np | keys[])] | unique[]) as $k | + select($k != "") | + ($op[$k] // null) as $o | + ($np[$k] // null) as $n | + select(($o.version // "(added)") != ($n.version // "(removed)")) | + [ + $k, + ($k | sub("^.*node_modules/"; "")), + ($o.version // "(added)"), + ($n.version // "(removed)"), + ($o.resolved // "-"), + ($n.resolved // "-"), + (if ($n // $o).dev then "dev" else "prod" end) + ] | @tsv + ' + ) +fi + +if [[ "${UPGRADED_NPM}" -eq 0 ]]; then + echo "No npm package version changes detected in \`xterm/package-lock.json\` relative to \`${BASE_REF}\`." >> "${SUMMARY}" + echo "" >> "${SUMMARY}" +fi + +echo "Audit summary written to: ${SUMMARY} (${UPGRADED_GO} Go module(s), ${UPGRADED_NPM} npm package(s) changed)" diff --git a/scripts/update-go-version.sh b/scripts/update-go-version.sh new file mode 100755 index 0000000..60dfaff --- /dev/null +++ b/scripts/update-go-version.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# +# Check the latest Go version and update Dockerfile, .goversion, go/go.mod, and CHANGELOG.md. + +cd "$(dirname "$0")/.." + +latest=$(curl -s 'https://go.dev/dl/?mode=json' | jq -r '[.[] | select(.stable == true)][0].version') +version="${latest#go}" +osversion="alpine3.24" +if [[ "${latest}" =~ ^go ]]; then + sed -i -re "s/^FROM golang:.*/FROM golang:${version}-${osversion} AS build/" Dockerfile + echo "${version}" > .goversion + sed -i -re "s/^go .*/go ${version}/" go/go.mod +fi + +export GOTOOLCHAIN="go$(cat .goversion)" + +deps=$( + ( + cd go && + GOOS=js GOARCH=wasm go get -u -t ./... 2>&1 && + go get -u -t -tags docker ./... 2>&1 && + go mod tidy 2>&1 + ) | grep upgrade | sed -re 's/go: //g' | awk '!seen[$0]++' +) + +if [[ -n $(git status -s Dockerfile .goversion) || -n "${deps}" ]]; then + sed -n '1,2p' < CHANGELOG.md > CHANGELOG.md-new + echo '## next' >> CHANGELOG.md-new + echo >> CHANGELOG.md-new + echo '### :wrench: Misc' >> CHANGELOG.md-new + echo >> CHANGELOG.md-new + if [[ -n $(git status -s Dockerfile .goversion) ]]; then + echo "* Update go: ${version}" | tee -a CHANGELOG.md-new + fi + if [[ -n "${deps}" ]]; then + echo "* Update go dependencies:" | tee -a CHANGELOG.md-new + echo "${deps}" | sed -re 's/^/ * /g' | tee -a CHANGELOG.md-new + fi + echo >> CHANGELOG.md-new + sed -n '3,$p' < CHANGELOG.md >> CHANGELOG.md-new + mv CHANGELOG.md-new CHANGELOG.md +fi + +exit 0