Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,31 @@ jobs:
# same surface. shellcheck is pre-installed on ubuntu-latest.
run: shellcheck --shell=sh scripts/install.sh

- name: Shell — run the installer end to end
# shellcheck proves the script parses; this proves it installs. The
# script is what install.socket.dev/patch serves and what the README
# tells people to pipe into a shell, so "it downloads the latest
# release, verifies SHA256SUMS, and produces a binary that runs" is
# worth asserting on every PR rather than discovering from a user.
# Installs the LATEST RELEASE, not this checkout — on a version-bump PR
# that is deliberately the previous version.
run: |
sh scripts/install.sh
command -v socket-patch
socket-patch --version

- name: Shell — the installer URL is consistent across the docs
# The README, the script's own usage comment, and the hosting runbook
# all name the canonical URL. Keeping them in lockstep is the whole
# promise of install.socket.dev/patch being "a copy of this file".
run: |
for f in README.md scripts/install.sh docs/installer-hosting.md; do
if ! grep -qF 'https://install.socket.dev/patch' "$f"; then
echo "Error: $f no longer references https://install.socket.dev/patch" >&2
exit 1
fi
done

- name: Shell — shellcheck the release scripts
run: shellcheck scripts/version-sync.sh scripts/bump-version.sh scripts/release-lint.sh

Expand Down
97 changes: 97 additions & 0 deletions .github/workflows/installer-drift.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
name: Installer drift

# install.socket.dev/patch is supposed to be a byte-for-byte copy of
# scripts/install.sh — the README says so, and the whole point of hosting the
# installer on a Socket domain is that the bytes are auditable against this
# repository. Nothing enforces that at publish time from this side: the copy is
# published out of depscan's vendored `submodules/socket-patch` pin, so an
# installer change merged here is not live until that pin is bumped and depscan
# deploys (see docs/installer-hosting.md).
#
# This job is the watchdog for that gap. It is deliberately NOT part of CI: it
# checks a deployed artifact, not the diff, and a red run here means "go bump
# the pin", not "this PR is broken".
on:
schedule:
# Mondays, 07:00 UTC.
- cron: '0 7 * * 1'
workflow_dispatch:

permissions:
contents: read

jobs:
drift:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- name: Fetch the hosted installer
id: fetch
# Not `curl -f`: a non-200 body and its headers are the diagnostic.
run: |
url=https://install.socket.dev/patch
set +e
http=$(curl -sS -D headers.txt -o hosted-install.sh -w '%{http_code}' --max-time 30 "$url")
rc=$?
set -e

# curl exit 6 is "could not resolve host": the domain has not been
# stood up yet, so there is nothing to be in drift with. Report and
# pass, rather than being red from the day this workflow merges.
if [ "$rc" -eq 6 ]; then
echo "::notice::install.socket.dev does not resolve yet — skipping the drift check."
echo 'deployed=false' >> "$GITHUB_OUTPUT"
exit 0
fi

if [ "$rc" -ne 0 ]; then
echo "::error::curl exited $rc fetching $url"
exit 1
fi

if [ "$http" != '200' ]; then
echo "::error::$url returned HTTP $http"
# The failure mode this host is most exposed to: Cloudflare's bot
# challenge answers plain curl with a 403 and an HTML interstitial,
# which `curl | sh` would pipe straight into a shell.
if grep -qi '^cf-mitigated:' headers.txt; then
echo "::error::Cloudflare is challenging plain HTTP clients for install.socket.dev. The DNS record needs the same bot-challenge exemption patch.socket.dev has, or the documented one-liner feeds an HTML challenge page to sh."
fi
sed -n '1,40p' headers.txt
exit 1
fi

echo 'deployed=true' >> "$GITHUB_OUTPUT"

- name: Compare against scripts/install.sh
if: steps.fetch.outputs.deployed == 'true'
run: |
if ! diff -u scripts/install.sh hosted-install.sh; then
echo "::error::install.socket.dev/patch has drifted from scripts/install.sh. Fix: bump submodules/socket-patch in depscan to this commit and deploy — see docs/installer-hosting.md."
exit 1
fi
echo "install.socket.dev/patch matches scripts/install.sh"

