11name : 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
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