Skip to content

Commit 6d2433c

Browse files
leliaclaude
andcommitted
fix(dependency-review): scope SFW token to a dedicated environment
Resolve zizmor secrets-outside-env (medium) without suppressing it. Split the single mode-switching smoke job into two: - python-sfw-smoke-free: untrusted PRs (Dependabot, forks, outside collaborators, externals). Anonymous free edition, never references the token. - python-sfw-smoke-enterprise: SocketDev org members (OWNER/MEMBER) on an in-repo PR. Authenticated enterprise edition; SOCKET_SFW_API_TOKEN is scoped to the `socket-firewall` GitHub environment, so only this job can read it. inspect now classifies PR trust (author_association OWNER/MEMBER, non-fork, non-Dependabot) and references no secret. No required-reviewer protection on the environment, so trusted dep PRs still run automatically. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 9f5b0fb commit 6d2433c

1 file changed

Lines changed: 68 additions & 42 deletions

File tree

.github/workflows/dependency-review.yml

Lines changed: 68 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
name: dependency-review
22

33
# Supply-chain guardrails for dependency-update PRs -- for BOTH Dependabot
4-
# and maintainers. Inspects the changed files, then runs a Socket Firewall
5-
# (sfw) install smoke job for Python dependency changes, picking the firewall
6-
# edition per-PR:
4+
# and maintainers. `inspect` classifies the PR, then exactly one Socket
5+
# Firewall (sfw) install smoke job runs when Python deps change:
76
#
8-
# - Trusted SocketDev members on an in-repo (non-fork) PR, when the
9-
# SOCKET_SFW_API_TOKEN secret is present -> Socket Firewall ENTERPRISE
10-
# (authenticated, full org-policy enforcement).
11-
# - Everything else -- Dependabot, forks, external contributors, or a
12-
# missing token -> Socket Firewall FREE (anonymous, no API token), which
13-
# is safe in the unprivileged `pull_request` context.
7+
# - python-sfw-smoke-enterprise -- trusted SocketDev org members
8+
# (author_association OWNER/MEMBER) on an in-repo (non-fork) PR. Runs the
9+
# authenticated enterprise edition for full org-policy enforcement. The
10+
# SOCKET_SFW_API_TOKEN is scoped to the `socket-firewall` environment, so
11+
# it is the ONLY job that can read the token.
12+
# - python-sfw-smoke-free -- everyone else (Dependabot, forks, outside
13+
# collaborators, external contributors). Anonymous free edition, no token.
14+
# This job never references the secret.
1415
#
15-
# The mode is computed in `inspect` and degrades to free whenever the token is
16-
# absent (e.g. before it has been added to the repo/org, or on fork PRs where
17-
# GitHub withholds secrets), so this workflow is safe to ship as-is and starts
18-
# using the enterprise edition automatically once the secret exists.
16+
# Splitting the jobs (rather than picking a mode in one job) keeps the token
17+
# out of scope on every untrusted run and satisfies zizmor's
18+
# `secrets-outside-env` audit without suppressing it. The free path runs in
19+
# the unprivileged `pull_request` context with no secret-leak surface.
1920
#
2021
# Pattern adapted from SocketDev/socket-python-cli.
2122