- name: Check the hosted copy is a usable script
if: steps.fetch.outputs.deployed == 'true'
# Belt and braces: even with matching bytes, verify what is served is
# something a shell will accept. Catches a publish that mangled line
# endings or content-encoding in a way diff -u glosses over.
run: |
shellcheck --shell=sh hosted-install.sh
sh -n hosted-install.sh

- name: Check the published checksum
if: steps.fetch.outputs.deployed == 'true'
run: |
served=$(curl -fsSL --max-time 30 https://install.socket.dev/patch.sha256 | tr -d '[:space:]')
expected=$(sha256sum scripts/install.sh | awk '{print $1}')
if [ "$served" != "$expected" ]; then
echo "::error::install.socket.dev/patch.sha256 is $served, expected $expected"
exit 1
fi
echo "published checksum matches: $expected"
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,19 @@ into the new version's section — see docs/releasing.md.

### Changed

- **The documented one-liner installs from `https://install.socket.dev/patch`.**
The previous URL was `raw.githubusercontent.com`, which asks users to trust a
third-party CDN for a script they pipe into a shell and is the first URL a
locked-down egress policy blocks. The hosted copy is byte-for-byte
`scripts/install.sh`, with its SHA-256 published at
`install.socket.dev/patch.sha256`; the GitHub raw URL keeps working and serves
the same bytes. Binaries are still downloaded from the GitHub release and
verified against its `SHA256SUMS` — the trust model is unchanged, only the
script's origin moved. New: `docs/installer-hosting.md` (how the copy is
published), a CI step that runs the installer end to end instead of only
linting it, and an `installer-drift` workflow that checks the hosted copy
against this repository weekly.

- **Release workflow consolidated into a single `release.yml`.** One
dispatch now publishes every ecosystem package — crates.io, npm, PyPI,
RubyGems (both gems, via OIDC trusted publishing), Packagist, Maven
Expand Down
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,22 @@ CVEs you've already fixed.
One-line install (macOS / Linux):

```bash
curl -fsSL https://raw.githubusercontent.com/SocketDev/socket-patch/main/scripts/install.sh | sh
curl -fsSL https://install.socket.dev/patch | sh
```

Detects your platform (macOS/Linux, x64/ARM64), downloads the latest binary, and installs
to `/usr/local/bin` or `~/.local/bin`. Use `sudo sh` instead of `sh` if `/usr/local/bin`
requires root.
Detects your platform (macOS/Linux, x64/ARM64), downloads the latest binary, verifies it
against the release's `SHA256SUMS`, and installs to `/usr/local/bin` or `~/.local/bin`.
Use `sudo sh` instead of `sh` if `/usr/local/bin` requires root. Pin a version with
`SOCKET_PATCH_VERSION=3.3.0 sh` instead of plain `sh`.

