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
153 changes: 146 additions & 7 deletions package-firewall/bash/README.md

Large diffs are not rendered by default.

154 changes: 146 additions & 8 deletions package-firewall/bash/generate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ NPM_REGISTRY_URL="${FQDN}/v1/namespaces/${ENDOR_NAMESPACE}/firewall/npm/"
NPM_REGISTRY_HOST="${FQDN_HOST}/v1/namespaces/${ENDOR_NAMESPACE}/firewall/npm/"
PYPI_URL="${FQDN}/v1/namespaces/${ENDOR_NAMESPACE}/firewall/pypi/simple/"
MAVEN_REGISTRY_URL="${FQDN}/v1/namespaces/${ENDOR_NAMESPACE}/firewall/maven/"
# VS Code carries its credential as a URL path segment (_ak/<token>) rather than
# in userinfo, so only the base is known here; the token is appended at install
# time once the attribution label exists.
VSCODE_GALLERY_BASE="${FQDN}/v1/namespaces/${ENDOR_NAMESPACE}/firewall/vscode"
API_SECRET_B64=$(printf '%s' "${ENDOR_API_SECRET}" | base64 | tr -d '\n')

# ─── Output directory ─────────────────────────────────────────────────────────
Expand All @@ -85,7 +89,8 @@ substitute() {
-e "s|{{NPM_REGISTRY_HOST}}|${NPM_REGISTRY_HOST}|g" \
-e "s|{{PYPI_URL}}|${PYPI_URL}|g" \
-e "s|{{TRUSTED_HOST}}|${TRUSTED_HOST}|g" \
-e "s|{{MAVEN_REGISTRY_URL}}|${MAVEN_REGISTRY_URL}|g"
-e "s|{{MAVEN_REGISTRY_URL}}|${MAVEN_REGISTRY_URL}|g" \
-e "s|{{VSCODE_GALLERY_BASE}}|${VSCODE_GALLERY_BASE}|g"
}

# inline_common
Expand Down Expand Up @@ -121,6 +126,7 @@ emit_all_blocks() {
emit_block_assignment "UV_BLOCK" "$SHARED_BLOCKS_DIR/uvtoml.txt"
emit_block_assignment "GO_BLOCK" "$SHARED_BLOCKS_DIR/goenv.txt"
emit_block_assignment "MAVEN_BLOCK" "$SHARED_BLOCKS_DIR/mavensettings.txt"
emit_block_assignment "VSCODE_GALLERY_BLOCK" "$SHARED_BLOCKS_DIR/vscodegallery.txt"
echo "# ─────────────────────────────────────────────────────────────────────────────"
echo ""
}
Expand All @@ -129,11 +135,13 @@ arg_parsing_block() {
cat << 'ARGBLOCK'
# ── Argument parsing ──────────────────────────────────────────────────────────
DRY_RUN=0
VSCODE_WATCHER=1
_ENDOR_WARNED=0
for _arg in "$@"; do
case "$_arg" in
--dry-run) DRY_RUN=1 ;;
*) echo "[endor] Unknown argument: $_arg (supported: --dry-run)" >&2; exit 1 ;;
--no-vscode-watcher) VSCODE_WATCHER=0 ;;
*) echo "[endor] Unknown argument: $_arg (supported: --dry-run, --no-vscode-watcher)" >&2; exit 1 ;;
esac
done
unset _arg
Expand Down Expand Up @@ -179,6 +187,13 @@ ENDOR_AUTH_B64="$(printf '%s:%s' "$ENDOR_ATTR_USER" "$ENDOR_API_SECRET" | endor_
ENDOR_PYPI_URL="https://$(endor_urlenc_b64 "$ENDOR_ATTR_USER"):$(endor_urlenc_b64 "$ENDOR_API_SECRET")@{{FQDN_HOST}}/v1/namespaces/{{NAMESPACE}}/firewall/pypi/simple/"
ENDOR_GO_PROXY_URL="https://$(endor_urlenc_b64 "$ENDOR_ATTR_USER"):$(endor_urlenc_b64 "$ENDOR_API_SECRET")@{{FQDN_HOST}}/v1/namespaces/{{NAMESPACE}}/firewall/go/,direct"

