Skip to content

feat(signals): share the scout creation check between endpoint and facade #318672

feat(signals): share the scout creation check between endpoint and facade

feat(signals): share the scout creation check between endpoint and facade #318672

Workflow file for this run

on:
push:
branches:
- master
pull_request:
name: Security
permissions:
contents: read
env:
SEMGREP_ENABLE_VERSION_CHECK: 'false'
SEMGREP_IMAGE: semgrep/semgrep:1.175.0@sha256:b94b53d02fd4a022f9eac4e2af1380f5c3c4c21400e79d3336bdff1d1db5e796
jobs:
# Job to decide which path-scoped semgrep scans need to run.
# See .github/actions/paths-filter/README.md for filter semantics
# Pushes and PRs above GitHub's 3000-file API limit run every scan.
changes:
runs-on: ubuntu-24.04
timeout-minutes: 5
if: github.repository == 'PostHog/posthog'
name: Determine need to run Semgrep checks
permissions:
contents: read
pull-requests: read
outputs:
python: ${{ steps.oversized.outputs.force_all || steps.filter.outputs.python || 'true' }}
go: ${{ steps.oversized.outputs.force_all || steps.filter.outputs.go || 'true' }}
rust: ${{ steps.oversized.outputs.force_all || steps.filter.outputs.rust || 'true' }}
js: ${{ steps.oversized.outputs.force_all || steps.filter.outputs.js || 'true' }}
products-frontend: ${{ steps.oversized.outputs.force_all || steps.filter.outputs.products-frontend || 'true' }}
general: ${{ steps.oversized.outputs.force_all || steps.filter.outputs.general || 'true' }}
devex: ${{ steps.oversized.outputs.force_all || steps.filter.outputs.devex || 'true' }}
test-rules: ${{ steps.oversized.outputs.force_all || steps.filter.outputs.test-rules || 'true' }}
desktop: ${{ steps.oversized.outputs.force_all || steps.filter.outputs.desktop || 'true' }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
clean: false
persist-credentials: false
sparse-checkout: .github/actions/paths-filter
sparse-checkout-cone-mode: false
- name: Force all scans for oversized pull requests
id: oversized
if: github.event_name == 'pull_request' && github.event.pull_request.changed_files > 3000
run: echo "force_all=true" >> "$GITHUB_OUTPUT"
- uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
id: app-token
# forks can't read org secrets — fall back to github.token
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
with:
client-id: ${{ vars.GH_APP_POSTHOG_PATHS_FILTER_APP_ID }}
private-key: ${{ secrets.GH_APP_POSTHOG_PATHS_FILTER_PRIVATE_KEY }}
# paths-filter only calls pulls.listFiles
permission-pull-requests: read
- uses: ./.github/actions/paths-filter
id: filter
if: github.event_name != 'push' && steps.oversized.outputs.force_all != 'true'
with:
token: ${{ steps.app-token.outputs.token || github.token }}
filters: |
python:
- 'bin/**'
- 'common/**'
- 'ee/**'
- 'posthog/**'
- 'products/**'
# the scan --excludes products/desktop (semgrep-desktop covers it)
- '!products/desktop/**'
- 'services/llm-gateway/**'
- 'services/stripe-mock/**'
- 'tools/**'
- '.semgrep/rules/security/**'
- '.github/workflows/ci-security.yaml'
go:
- 'livestream/**'
- 'services/hogql-language-service/**'
- '.github/workflows/ci-security.yaml'
rust:
- 'cli/**'
- 'rust/**'
- '.github/workflows/ci-security.yaml'
js:
- 'frontend/**'
- 'nodejs/**'
- 'packages/**'
- 'services/agent-proxy/**'
- 'services/integration-service/**'
- 'services/mcp/**'
- 'services/oauth-proxy/**'
- 'services/stripe-app/**'
- '.semgrep/rules/security/**'
- '.github/workflows/ci-security.yaml'
products-frontend:
- 'products/**'
- '.semgrep/rules/security/prefer-codegen-api.yaml'
- '.github/workflows/ci-security.yaml'
# semgrep-general scans whatever the language-specific jobs
# don't. Keep these excludes aligned with its --exclude list.
general:
- '**'
- '!cli/**'
- '!common/**'
- '!ee/**'
- '!frontend/**'
- '!livestream/**'
- '!nodejs/**'
- '!packages/**'
- '!posthog/**'
- '!products/**'
- '!rust/**'
- '!.semgrep/**'
- '!docs/**'
- '!services/**'
# Keep these paths aligned with semgrep-devex's scan targets.
devex:
- 'bin/**'
- 'common/**'
- 'docs/onboarding/**'
- 'ee/**'
- 'frontend/**'
- 'packages/**'
- 'posthog/**'
- 'products/**'
- 'services/**'
- 'tools/**'
- '.semgrep/rules/devex/**'
- '.github/workflows/ci-security.yaml'
# Rule tests only read fixtures under .semgrep/.
test-rules:
- '.semgrep/**'
- '.github/workflows/ci-security.yaml'
# products/desktop is an imported standalone app. It doesn't follow the
# monorepo's semgrep *convention* rules, so those jobs exclude it; this
# filter gates semgrep-desktop, which gives it universal security coverage.
desktop:
- 'products/desktop/**'
- '.github/workflows/ci-security.yaml'
semgrep-python:
needs: changes
if: needs.changes.outputs.python == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 30
env:
DOCKERHUB_USERNAME: ${{ vars.DOCKERHUB_USER }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
steps:
# The login only lifts the Docker Hub pull rate limit, and anonymous pulls still
# work, so every Docker Hub login in CI is best-effort.
- name: Log in to Docker Hub
continue-on-error: true
if: ${{ env.DOCKERHUB_USERNAME != '' && env.DOCKERHUB_TOKEN != '' }}
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ vars.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Run Semgrep
uses: ./.github/actions/semgrep-ci
with:
image: ${{ env.SEMGREP_IMAGE }}
args: >-
--config p/python
--config p/owasp-top-ten
--config p/security-audit
--config p/trailofbits
--config .semgrep/rules/security
--exclude-rule trailofbits.generic.redis-unencrypted-transport.redis-unencrypted-transport
--exclude-rule python.django.security.audit.csrf-exempt.no-csrf-exempt
--exclude-rule generic.html-templates.security.var-in-href.var-in-href
--include /bin
--include /common
--include /ee
--include /posthog
--include /products
--include /services/llm-gateway
--include /services/stripe-mock
--include /tools
--exclude products/desktop
--jobs 4
semgrep-go:
needs: changes
if: needs.changes.outputs.go == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 10
env:
DOCKERHUB_USERNAME: ${{ vars.DOCKERHUB_USER }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
steps:
- name: Log in to Docker Hub
continue-on-error: true
if: ${{ env.DOCKERHUB_USERNAME != '' && env.DOCKERHUB_TOKEN != '' }}
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ vars.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Run Semgrep
uses: ./.github/actions/semgrep-ci
with:
image: ${{ env.SEMGREP_IMAGE }}
args: >-
--config p/golang
--config p/owasp-top-ten
--config p/security-audit
--config p/trailofbits
--config r/go.lang.security
--exclude-rule go.lang.security.audit.xss.no-fprintf-to-responsewriter.no-fprintf-to-responsewriter
--include /livestream
--include /services/hogql-language-service
semgrep-rust:
needs: changes
if: needs.changes.outputs.rust == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 10
env:
DOCKERHUB_USERNAME: ${{ vars.DOCKERHUB_USER }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
steps:
- name: Log in to Docker Hub
continue-on-error: true
if: ${{ env.DOCKERHUB_USERNAME != '' && env.DOCKERHUB_TOKEN != '' }}
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ vars.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Run Semgrep
uses: ./.github/actions/semgrep-ci
with:
image: ${{ env.SEMGREP_IMAGE }}
args: >-
--config p/rust
--config p/owasp-top-ten
--config p/security-audit
--config p/trailofbits
--config r/rust.lang.security
--exclude-rule trailofbits.generic.curl-unencrypted-url.curl-unencrypted-url
--exclude-rule trailofbits.generic.redis-unencrypted-transport.redis-unencrypted-transport
--exclude-rule trailofbits.rs.panic-in-function-returning-result.panic-in-function-returning-result
--include /cli
--include /rust
semgrep-js:
needs: changes
if: needs.changes.outputs.js == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 20
env:
DOCKERHUB_USERNAME: ${{ vars.DOCKERHUB_USER }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
steps:
- name: Log in to Docker Hub
continue-on-error: true
if: ${{ env.DOCKERHUB_USERNAME != '' && env.DOCKERHUB_TOKEN != '' }}
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ vars.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Run Semgrep
uses: ./.github/actions/semgrep-ci
with:
image: ${{ env.SEMGREP_IMAGE }}
args: >-
--config .semgrep/rules/security
--config p/javascript
--config p/owasp-top-ten
--config p/security-audit
--config p/trailofbits
--include /frontend
--include /nodejs
--include /packages
--include /services/agent-proxy
--include /services/integration-service
--include /services/mcp
--include /services/oauth-proxy
--include /services/stripe-app
semgrep-products-frontend:
needs: changes
if: needs.changes.outputs.products-frontend == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 20
env:
DOCKERHUB_USERNAME: ${{ vars.DOCKERHUB_USER }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
steps:
- name: Log in to Docker Hub
continue-on-error: true
if: ${{ env.DOCKERHUB_USERNAME != '' && env.DOCKERHUB_TOKEN != '' }}
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ vars.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# Single-rule scan — keeps the failure surface here aligned with the
# job name. Other custom TS rules (no-direct-fuse-instantiation,
# require-iframe-sandbox, ...) live in semgrep-js. When more
# product-specific rules land, extend the --config list explicitly.
- name: Run Semgrep
uses: ./.github/actions/semgrep-ci
with:
image: ${{ env.SEMGREP_IMAGE }}
args: >-
--config .semgrep/rules/security/prefer-codegen-api.yaml
--include /products
# scans GitHub Actions and other repo-wide config
semgrep-general:
needs: changes
if: needs.changes.outputs.general == 'true'
runs-on: ubuntu-24.04
# The scan itself sits around 12 minutes, so 20 left too little headroom
# for a slow runner and the job was tripping the timeout.
timeout-minutes: 30
env:
DOCKERHUB_USERNAME: ${{ vars.DOCKERHUB_USER }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
steps:
- name: Log in to Docker Hub
continue-on-error: true
if: ${{ env.DOCKERHUB_USERNAME != '' && env.DOCKERHUB_TOKEN != '' }}
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ vars.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# exclude all directories already scanned by other jobs
#
# This job is the only one that scans .github/, so a workflow rule in
# .semgrep/rules/security/ gates nothing until it is named here. The jobs
# that load that whole directory restrict their targets to trees that hold
# no workflow file.
- name: Run Semgrep
uses: ./.github/actions/semgrep-ci
with:
image: ${{ env.SEMGREP_IMAGE }}
args: >-
--config p/owasp-top-ten
--config p/security-audit
--config p/trailofbits
--config p/github-actions
--config .semgrep/rules/security/github-actions-publish-on-pull-request.yaml
--exclude-rule dockerfile.security.no-sudo-in-dockerfile.no-sudo-in-dockerfile
--exclude-rule trailofbits.generic.redis-unencrypted-transport.redis-unencrypted-transport
--exclude-rule trailofbits.yaml.docker-compose.port-all-interfaces.port-all-interfaces
--exclude-rule yaml.github-actions.security.audit.unsafe-add-mask-workflow-command.unsafe-add-mask-workflow-command
--exclude /cli
--exclude /common
--exclude /ee
--exclude /frontend
--exclude /livestream
--exclude /nodejs
--exclude /packages
--exclude /posthog
--exclude /products
--exclude /rust
--exclude /.semgrep
--exclude /docs
--exclude /services
--jobs 4
# Devex / hygiene rules. Two passes:
# - WARNING-severity rules print findings without failing the job
# (used while migrating away from a pattern with a known backlog).
# - ERROR-severity rules fail the job on any finding (regression guards).
# See `.semgrep/rules/devex/README.md`.
semgrep-devex:
needs: changes
if: needs.changes.outputs.devex == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 20
env:
DOCKERHUB_USERNAME: ${{ vars.DOCKERHUB_USER }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
steps:
- name: Log in to Docker Hub
continue-on-error: true
if: ${{ env.DOCKERHUB_USERNAME != '' && env.DOCKERHUB_TOKEN != '' }}
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ vars.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# PR merge commit plus both parents, so the new-warnings
# pass can baseline against the PR base (HEAD^1)
fetch-depth: 2
persist-credentials: false
# Pass 1: print WARNING-severity findings. No --error — this step
# does not fail CI. Backlogs from migration-in-progress rules show
# up here.
- name: Warnings (informational)
run: |
docker run --rm -v "${{ github.workspace }}:/src" -w /src \
-e SEMGREP_ENABLE_VERSION_CHECK=false \
${{ env.SEMGREP_IMAGE }} \
semgrep \
--config ".semgrep/rules/devex/" \
--severity=WARNING \
--metrics=off \
--verbose \
--timeout 300 \
bin/ common/ docs/onboarding/ ee/ frontend/ packages/ posthog/ products/ services/ tools/
# Pass 2: enforce ERROR-severity rules. Any finding fails the
# job — regression guards live here.
- name: Errors (blocking)
run: |
docker run --rm -v "${{ github.workspace }}:/src" -w /src \
-e SEMGREP_ENABLE_VERSION_CHECK=false \
${{ env.SEMGREP_IMAGE }} \
semgrep \
--config ".semgrep/rules/devex/" \
--severity=ERROR \
--exclude "products/desktop" \
--error \
--metrics=off \
--verbose \
--timeout 300 \
bin/ common/ docs/onboarding/ ee/ frontend/ packages/ posthog/ products/ services/ tools/
# Pass 3 (PRs only): fail on newly-introduced WARNING findings.
# --baseline-commit diffs against the PR base (first parent of the
# merge commit), so the pass-1 backlog is grandfathered while new
# violations block. See .semgrep/rules/devex/README.md.
- name: New warnings (blocking)
if: github.event_name == 'pull_request'
run: |
docker run --rm -v "${{ github.workspace }}:/src" -w /src \
-e SEMGREP_ENABLE_VERSION_CHECK=false \
${{ env.SEMGREP_IMAGE }} \
semgrep \
--config ".semgrep/rules/devex/" \
--severity=WARNING \
--baseline-commit "$(git rev-parse HEAD^1)" \
--error \
--metrics=off \
--verbose \
--timeout 300 \
bin/ common/ docs/onboarding/ ee/ frontend/ packages/ posthog/ products/ services/ tools/
semgrep-test-rules:
needs: changes
if: needs.changes.outputs.test-rules == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 10
env:
DOCKERHUB_USERNAME: ${{ vars.DOCKERHUB_USER }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
steps:
- name: Log in to Docker Hub
continue-on-error: true
if: ${{ env.DOCKERHUB_USERNAME != '' && env.DOCKERHUB_TOKEN != '' }}
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ vars.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Test custom Semgrep rules
run: |
docker run --rm -v "${{ github.workspace }}:/src" -w /src \
-e SEMGREP_ENABLE_VERSION_CHECK=false \
${{ env.SEMGREP_IMAGE }} \
semgrep --test .semgrep/
# semgrep-general runs `semgrep ci`, which drops every match that the merge-base
# copy of a file also has. A rule-only change leaves every workflow unchanged, so
# that scan cannot report what the edited rule now matches. This full scan can.
- name: Scan workflows with the workflow rules
run: |
docker run --rm -v "${{ github.workspace }}:/src" -w /src \
-e SEMGREP_ENABLE_VERSION_CHECK=false \
${{ env.SEMGREP_IMAGE }} \
semgrep scan --error --metrics=off --timeout 300 \
--config .semgrep/rules/security/github-actions-publish-on-pull-request.yaml \
.github/workflows/
# products/desktop is an imported standalone app that does not follow the monorepo's
# semgrep *convention* rules (.semgrep custom rules, devex), so those jobs exclude it.
# This job gives its TS/Python real static security coverage from the universal registry
# packs, blocking on ERROR-severity findings only (the repo's ERROR-fails model).
semgrep-desktop:
needs: changes
if: needs.changes.outputs.desktop == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 20
env:
DOCKERHUB_USERNAME: ${{ vars.DOCKERHUB_USER }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
steps:
- name: Log in to Docker Hub
continue-on-error: true
if: ${{ env.DOCKERHUB_USERNAME != '' && env.DOCKERHUB_TOKEN != '' }}
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ vars.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Run Semgrep (desktop, universal security packs)
run: |
docker run --rm -v "${{ github.workspace }}:/src" -w /src \
-e SEMGREP_ENABLE_VERSION_CHECK=false \
${{ env.SEMGREP_IMAGE }} \
semgrep \
--config "p/javascript" \
--config "p/python" \
--config "p/owasp-top-ten" \
--config "p/security-audit" \
--config "p/trailofbits" \
--severity=ERROR \
--error \
--metrics=off \
--verbose \
--timeout 300 \
products/desktop/
semgrep_checks:
needs:
- changes
- semgrep-python
- semgrep-go
- semgrep-rust
- semgrep-js
- semgrep-products-frontend
- semgrep-general
- semgrep-devex
- semgrep-test-rules
- semgrep-desktop
name: Semgrep Checks Pass
runs-on: ubuntu-24.04
timeout-minutes: 5
if: ${{ !cancelled() }}
steps:
- name: Check matrix outcome
run: |
if [[ "${{ needs.changes.result }}" != "success" ]]; then
echo "changes (path filter) job did not succeed - cannot trust skips."
exit 1
fi
if [[ "${{ needs.semgrep-python.result }}" != "success" && "${{ needs.semgrep-python.result }}" != "skipped" ]]; then
echo "semgrep-python did not complete successfully."
exit 1
fi
if [[ "${{ needs.semgrep-go.result }}" != "success" && "${{ needs.semgrep-go.result }}" != "skipped" ]]; then
echo "semgrep-go did not complete successfully."
exit 1
fi
if [[ "${{ needs.semgrep-rust.result }}" != "success" && "${{ needs.semgrep-rust.result }}" != "skipped" ]]; then
echo "semgrep-rust did not complete successfully."
exit 1
fi
if [[ "${{ needs.semgrep-js.result }}" != "success" && "${{ needs.semgrep-js.result }}" != "skipped" ]]; then
echo "semgrep-js did not complete successfully."
exit 1
fi
if [[ "${{ needs.semgrep-products-frontend.result }}" != "success" && "${{ needs.semgrep-products-frontend.result }}" != "skipped" ]]; then
echo "semgrep-products-frontend did not complete successfully."
exit 1
fi
if [[ "${{ needs.semgrep-general.result }}" != "success" && "${{ needs.semgrep-general.result }}" != "skipped" ]]; then
echo "semgrep-general did not complete successfully."
exit 1
fi
if [[ "${{ needs.semgrep-devex.result }}" != "success" && "${{ needs.semgrep-devex.result }}" != "skipped" ]]; then
echo "semgrep-devex did not complete successfully."
exit 1
fi
if [[ "${{ needs.semgrep-test-rules.result }}" != "success" && "${{ needs.semgrep-test-rules.result }}" != "skipped" ]]; then
echo "semgrep-test-rules did not complete successfully."
exit 1
fi
if [[ "${{ needs.semgrep-desktop.result }}" != "success" && "${{ needs.semgrep-desktop.result }}" != "skipped" ]]; then
echo "semgrep-desktop did not complete successfully."
exit 1
fi
echo "All checks passed."