`install.socket.dev` serves a copy of [`scripts/install.sh`](scripts/install.sh) from
this repository — read it before you run it, either there or at
[install.socket.dev/patch](https://install.socket.dev/patch). If you would rather not
depend on the Socket domain, `curl -fsSL
https://raw.githubusercontent.com/SocketDev/socket-patch/main/scripts/install.sh | sh`
does the same thing from the same bytes. See
[docs/installer-hosting.md](docs/installer-hosting.md) for how the hosted copy is
published.

On Windows, install via npm or the dotnet tool (below), or grab a prebuilt
`socket-patch-*-pc-windows-msvc.zip` from the
Expand Down
91 changes: 91 additions & 0 deletions docs/installer-hosting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Hosting the installer at install.socket.dev

The documented one-liner is

```sh
curl -fsSL https://install.socket.dev/patch | sh
```

`install.socket.dev/patch` serves a **byte-for-byte copy of
[`scripts/install.sh`](../scripts/install.sh)** — not a rendered template, not a
different script. The README says so, so it has to stay true.

## Why a Socket domain

The one-liner used to point at `raw.githubusercontent.com`. That asks a user to
trust a third-party CDN for a script they pipe into a shell, and it is the first
URL a locked-down egress policy blocks. `install.socket.dev` is a name Socket
controls, already inside the trust boundary a customer grants `socket.dev`, and
it stays stable if the artifacts ever move.

The GitHub URL still works and still serves the same bytes. Anyone who would
rather not add a dependency on the Socket domain can keep using it.

## What the trust model actually is

Unchanged by the hosting move, and worth being precise about:

- **The script** is fetched over HTTPS from a Socket-controlled host. Its SHA-256
is published alongside it at `install.socket.dev/patch.sha256`, and it can be
diffed against `scripts/install.sh` in this repo.
- **The binary** is fetched from the GitHub release and verified against that
release's `SHA256SUMS` before it is unpacked. Neither the script nor the
checksums are signed — this is checksum integrity rooted in HTTPS plus GitHub,
the same model `--update` and the gem/composer launchers use (see
[CLI_CONTRACT.md](../crates/socket-patch-cli/CLI_CONTRACT.md)).
- Nothing in the install path sends a Socket API token anywhere.

Hosting the script on a Socket domain moves *who serves the script*. It does not
add a signature, and the docs should not imply that it does.

## How a change to the installer reaches the domain

The publish path lives in [depscan][depscan], which vendors this repository as
`submodules/socket-patch`:

1. A change to `scripts/install.sh` merges **here**.
2. depscan's `submodules/socket-patch` pin is bumped to that commit.
3. depscan's prod deploy runs its **Publish install.socket.dev site** step,
which copies `submodules/socket-patch/scripts/install.sh` to
`gs://socket-install-prod/patch`, publishes its sha256 and the landing page,
then re-reads the object and fails the deploy if the bytes do not match.
4. `install-server` (a `gcs-bucket-server` instance, `tanka/lib/depscan/install-server.libsonnet`)
serves that bucket at `install.socket.dev`.

So an installer change needs a depscan submodule bump plus a deploy. That
indirection is deliberate: this repository is public and needs no write
credentials into a Socket bucket, and a submodule bump is a reviewed change, so
nothing reaches a `curl | sh` endpoint without review on the depscan side too.

**A new socket-patch release needs none of this.** The script resolves the latest
release itself at run time (`/releases/latest/download`), so cutting 3.4.0
changes what the hosted installer *installs* without changing the hosted
installer. Only edits to the script itself require a publish.

## The drift check

`.github/workflows/installer-drift.yml` (weekly, plus `workflow_dispatch`)
fetches `install.socket.dev/patch` and diffs it against `scripts/install.sh` on
`main`.

- **Different** → the job fails. The fix is a depscan submodule bump + deploy
(steps 2–3 above). Expect this to be red in the window between merging an
installer change here and bumping the pin there.
- **Host does not resolve** → the job reports "not deployed yet" and passes, so
the check is inert until the domain exists.

The check also runs `shellcheck` and `sh -n` against the *fetched* copy, so a
mangled publish is caught even when the hash somehow matches expectations.

## Known gaps

- **No Windows installer.** The script is POSIX `sh`; native Windows users go
through a package manager or a release archive. A `patch.ps1` object on the
same host would be the natural addition — the hosting side already supports
it, nothing here does yet.
- **Objects must stay flat.** `gcs-bucket-server` interpolates the object name
into the GCS JSON API URL unencoded, so only bucket-root keys resolve
(`patch`, `patch.sha256`, `index.html`). A nested path like
`/patch/3.3.0/install.sh` would 404 until that is fixed on the depscan side.

[depscan]: https://github.com/SocketDev/depscan
8 changes: 6 additions & 2 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ set -eu

# Socket Patch installer
# Usage:
# curl -fsSL https://raw.githubusercontent.com/SocketDev/socket-patch/main/scripts/install.sh | sh
# curl -fsSL https://install.socket.dev/patch | sh
#
# install.socket.dev/patch serves a byte-for-byte copy of this file; the URL
# above and the raw.githubusercontent.com path to this script are
# interchangeable. See docs/installer-hosting.md for how the copy is published.
#
# Override the version that gets installed by exporting SOCKET_PATCH_VERSION:
# curl -fsSL .../install.sh | SOCKET_PATCH_VERSION=3.0.0 sh
# curl -fsSL https://install.socket.dev/patch | SOCKET_PATCH_VERSION=3.0.0 sh

REPO="SocketDev/socket-patch"
BINARY="socket-patch"
Expand Down
Loading