# VS Code cannot send Basic auth for the gallery and cannot expand env vars in
# product.json, so the credential travels as a base64url path segment instead.
# Same attributed username as every other ecosystem — the firewall runs
# applyUserAttribution after resolving the _ak path token.
ENDOR_VSCODE_TOKEN="$(printf '%s:%s' "$ENDOR_ATTR_USER" "$ENDOR_API_SECRET" | endor_b64url)"
ENDOR_VSCODE_GALLERY_URL="{{VSCODE_GALLERY_BASE}}/_ak/${ENDOR_VSCODE_TOKEN}"

# No exports — every consumer is same-process template code inlined below.

echo "[endor] user attribution → ${ENDOR_ATTR_LABEL}"
Expand Down Expand Up @@ -207,22 +222,114 @@ script_header() {
echo ""
}

# build_script <template> <output> <description>
# ── VS Code re-apply watcher payload ──────────────────────────────────────────
# VS Code replaces product.json on every update, so a watcher re-applies the
# patch. The watcher needs a script at a stable path; copying "$0" is not an
# option because MDM tools routinely pipe scripts to bash or exec them from an
# already-unlinked temp file. Instead the repatch script is generated here, then
# base64'd into the installer, which decodes it next to the sidecar state.
#
# It deliberately re-uses neither credentials_block nor user_detection_block: the
# watcher can fire from launchd at boot with no console user, and detect_console_user
# exits 1 in that case. The already-rendered URL and home are read back from the
# 0600 root-owned sidecar state instead, so nothing has to be recomputed.

# repatch_prelude — stands in for arg parsing, user detection and credentials.
repatch_prelude() {
cat << 'REPATCHBLOCK'
DRY_RUN=0
_ENDOR_WARNED=0
VSCODE_WATCHER=0
_ENDOR_VSCODE_MODE=repatch

ENDOR_VSCODE_GALLERY_URL="$(vscode_state_get gallery_url 2>/dev/null || true)"
USER_HOME="$(vscode_state_get user_home 2>/dev/null || true)"

if [[ -z "$ENDOR_VSCODE_GALLERY_URL" ]]; then
echo "[endor-vscode] ERROR: no gallery_url in sidecar state — cannot re-apply." >&2
echo "[endor-vscode] Re-run endor-vscode.sh (or endor-all.sh) to reinitialise." >&2
exit 1
fi
REPATCHBLOCK
}

# build_repatch_script <output>
build_repatch_script() {
local output="$1"
{
echo "#!/usr/bin/env bash"
echo "# MDM-deployable: re-applies the Endor VS Code gallery patch to product.json."
echo "# Installed by endor-vscode.sh and run by launchd/systemd/cron after VS Code"
echo "# updates replace product.json. Not intended to be run by hand."
echo "# Generated for namespace=${ENDOR_NAMESPACE} fqdn=${FQDN}."
echo "# Do not edit — regenerate with generate.sh."
echo ""
echo "set -euo pipefail"
echo ""
echo "# ── Common functions (inlined from lib/common.sh) ────────────────────────────"
inline_common
echo "# ─────────────────────────────────────────────────────────────────────────────"
echo ""
repatch_prelude
echo ""
emit_block_assignment "VSCODE_GALLERY_BLOCK" "$SHARED_BLOCKS_DIR/vscodegallery.txt"
echo ""
substitute < "$TMPL_DIR/vscode.sh"
echo ""
script_footer
} > "$output"

chmod 700 "$output"
}

# emit_repatch_payload <repatch_script>
# Emits the base64 payload assignment the installer decodes to a stable path.
emit_repatch_payload() {
echo "# ── VS Code update-watcher payload (endor-vscode-repatch.sh) ────────────────"
echo "_ENDOR_VSCODE_REPATCH_B64=\$(cat <<'ENDOR_VSCODE_REPATCH_B64'"
endor_b64_file "$1"
echo "ENDOR_VSCODE_REPATCH_B64"
echo ")"
echo "# ─────────────────────────────────────────────────────────────────────────────"
echo ""
}