@@ -37,7 +38,7 @@ jobs:
3738
outputs:
3839
python_deps_changed: ${{ steps.diff.outputs.python_deps_changed }}
3940
workflow_or_action_changed: ${{ steps.diff.outputs.workflow_or_action_changed }}
40-
sfw_mode: ${{ steps.mode.outputs.sfw_mode }}
41+
is_trusted: ${{ steps.trust.outputs.is_trusted }}
4142
steps:
4243
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
4344
with:
@@ -73,30 +74,26 @@ jobs:
7374
echo "workflow_or_action_changed=$(has_file '^\.github/workflows/|^\.github/actions/|^\.github/dependabot\.yml$')"
7475
} >> "$GITHUB_OUTPUT"
7576
76-
- name: Determine Socket Firewall mode
77-
id: mode
77+
- name: Classify PR trust
78+
id: trust
79+
# Trusted == a SocketDev org member (OWNER/MEMBER) on an in-repo
80+
# (non-fork) PR. Note this references NO secret -- it only decides which
81+
# smoke job runs; the token stays scoped to the enterprise job.
7882
env:
7983
IS_DEPENDABOT: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }}
8084
IS_FORK: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
8185
AUTHOR_ASSOC: ${{ github.event.pull_request.author_association }}
82-
# Empty for fork PRs (secrets withheld) and until the secret is added.
83-
SOCKET_SFW_API_TOKEN: ${{ secrets.SOCKET_SFW_API_TOKEN }}
8486
run: |
85-
mode=firewall-free
86-
# Enterprise only for a SocketDev org member (OWNER/MEMBER) on an
87-
# in-repo PR, and only when the token is actually present. Everything
88-
# else -- Dependabot, forks, outside collaborators, external
89-
# contributors, or a missing token -- uses the free edition.
87+
is_trusted=false
9088
if [ "$IS_DEPENDABOT" != "true" ] \
9189
&& [ "$IS_FORK" != "true" ] \
92-
&& [ -n "$SOCKET_SFW_API_TOKEN" ] \
9390
&& printf '%s' "$AUTHOR_ASSOC" | grep -qE '^(OWNER|MEMBER)$'; then
94-
mode=firewall-enterprise
91+
is_trusted=true
9592
fi
9693
97-
echo "sfw_mode=$mode" >> "$GITHUB_OUTPUT"
94+
echo "is_trusted=$is_trusted" >> "$GITHUB_OUTPUT"
9895
{
99-
echo "## Socket Firewall mode: \`$mode\`"
96+
echo "## Socket Firewall edition: \`$([ "$is_trusted" = true ] && echo enterprise || echo free)\`"
10097
echo "- author_association: \`$AUTHOR_ASSOC\`"
10198
echo "- dependabot: \`$IS_DEPENDABOT\` | fork: \`$IS_FORK\`"
10299
} >> "$GITHUB_STEP_SUMMARY"
@@ -113,9 +110,11 @@ jobs:
113110
echo "- This workflow runs in pull_request context only; no publish secrets are exposed"
114111
} >> "$GITHUB_STEP_SUMMARY"
115112
116-
python-sfw-smoke:
113+
# Untrusted PRs (Dependabot, forks, outside collaborators, externals):
114+
# anonymous free edition. Never references the token.
115+
python-sfw-smoke-free:
117116
needs: inspect
118-
if: needs.inspect.outputs.python_deps_changed == 'true'
117+
if: needs.inspect.outputs.python_deps_changed == 'true' && needs.inspect.outputs.is_trusted != 'true'
119118
runs-on: ubuntu-latest
120119
timeout-minutes: 15
121120
steps:
@@ -127,21 +126,48 @@ jobs:
127126
- uses: ./.github/actions/setup-sfw
128127
with:
129128
uv: "true"
130-
mode: ${{ needs.inspect.outputs.sfw_mode }}
129+
mode: firewall-free
130+
131+
- name: Sync project through Socket Firewall (free)
132+
env:
133+
UV_PYTHON: "3.12"
134+
UV_PYTHON_DOWNLOADS: never
135+
run: sfw uv sync --locked --extra test --extra dev
136+
137+
- name: Import smoke test
138+
run: |
139+
uv run python -c "
140+
import socketdev
141+
from socketdev import socketdev as SocketDevClient
142+
from socketdev.core.api import API
143+
from socketdev.version import __version__
144+
print('import smoke OK', __version__)
145+
"
146+
147+
# Trusted SocketDev members: authenticated enterprise edition. The token is
148+
# scoped to the `socket-firewall` environment, so only this job can read it.
149+
python-sfw-smoke-enterprise:
150+
needs: inspect
151+
if: needs.inspect.outputs.python_deps_changed == 'true' && needs.inspect.outputs.is_trusted == 'true'
152+
runs-on: ubuntu-latest
153+
timeout-minutes: 15
154+
environment: socket-firewall
155+
steps:
156+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
157+
with:
158+
fetch-depth: 1
159+
persist-credentials: false
160+
161+
- uses: ./.github/actions/setup-sfw
162+
with:
163+
uv: "true"
164+
mode: firewall-enterprise
131165
socket-token: ${{ secrets.SOCKET_SFW_API_TOKEN }}
132166

133-
- name: Sync project through Socket Firewall
134-
# `sfw uv sync` is the intended way to route uv through Socket Firewall
135-
# (per Socket's own uv wrapper guidance). --locked verifies the exact
136-
# uv.lock set and fails on lockfile drift rather than silently
137-
# re-resolving, so the firewall inspects precisely what would install.
138-
# Note: uv's sfw integration is quieter than npm/pip -- it does not
139-
# print the "N packages fetched" footer, but interception is active.
140-
#
141-
# Use the runner's setup-python interpreter and forbid managed-Python
142-
# downloads: .python-version pins an exact patch (3.12.7) that uv would
143-
# otherwise fetch from GitHub, which the firewall's TLS interception
144-
# blocks. The firewall is here to vet PyPI installs, not the toolchain.
167+
- name: Sync project through Socket Firewall (enterprise)
168+
# See free job for the UV_PYTHON rationale: .python-version pins an
169+
# exact patch that uv would otherwise fetch from GitHub through the
170+
# firewall (blocked by its TLS interception); use the runner's Python.
145171
env:
146172
UV_PYTHON: "3.12"
147173
UV_PYTHON_DOWNLOADS: never

0 commit comments

Comments
 (0)