Repository navigation
fix(docker): stop trusting the compromised eMudhra intermediate CAs #169
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: dependency-review | |
| # Supply-chain guardrails for dependency-change PRs -- for BOTH Dependabot and | |
| # maintainers. `inspect` classifies the PR, then the right Socket Firewall (sfw) | |
| # smoke job runs when Python deps change: | |
| # | |
| # - python-sfw-smoke-enterprise -- trusted authors: any in-repo (non-fork) | |
| # PR, i.e. maintainers with write access AND Dependabot (its dependabot/* | |
| # branches live in-repo). Runs the authenticated enterprise edition for | |
| # full org-policy enforcement, reading the SOCKET_SFW_API_TOKEN secret. | |
| # Dependabot-triggered runs read secrets from the *Dependabot* secret | |
| # store, never Actions/environment secrets, so the token must ALSO be | |
| # mirrored there (one-time admin step): | |
| # | |
| # gh secret set SOCKET_SFW_API_TOKEN --app dependabot | |
| # | |
| # This is NOT the pull_request_target footgun: Dependabot secrets are | |
| # handed only to runs actually triggered by dependabot[bot], still in the | |
| # unprivileged read-only pull_request context -- fork PRs from external | |
| # contributors can never read them. Until the mirror exists, setup-sfw | |
| # degrades Dependabot runs to firewall-free with a warning (see that | |
| # action's resolve step), so nothing strands. | |
| # - python-sfw-smoke-free -- fork PRs from external contributors. Anonymous | |
| # free edition, no token. Never references the secret. | |
| # | |
| # Splitting the jobs (rather than picking a mode in one job) means only the | |
| # enterprise job ever names the token; the free path (forks) has no | |
| # secret-leak surface. Both run in the unprivileged `pull_request` context. | |
| # | |
| # Secret scoping vs. the approval-gate trap (matches socket-python-cli#224): | |
| # The enterprise job uses `environment: socket-firewall` so the | |
| # SOCKET_SFW_API_TOKEN can be scoped to that environment -- only this job can | |
| # read it. KEEP the environment; it is good secret hygiene. What must NOT exist | |
| # on that environment is a "required reviewers" approval rule. That rule is the | |
| # trap: the enterprise SFW check cannot itself be a required status check (it is | |
| # skipped on fork PRs, which only run the free edition, and a | |
| # never-created required check blocks merge forever), so a manual deployment | |
| # gate is both self-approvable (prevent_self_review defaults off; admins bypass) | |
| # AND skippable -- maintainers merge without it ever running. Configure the | |
| # environment with no reviewers: | |
| # | |
| # gh api -X PUT repos/SocketDev/socket-basics/environments/socket-firewall \ | |
| # --input - <<<'{"wait_timer":0,"prevent_self_review":false,"reviewers":null,"deployment_branch_policy":null}' | |
| # | |
| # Coverage is instead enforced by the always-on `dependency-review-gate` job | |
| # below -- mark THAT as the single required status check. It runs on every PR | |
| # (if: always(), never skipped, so the required context is always created), | |
| # requires the free job for forks and the enterprise job for maintainers and | |
| # Dependabot, and is a no-op when no Python deps changed. | |
| # | |
| # Docker dependency changes: the main image is already build-smoke-tested by | |
| # smoke-test.yml on every PR, so only the app_tests image (uncovered elsewhere) | |
| # is built here. | |
| # | |
| # Pattern adapted from SocketDev/socket-sdk-python and SocketDev/socket-python-cli. | |
| on: | |
| pull_request: | |
| types: [opened, synchronize, reopened, ready_for_review] | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: dependency-review-${{ github.event.pull_request.number }} | |
| cancel-in-progress: true | |
| jobs: | |
| inspect: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| outputs: | |
| python_deps_changed: ${{ steps.diff.outputs.python_deps_changed }} | |
| app_tests_docker_changed: ${{ steps.diff.outputs.app_tests_docker_changed }} | |
| workflow_or_action_changed: ${{ steps.diff.outputs.workflow_or_action_changed }} | |
| is_trusted: ${{ steps.trust.outputs.is_trusted }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Inspect changed files | |
| id: diff | |
| env: | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| run: | | |
| CHANGED_FILES="$(git diff --name-only "$BASE_SHA" "$HEAD_SHA")" | |
| { | |
| echo "## Changed files" | |
| echo '```' | |
| printf '%s\n' "$CHANGED_FILES" | |
| echo '```' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| has_file() { | |
| local pattern="$1" | |
| if printf '%s\n' "$CHANGED_FILES" | grep -Eq "$pattern"; then | |
| echo "true" | |
| else | |
| echo "false" | |
| fi | |
| } | |
| { | |
| echo "python_deps_changed=$(has_file '^(pyproject\.toml|uv\.lock)$')" | |
| echo "app_tests_docker_changed=$(has_file '^app_tests/Dockerfile$')" | |
| echo "workflow_or_action_changed=$(has_file '^\.github/workflows/|^\.github/actions/|^action\.yml$|^\.github/dependabot\.yml$')" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Classify PR trust | |
| id: trust | |
| # Trusted == any in-repo (non-fork) PR. Only accounts with write access | |
| # can push a branch to this repo, so a non-fork PR already implies a | |
| # trusted author -- the same boundary GitHub uses to decide whether | |
| # secrets are exposed at all. That includes Dependabot: its | |
| # dependabot/* branches are in-repo, and dependency bumps are exactly | |
| # where org-policy (enterprise) enforcement matters most. The | |
| # enterprise job's token resolves from the Dependabot secret store on | |
| # its runs (see the header note). | |
| # | |
| # NB: author_association is deliberately NOT used to require strict org | |
| # membership. It only reflects PUBLIC org membership, so private members | |
| # (the common case) show up as CONTRIBUTOR and would be misclassified. | |
| # This step references NO secret regardless -- it only decides which | |
| # smoke job runs. | |
| env: | |
| IS_DEPENDABOT: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }} | |
| IS_FORK: ${{ github.event.pull_request.head.repo.full_name != github.repository }} | |
| AUTHOR_ASSOC: ${{ github.event.pull_request.author_association }} | |
| run: | | |
| is_trusted=false | |
| if [ "$IS_FORK" != "true" ]; then | |
| is_trusted=true | |
| fi | |
| echo "is_trusted=$is_trusted" >> "$GITHUB_OUTPUT" | |
| { | |
| echo "## Socket Firewall edition: \`$([ "$is_trusted" = true ] && echo enterprise || echo free)\`" | |
| echo "- author_association: \`$AUTHOR_ASSOC\`" | |
| echo "- dependabot: \`$IS_DEPENDABOT\` | fork: \`$IS_FORK\`" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Summarize review expectations | |
| env: | |
| PR_URL: ${{ github.event.pull_request.html_url }} | |
| run: | | |
| { | |
| echo "## Dependency Review Checklist" | |
| echo "- PR: $PR_URL" | |
| echo "- Confirm upstream release notes before merge" | |
| echo "- Do not treat a dependency PR as trusted solely because of the actor" | |
| echo "- This workflow runs in pull_request context only; no publish secrets are exposed" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| # Untrusted PRs (forks from outside collaborators / externals): | |
| # anonymous free edition. Never references the token. | |
| python-sfw-smoke-free: | |
| needs: inspect | |
| if: needs.inspect.outputs.python_deps_changed == 'true' && needs.inspect.outputs.is_trusted != 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 1 | |
| persist-credentials: false | |
| - uses: ./.github/actions/setup-sfw | |
| with: | |
| uv: "true" | |
| mode: firewall-free | |
| - name: Sync project through Socket Firewall (free) | |
| env: | |
| UV_PYTHON: "3.12" | |
| UV_PYTHON_DOWNLOADS: never | |
| run: | | |
| set -o pipefail | |
| sfw uv sync --locked --extra dev 2>&1 | tee sfw-report-free.log | |
| - name: Collect Socket Firewall JSON report | |
| if: always() | |
| # socketdev/action writes a structured report to $SFW_JSON_REPORT_PATH. | |
| run: cp "${SFW_JSON_REPORT_PATH:-/nonexistent}" sfw-report-free.json 2>/dev/null || echo "no SFW JSON report produced" | |
| - name: Import smoke test | |
| # --no-sync: plain `uv run` would re-sync (and potentially hit package | |
| # indexes) OUTSIDE the firewall wrapper; the env was already synced | |
| # through sfw above. | |
| run: | | |
| uv run --no-sync python -c " | |
| import socket_basics | |
| from socket_basics.version import __version__ | |
| print('import smoke OK', __version__) | |
| " | |
| - name: Upload Socket Firewall report | |
| if: always() | |
| uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 | |
| with: | |
| name: sfw-report-free | |
| path: | | |
| sfw-report-free.log | |
| sfw-report-free.json | |
| if-no-files-found: warn | |
| retention-days: 14 | |
| # Trusted authors (SocketDev members + Dependabot): authenticated enterprise | |
| # edition. Only this job references the token (the free job never does). | |
| # `environment:` scopes the secret to this job for maintainer runs; on | |
| # Dependabot runs the same expression resolves from the Dependabot secret | |
| # store instead (mirror required -- header note). The environment must have | |
| # NO required-reviewers rule (see the header note); coverage is enforced by | |
| # dependency-review-gate, not a manual approval gate. | |
| python-sfw-smoke-enterprise: | |
| needs: inspect | |
| if: needs.inspect.outputs.python_deps_changed == 'true' && needs.inspect.outputs.is_trusted == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| environment: socket-firewall | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 1 | |
| persist-credentials: false | |
| - uses: ./.github/actions/setup-sfw | |
| with: | |
| uv: "true" | |
| mode: firewall-enterprise | |
| socket-token: ${{ secrets.SOCKET_SFW_API_TOKEN }} | |
| - name: Sync project through Socket Firewall (enterprise) | |
| # UV_PYTHON pins the runner's interpreter so uv does not fetch a | |
| # uv-managed Python through the firewall (blocked by its TLS interception). | |
| env: | |
| UV_PYTHON: "3.12" | |
| UV_PYTHON_DOWNLOADS: never | |
| run: | | |
| set -o pipefail | |
| sfw uv sync --locked --extra dev 2>&1 | tee sfw-report-enterprise.log | |
| - name: Collect Socket Firewall JSON report | |
| if: always() | |
| # socketdev/action writes a structured report to $SFW_JSON_REPORT_PATH. | |
| run: cp "${SFW_JSON_REPORT_PATH:-/nonexistent}" sfw-report-enterprise.json 2>/dev/null || echo "no SFW JSON report produced" | |
| - name: Import smoke test | |
| # --no-sync: plain `uv run` would re-sync (and potentially hit package | |
| # indexes) OUTSIDE the firewall wrapper; the env was already synced | |
| # through sfw above. | |
| run: | | |
| uv run --no-sync python -c " | |
| import socket_basics | |
| from socket_basics.version import __version__ | |
| print('import smoke OK', __version__) | |
| " | |
| - name: Upload Socket Firewall report | |
| if: always() | |
| uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 | |
| with: | |
| name: sfw-report-enterprise | |
| path: | | |
| sfw-report-enterprise.log | |
| sfw-report-enterprise.json | |
| if-no-files-found: warn | |
| retention-days: 14 | |
| # app_tests image build-smoke (the main image is covered by smoke-test.yml). | |
| docker-smoke-app-tests: | |
| needs: inspect | |
| if: needs.inspect.outputs.app_tests_docker_changed == 'true' | |
| uses: ./.github/workflows/_docker-pipeline.yml | |
| permissions: | |
| contents: read | |
| packages: read # pull the private Socket-built trivy base image | |
| with: | |
| name: socket-basics-app-tests | |
| dockerfile: app_tests/Dockerfile | |
| # app_tests/ is its own build context (its pyproject/uv.lock, fixture apps). | |
| context: app_tests | |
| check_set: app-tests | |
| push: false | |
| workflow-notice: | |
| needs: inspect | |
| if: needs.inspect.outputs.workflow_or_action_changed == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 2 | |
| steps: | |
| - name: Flag workflow-sensitive updates | |
| run: | | |
| { | |
| echo "## Sensitive File Notice" | |
| echo "This PR changes workflow, composite-action, action.yml, or dependabot config files." | |
| echo "Require explicit human review before merge." | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| # Aggregator gate (socket-python-cli#224, Pattern 2). Single always-on status | |
| # that closes the bypass blindspot -- mark THIS job (and only this job) as the | |
| # required status check for the branch (Settings -> Branches). Two rules: | |
| # | |
| # 1. Fail if ANY needed conditional job ended in failure/cancelled | |
| # (success and skipped both pass -- a skipped job is a legitimate no-run). | |
| # 2. Coverage: when Python deps changed, the trust-appropriate SFW edition | |
| # (enterprise for maintainers + Dependabot, free for forks) must have | |
| # actually succeeded -- not merely been skipped. | |
| # | |
| # It runs on every PR (if: always(), never skipped via a job-level condition, | |
| # so the required context is always created -- avoiding the "Expected -- | |
| # Waiting for status" deadlock that strands a required-but-skipped check), and | |
| # never waits on a manual gate. IMPORTANT: merge this job to main BEFORE adding | |
| # it to branch protection, or every other open PR strands on the same trap. | |
| dependency-review-gate: | |
| needs: | |
| - inspect | |
| - python-sfw-smoke-free | |
| - python-sfw-smoke-enterprise | |
| - docker-smoke-app-tests | |
| if: always() | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 2 | |
| steps: | |
| - name: Enforce dependency-review coverage | |
| env: | |
| INSPECT_RESULT: ${{ needs.inspect.result }} | |
| DEPS_CHANGED: ${{ needs.inspect.outputs.python_deps_changed }} | |
| IS_TRUSTED: ${{ needs.inspect.outputs.is_trusted }} | |
| FREE_RESULT: ${{ needs.python-sfw-smoke-free.result }} | |
| ENTERPRISE_RESULT: ${{ needs.python-sfw-smoke-enterprise.result }} | |
| DOCKER_RESULT: ${{ needs.docker-smoke-app-tests.result }} | |
| run: | | |
| fail=0 | |
| # Rule 0: inspect must succeed -- its outputs drive every rule below. | |
| # If it failed/cancelled, DEPS_CHANGED/IS_TRUSTED are empty and the | |
| # coverage rules would silently pass, so fail closed instead. | |
| echo "inspect: $INSPECT_RESULT" | |
| if [ "$INSPECT_RESULT" != "success" ]; then | |
| echo "::error::inspect job did not succeed (result: $INSPECT_RESULT); cannot trust PR classification. Failing closed." | |
| fail=1 | |
| fi | |
| # Rule 1: any real failure/cancellation in a conditional job blocks. | |
| for pair in \ | |
| "python-sfw-smoke-free=$FREE_RESULT" \ | |
| "python-sfw-smoke-enterprise=$ENTERPRISE_RESULT" \ | |
| "docker-smoke-app-tests=$DOCKER_RESULT"; do | |
| name="${pair%%=*}"; res="${pair#*=}" | |
| echo "$name: $res" | |
| if [ "$res" = "failure" ] || [ "$res" = "cancelled" ]; then | |
| echo "::error::$name ended in $res" | |
| fail=1 | |
| fi | |
| done | |
| # Rule 2: when deps changed, the required SFW edition must have run+passed. | |
| if [ "$DEPS_CHANGED" = "true" ]; then | |
| if [ "$IS_TRUSTED" = "true" ]; then | |
| edition="enterprise"; required="$ENTERPRISE_RESULT" | |
| else | |
| edition="free"; required="$FREE_RESULT" | |
| fi | |
| echo "Python deps changed; required Socket Firewall edition: $edition ($required)" | |
| if [ "$required" != "success" ]; then | |
| echo "::error::Required Socket Firewall smoke ($edition) did not succeed (result: $required). This PR changes Python dependencies and must pass the Socket Firewall check before merge." | |
| fail=1 | |
| fi | |
| else | |
| echo "No Python dependency changes -- Socket Firewall smoke not required." | |
| fi | |
| if [ "$fail" -eq 0 ]; then | |
| echo "dependency-review-gate: all required checks satisfied. ✅" | |
| fi | |
| exit "$fail" |