# endor_b64_file <file> — base64, no line wrapping (GNU wraps at 76 by default).
endor_b64_file() {
if base64 --help 2>&1 | grep -q -- '-w'; then
base64 -w0 < "$1"
else
base64 < "$1" | tr -d '\n'
fi
echo ""
}

# build_script <template> <output> <description> [extra_emitter] [no_envsh]
# <extra_emitter> runs after the block assignments, before the ecosystem template
# — used to inject the VS Code watcher payload.
# <no_envsh> set to 1 skips the env.sh / shell-rc setup. VS Code reads no ENDOR_*
# env vars (its credential is baked into product.json), so writing env.sh and
# sourcing it from the user's .zshrc would be a side effect with no purpose.
build_script() {
local template="$1"
local output="$2"
local description="$3"
local extra_emitter="${4:-}"
local no_envsh="${5:-0}"

{
script_header "$output" "$description"
credentials_block
echo ""
emit_all_blocks
echo "# ════════════════════════════════════════════════════════════════════════════"
echo "# Env setup"
echo "# ════════════════════════════════════════════════════════════════════════════"
substitute < "$TMPL_DIR/envsh.sh"
echo ""
[[ -n "$extra_emitter" ]] && "$extra_emitter"
if [[ "$no_envsh" != "1" ]]; then
echo "# ════════════════════════════════════════════════════════════════════════════"
echo "# Env setup"
echo "# ════════════════════════════════════════════════════════════════════════════"
substitute < "$TMPL_DIR/envsh.sh"
echo ""
fi
substitute < "$template"
echo ""
script_footer
Expand Down Expand Up @@ -265,10 +372,28 @@ build_script \
"$OUT_DIR/endor-maven.sh" \
"Configures Maven (~/.m2/settings.xml) for Endor Package Firewall."

# The repatch script must exist before anything that embeds it.
build_repatch_script "$OUT_DIR/endor-vscode-repatch.sh"
REPATCH_SCRIPT="$OUT_DIR/endor-vscode-repatch.sh"
emit_vscode_repatch_payload() { emit_repatch_payload "$REPATCH_SCRIPT"; }

build_script \
"$TMPL_DIR/vscode.sh" \
"$OUT_DIR/endor-vscode.sh" \
"Configures VS Code (product.json extension gallery) for Endor Package Firewall." \
emit_vscode_repatch_payload \
1

# ─── Generate remove script ───────────────────────────────────────────────────
build_remove_script "$OUT_DIR/endor-remove.sh"

# ─── Generate combined all.sh ─────────────────────────────────────────────────
# VS Code is deliberately NOT part of endor-all. It is the only ecosystem that
# writes inside an application bundle (which breaks codesign verification and, on
# macOS Ventura+, needs the App Management TCC grant) and the only one that
# installs a persistent daemon. Folding it in here would silently widen the blast
# radius of every existing endor-all deployment on the next regeneration.
# Deploy endor-vscode.sh alongside endor-all.sh instead — see the READMEs.
{
script_header "$OUT_DIR/endor-all.sh" \
"Configures all package managers for Endor Package Firewall. Covers: npm · pnpm · yarn classic · yarn 2+ · bun · pip · uv · poetry · go · maven"
Expand Down Expand Up @@ -316,11 +441,24 @@ printf " %-24s %s\n" "endor-python.sh" "pip · uv · poetry"
printf " %-24s %s\n" "endor-go.sh" "go modules (GOPROXY)"
printf " %-24s %s\n" "endor-maven.sh" "maven (~/.m2/settings.xml)"
printf " %-24s %s\n" "endor-all.sh" "all of the above (single-script deploy)"
printf " %-24s %s\n" "endor-vscode.sh" "VS Code + Insiders extension gallery (deploy alongside endor-all.sh)"
printf " %-24s %s\n" "endor-remove.sh" "offboarding — strips all Endor config"
echo ""
printf " %-24s %s\n" "endor-vscode-repatch.sh" "installed by endor-vscode.sh; shown so you can read it"
echo ""
echo " All scripts accept --dry-run to preview changes without writing anything."
echo " endor-vscode.sh also accepts --no-vscode-watcher (not recommended — see below)."
echo " Upload to your MDM tool. Each script is self-contained and idempotent."
echo ""
echo " ⚠ VS Code prerequisites — endor-vscode.sh is NOT included in endor-all.sh:"
echo " · macOS Ventura+ requires the App Management (SystemPolicyAppBundles) TCC"
echo " grant for your MDM agent, via a PPPC profile. root is NOT exempt."
echo " · The gallery token lands in world-readable product.json (0644) — VS Code"
echo " offers no indirection. Use a dedicated, separately revocable API key."
echo " · Keep *.vsassets.io / *.vscode-unpkg.net reachable: extension downloads"
echo " still come from Microsoft's CDN by design."
echo " · codesign --verify will report the bundle as modified. Expected. Do not re-sign."
echo ""
echo " To customise: edit shared/blocks/*.txt (shared config content)"
echo " or shared/blocks/envsh.txt (bash env var block)"
echo " or templates/*.sh (orchestration logic)"
Expand Down
35 changes: 35 additions & 0 deletions package-firewall/bash/templates/remove.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@
# Go:
# ~/.config/go/env
#
# Maven:
# ~/.m2/settings.xml
#
# VS Code (stable + Insiders, all discovered install paths):
# .../Contents/Resources/app/product.json (macOS)
# /usr/share/code*/resources/app/product.json (Linux)
# plus the update watcher (launchd/systemd/cron) and the sidecar state dir
#
# Shell profiles (env.sh source line):
# ~/.zshrc
# ~/.bash_profile
Expand All @@ -27,6 +35,8 @@
# - Files with no Endor block are skipped (nothing modified)
# - Files where Endor block is the only content are deleted
# - Files with other content have only the block stripped
# - product.json is restored from the original captured in its marker, not by
# stripping a block — JSON cannot carry a sentinel comment
# - --dry-run: prints what would happen, writes nothing
# - Safe to run multiple times (idempotent)

Expand Down Expand Up @@ -144,11 +154,36 @@ echo "[endor-remove] ── Maven ───────────────

remove_xml_block "$USER_HOME/.m2/settings.xml" "$CONSOLE_USER" "$USER_GROUP"

# ── VS Code product.json ─────────────────────────────────────────────────────────
# Not a sentinel block: the original extensionsGallery is restored byte-for-byte
# from the base64 copy stored in the product.json marker, then the marker itself is
# dropped. Removing the update watcher first means it cannot re-patch a file we are
# about to restore.
echo ""
echo "[endor-remove] ── VS Code ──────────────────────────────────────────────────────────"

vscode_remove_watcher

_VSCODE_REMOVED=0
while IFS= read -r _vsc_pj; do
[[ -n "$_vsc_pj" ]] || continue
_VSCODE_REMOVED=1
vscode_unpatch "$_vsc_pj" || _ENDOR_WARNED=1
done < <(vscode_install_paths "$USER_HOME")
unset _vsc_pj

if [[ "$_VSCODE_REMOVED" == "0" ]]; then
echo "[endor-remove] skip (no VS Code) : no installation found"
fi

echo ""
if [[ "${DRY_RUN:-0}" == "1" ]]; then
echo "[endor-remove] ✓ Dry run complete — no files modified."
else
echo "[endor-remove] ✓ Removal complete."
echo "[endor-remove] Package managers will fall back to their default registries."
echo "[endor-remove] Open a new terminal for shell profile changes to take effect."
if [[ "$_VSCODE_REMOVED" == "1" ]]; then
echo "[endor-remove] Restart VS Code — product.json is only read at startup."
fi
fi
Loading
Loading