mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Merge e976d100f3 into 9453c8c0c9
This commit is contained in:
commit
4f18395e93
3 changed files with 306 additions and 0 deletions
181
.github/workflows/build-and-release.yml
vendored
181
.github/workflows/build-and-release.yml
vendored
|
|
@ -130,6 +130,187 @@ jobs:
|
||||||
release-artifacts/uninstall-endpoint-mac.sh \
|
release-artifacts/uninstall-endpoint-mac.sh \
|
||||||
release-artifacts/uninstall-endpoint-windows.ps1
|
release-artifacts/uninstall-endpoint-windows.ps1
|
||||||
|
|
||||||
|
publish-homebrew:
|
||||||
|
name: Update Homebrew tap
|
||||||
|
if: github.event_name == 'release' && !github.event.release.prerelease
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Extract SHA256 checksums from the released install script
|
||||||
|
env:
|
||||||
|
VERSION: ${{ github.event.release.tag_name }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# The release pipeline already bakes per-platform SHA256s into
|
||||||
|
# install-safe-chain.sh. Parse them out instead of re-downloading
|
||||||
|
# ~200MB of binaries just to recompute the same hashes.
|
||||||
|
curl -fsSL --retry 3 \
|
||||||
|
"https://github.com/AikidoSec/safe-chain/releases/download/${VERSION}/install-safe-chain.sh" \
|
||||||
|
-o install-safe-chain.sh
|
||||||
|
|
||||||
|
extract_sha() {
|
||||||
|
grep -E "^${1}=\"[a-f0-9]+\"" install-safe-chain.sh \
|
||||||
|
| head -n1 \
|
||||||
|
| sed -E "s/^${1}=\"([a-f0-9]+)\"/\1/"
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "SHA_MACOS_X64=$(extract_sha SHA256_MACOS_X64)"
|
||||||
|
echo "SHA_MACOS_ARM64=$(extract_sha SHA256_MACOS_ARM64)"
|
||||||
|
echo "SHA_LINUX_X64=$(extract_sha SHA256_LINUX_X64)"
|
||||||
|
echo "SHA_LINUX_ARM64=$(extract_sha SHA256_LINUX_ARM64)"
|
||||||
|
} >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Verify all checksums were extracted
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
missing=()
|
||||||
|
for var in SHA_MACOS_X64 SHA_MACOS_ARM64 SHA_LINUX_X64 SHA_LINUX_ARM64; do
|
||||||
|
if [ -z "${!var:-}" ]; then
|
||||||
|
missing+=("$var")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ ${#missing[@]} -gt 0 ]; then
|
||||||
|
echo "::error::Could not extract checksum(s) from install-safe-chain.sh: ${missing[*]}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Checkout Homebrew tap
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: AikidoSec/homebrew-tap
|
||||||
|
path: homebrew-tap
|
||||||
|
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
||||||
|
persist-credentials: true
|
||||||
|
|
||||||
|
- name: Render formula from template
|
||||||
|
env:
|
||||||
|
VERSION: ${{ github.event.release.tag_name }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
mkdir -p homebrew-tap/Formula
|
||||||
|
|
||||||
|
# Quoted heredoc preserves Ruby `#{version}` interpolation and the
|
||||||
|
# backticks inside the caveats string. Placeholders are substituted
|
||||||
|
# with sed below, after the heredoc is written.
|
||||||
|
cat > homebrew-tap/Formula/safe-chain.rb <<'RUBY'
|
||||||
|
# typed: strict
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# This file is auto-generated by AikidoSec/safe-chain's release
|
||||||
|
# workflow. Do not edit by hand — changes will be overwritten on
|
||||||
|
# the next release.
|
||||||
|
#
|
||||||
|
# Aikido Safe Chain wraps your package managers (npm, yarn, pnpm,
|
||||||
|
# pip, poetry, uv, ...) and blocks installations of known-malicious
|
||||||
|
# packages using the Aikido Intel feed.
|
||||||
|
class SafeChain < Formula
|
||||||
|
desc "Block malicious packages from npm, yarn, pnpm, pip, poetry and uv"
|
||||||
|
homepage "https://github.com/AikidoSec/safe-chain"
|
||||||
|
version "__VERSION__"
|
||||||
|
license "AGPL-3.0-or-later"
|
||||||
|
|
||||||
|
livecheck do
|
||||||
|
url :stable
|
||||||
|
strategy :github_latest
|
||||||
|
end
|
||||||
|
|
||||||
|
on_macos do
|
||||||
|
on_arm do
|
||||||
|
url "https://github.com/AikidoSec/safe-chain/releases/download/#{version}/safe-chain-macos-arm64",
|
||||||
|
using: :nounzip
|
||||||
|
sha256 "__SHA_MACOS_ARM64__"
|
||||||
|
end
|
||||||
|
on_intel do
|
||||||
|
url "https://github.com/AikidoSec/safe-chain/releases/download/#{version}/safe-chain-macos-x64",
|
||||||
|
using: :nounzip
|
||||||
|
sha256 "__SHA_MACOS_X64__"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
on_linux do
|
||||||
|
on_arm do
|
||||||
|
url "https://github.com/AikidoSec/safe-chain/releases/download/#{version}/safe-chain-linux-arm64",
|
||||||
|
using: :nounzip
|
||||||
|
sha256 "__SHA_LINUX_ARM64__"
|
||||||
|
end
|
||||||
|
on_intel do
|
||||||
|
url "https://github.com/AikidoSec/safe-chain/releases/download/#{version}/safe-chain-linux-x64",
|
||||||
|
using: :nounzip
|
||||||
|
sha256 "__SHA_LINUX_X64__"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def install
|
||||||
|
binary_name = if OS.mac?
|
||||||
|
Hardware::CPU.arm? ? "safe-chain-macos-arm64" : "safe-chain-macos-x64"
|
||||||
|
else
|
||||||
|
Hardware::CPU.arm? ? "safe-chain-linux-arm64" : "safe-chain-linux-x64"
|
||||||
|
end
|
||||||
|
|
||||||
|
chmod 0755, binary_name
|
||||||
|
bin.install binary_name => "safe-chain"
|
||||||
|
end
|
||||||
|
|
||||||
|
def caveats
|
||||||
|
<<~CAVEATS
|
||||||
|
Aikido Safe Chain works by wrapping your package managers (npm, yarn,
|
||||||
|
pnpm, pip, poetry, uv, ...) with shell aliases. After installing, run:
|
||||||
|
|
||||||
|
safe-chain setup
|
||||||
|
|
||||||
|
For CI/non-interactive environments, use:
|
||||||
|
|
||||||
|
safe-chain setup-ci
|
||||||
|
|
||||||
|
Then restart your shell (or `source ~/.zshrc` / `~/.bashrc`) so the
|
||||||
|
new aliases are picked up.
|
||||||
|
CAVEATS
|
||||||
|
end
|
||||||
|
|
||||||
|
test do
|
||||||
|
assert_match version.to_s, shell_output("#{bin}/safe-chain --version")
|
||||||
|
assert_match "safe-chain setup", shell_output("#{bin}/safe-chain help")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
RUBY
|
||||||
|
|
||||||
|
sed -i \
|
||||||
|
-e "s|__VERSION__|${VERSION}|g" \
|
||||||
|
-e "s|__SHA_MACOS_ARM64__|${SHA_MACOS_ARM64}|g" \
|
||||||
|
-e "s|__SHA_MACOS_X64__|${SHA_MACOS_X64}|g" \
|
||||||
|
-e "s|__SHA_LINUX_ARM64__|${SHA_LINUX_ARM64}|g" \
|
||||||
|
-e "s|__SHA_LINUX_X64__|${SHA_LINUX_X64}|g" \
|
||||||
|
homebrew-tap/Formula/safe-chain.rb
|
||||||
|
|
||||||
|
# Fail fast if any placeholder wasn't substituted (e.g. a new
|
||||||
|
# __XXX__ added to the template without a matching sed rule).
|
||||||
|
if grep -q "__[A-Z_]*__" homebrew-tap/Formula/safe-chain.rb; then
|
||||||
|
echo "::error::Unsubstituted placeholder(s) remain in the formula:"
|
||||||
|
grep -n "__[A-Z_]*__" homebrew-tap/Formula/safe-chain.rb
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Commit and push formula update
|
||||||
|
working-directory: homebrew-tap
|
||||||
|
env:
|
||||||
|
VERSION: ${{ github.event.release.tag_name }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
git add Formula/safe-chain.rb
|
||||||
|
|
||||||
|
if git diff --staged --quiet; then
|
||||||
|
echo "No formula changes to commit."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
git commit -m "safe-chain ${VERSION}"
|
||||||
|
git push
|
||||||
|
|
||||||
publish-npm:
|
publish-npm:
|
||||||
name: Publish to npm
|
name: Publish to npm
|
||||||
if: github.event_name == 'release'
|
if: github.event_name == 'release'
|
||||||
|
|
|
||||||
11
README.md
11
README.md
|
|
@ -57,6 +57,17 @@ curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/inst
|
||||||
iex (iwr "https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.ps1" -UseBasicParsing)
|
iex (iwr "https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.ps1" -UseBasicParsing)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### macOS / Linux (Homebrew)
|
||||||
|
|
||||||
|
If you'd prefer a package manager over piping a script from the internet, install Safe Chain via our [Homebrew tap](https://github.com/AikidoSec/homebrew-tap):
|
||||||
|
|
||||||
|
```shell
|
||||||
|
brew install AikidoSec/tap/safe-chain
|
||||||
|
safe-chain setup
|
||||||
|
```
|
||||||
|
|
||||||
|
After running `safe-chain setup`, restart your shell to pick up the new aliases. For CI/non-interactive environments use `safe-chain setup-ci` instead.
|
||||||
|
|
||||||
### Pinning to a specific version
|
### Pinning to a specific version
|
||||||
|
|
||||||
To install a specific version instead of the latest, replace `latest` with the version number in the URL (available from version 1.3.2 onwards):
|
To install a specific version instead of the latest, replace `latest` with the version number in the URL (available from version 1.3.2 onwards):
|
||||||
|
|
|
||||||
114
docs/homebrew.md
Normal file
114
docs/homebrew.md
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
# Homebrew Tap
|
||||||
|
|
||||||
|
End-user install instructions live in the top-level [`README.md`](../README.md#macos--linux-homebrew). This page is for maintainers: how the tap is wired up and what one-time setup is required.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
AikidoSec/safe-chain AikidoSec/homebrew-tap
|
||||||
|
┌──────────────────────────┐ ┌──────────────────────────┐
|
||||||
|
│ tag push (vX.Y.Z) │ │ Formula/ │
|
||||||
|
│ → create-binaries │ │ safe-chain.rb │
|
||||||
|
│ → publish-binaries │ │ ^ │
|
||||||
|
│ (draft release + │ │ │ │
|
||||||
|
│ install-*.sh with │ │ │ │
|
||||||
|
│ baked SHA256s) │ │ │ git push │
|
||||||
|
│ │ │ │ on each release │
|
||||||
|
│ release.published │ ────────► │ │ │
|
||||||
|
│ → publish-homebrew │ │ │
|
||||||
|
│ → publish-npm │ │ │
|
||||||
|
└──────────────────────────┘ └──────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
The `publish-homebrew` job in [`.github/workflows/build-and-release.yml`](../.github/workflows/build-and-release.yml) fires on `release.published` (i.e. when a maintainer publishes the draft release that `publish-binaries` created on the tag push). It:
|
||||||
|
|
||||||
|
1. Downloads the release's `install-safe-chain.sh` asset (already has per-platform SHA256s baked in by the earlier `publish-binaries` step — see lines 76–86 of the workflow).
|
||||||
|
2. Parses the four SHA256s it needs (macOS x64/arm64, Linux x64/arm64) out of the install script.
|
||||||
|
3. Renders `Formula/safe-chain.rb` from a quoted heredoc template, substituting the new version and SHAs.
|
||||||
|
4. Pushes the rendered formula to the `main` branch of `AikidoSec/homebrew-tap`.
|
||||||
|
|
||||||
|
Prereleases are skipped (`if: ... && !github.event.release.prerelease`) so beta tags like `0.0.1-sha256-in-installer-beta` don't update the stable tap.
|
||||||
|
|
||||||
|
## One-time setup
|
||||||
|
|
||||||
|
This needs to happen exactly once. Until it's done, the `publish-homebrew` job will fail on the next release with a 404 on the tap-repo checkout (which is harmless — it doesn't block npm publish — but the job will appear red).
|
||||||
|
|
||||||
|
### 1. Create the tap repository
|
||||||
|
|
||||||
|
Create a new public repository under the AikidoSec organisation called **`homebrew-tap`** (the `homebrew-` prefix is required by `brew tap`; users will run `brew tap AikidoSec/tap` which expands to `AikidoSec/homebrew-tap`).
|
||||||
|
|
||||||
|
```
|
||||||
|
Owner: AikidoSec
|
||||||
|
Repository: homebrew-tap
|
||||||
|
Visibility: Public
|
||||||
|
Initialize: Yes (add a README — anything will do, the workflow only touches Formula/safe-chain.rb)
|
||||||
|
Default branch: main
|
||||||
|
```
|
||||||
|
|
||||||
|
No further repo configuration is required. The `Formula/` directory will be created by the workflow on the first publish.
|
||||||
|
|
||||||
|
### 2. Create the token used by the workflow
|
||||||
|
|
||||||
|
The workflow needs to push to `AikidoSec/homebrew-tap` from `AikidoSec/safe-chain`. The default `GITHUB_TOKEN` is scoped to the safe-chain repo only and cannot push cross-repo, so we need a token that grants write access to the tap repo.
|
||||||
|
|
||||||
|
**Recommended:** a fine-grained personal access token from a maintenance bot account (or a deploy-token-style PAT), scoped to `AikidoSec/homebrew-tap` only.
|
||||||
|
|
||||||
|
- Resource owner: `AikidoSec`
|
||||||
|
- Repository access: **Only select repositories** → `AikidoSec/homebrew-tap`
|
||||||
|
- Repository permissions: **Contents: Read and write**
|
||||||
|
- Expiration: 1 year (set a calendar reminder; expired tokens silently break releases)
|
||||||
|
|
||||||
|
Alternative: a GitHub App installation with `contents:write` on the tap repo and a step that exchanges the App's private key for an installation token. More secure for an org-owned automation but more setup. The PAT path is fine for a low-blast-radius tap.
|
||||||
|
|
||||||
|
### 3. Add the token as a secret on `AikidoSec/safe-chain`
|
||||||
|
|
||||||
|
Settings → Secrets and variables → Actions → New repository secret:
|
||||||
|
|
||||||
|
- Name: `HOMEBREW_TAP_TOKEN`
|
||||||
|
- Value: the PAT (or App installation token) from step 2
|
||||||
|
|
||||||
|
That's it. The next release published from `main` will populate `Formula/safe-chain.rb` in the tap.
|
||||||
|
|
||||||
|
### 4. (Optional) Bootstrap with the current release
|
||||||
|
|
||||||
|
If you want users to be able to `brew install AikidoSec/tap/safe-chain` immediately rather than waiting for the next release, manually trigger the workflow against the current tag:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gh workflow run "Create Release" --repo AikidoSec/safe-chain --ref <current-tag>
|
||||||
|
```
|
||||||
|
|
||||||
|
Or simply cut a patch release; the next normal release will populate the tap.
|
||||||
|
|
||||||
|
## How users install
|
||||||
|
|
||||||
|
```sh
|
||||||
|
brew install AikidoSec/tap/safe-chain
|
||||||
|
safe-chain setup
|
||||||
|
```
|
||||||
|
|
||||||
|
`brew install` downloads the prebuilt platform-specific binary from the GitHub release (verified against the SHA256 in the formula) and places it on PATH as `safe-chain`. The user then runs `safe-chain setup` (or `safe-chain setup-ci`) to install the shell aliases that wrap `npm`, `yarn`, `pnpm`, `pip`, etc. This second step is the same as the one in the `curl | sh` install path; Homebrew doesn't (and shouldn't) modify user shell rc files at install time.
|
||||||
|
|
||||||
|
## How users upgrade
|
||||||
|
|
||||||
|
```sh
|
||||||
|
brew upgrade safe-chain
|
||||||
|
```
|
||||||
|
|
||||||
|
The formula's `livecheck` block uses GitHub's `releases/latest` strategy, so `brew outdated` and Homebrew's auto-bump tooling pick up new versions automatically once the tap repo has been updated by the release workflow.
|
||||||
|
|
||||||
|
## Testing the formula locally
|
||||||
|
|
||||||
|
```sh
|
||||||
|
brew tap AikidoSec/tap
|
||||||
|
brew install --build-from-source AikidoSec/tap/safe-chain
|
||||||
|
brew test AikidoSec/tap/safe-chain
|
||||||
|
brew style $(brew --repository)/Library/Taps/aikidosec/homebrew-tap/Formula/safe-chain.rb
|
||||||
|
brew audit --new --formula AikidoSec/tap/safe-chain
|
||||||
|
```
|
||||||
|
|
||||||
|
The PR that wired this up (#TBD) validated all four of these against the 1.5.3 release on macOS arm64 (Homebrew 5.1.11).
|
||||||
|
|
||||||
|
## Future work
|
||||||
|
|
||||||
|
- **Submission to `homebrew-core`:** A custom tap is the lowest-friction path. If we ever want `brew install safe-chain` (no tap prefix) to work, the formula needs to be submitted to Homebrew's central `homebrew-core` repo. That requires the project to meet [Homebrew's notability criteria](https://docs.brew.sh/Acceptable-Formulae) and the formula to build from source (no prebuilt binaries) — which would mean either compiling the Node + pkg bundling in-formula or rewriting the wrapper in a compiled language. Not in scope right now.
|
||||||
|
- **winget and Chocolatey:** Also requested in [#372](https://github.com/AikidoSec/safe-chain/issues/372). Each has its own manifest format and release-time automation; they should be separate PRs.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue