name: AppSec Tests
on:
  workflow_call: # allows to reuse this workflow
    inputs:
      ref:
        description: 'The branch to run the workflow on'
        required: true
        type: string
  workflow_dispatch: # manually
  schedule: # nightly
    - cron: "0 0 * * *"
  pull_request: # on pull requests touching appsec files
    paths:
      - '.github/workflows/appsec.yml'
      - 'internal/appsec/**'
      - 'appsec/**'
      - 'contrib/**/appsec.go'
      - '**/go.mod'
  push:
    branches:
      - release-v*
    tags-ignore:
      - 'contrib/**'
      - 'instrumentation/**'
      - 'internal/**'
      - 'orchestrion/**'
      - 'scripts/**'
env:
  DD_APPSEC_WAF_TIMEOUT: 1m
  GOEXPERIMENT: synctest # TODO: remove once go1.25 is the minimum supported version
  PACKAGES: >-
    ./appsec/...
    ./instrumentation/appsec/...
    ./internal/appsec/...
  SUBMODULES: >-
    ./contrib/database/sql
    ./contrib/gin-gonic/gin
    ./contrib/google.golang.org/grpc
    ./contrib/net/http
    ./contrib/gorilla/mux
    ./contrib/go-chi/chi
    ./contrib/go-chi/chi.v5
    ./contrib/labstack/echo.v4
    ./contrib/99designs/gqlgen
    ./contrib/graphql-go/graphql
    ./contrib/graph-gophers/graphql-go

concurrency:
  # Automatically cancel previous runs if a new one is triggered to conserve resources.
  group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}

permissions:
  contents: read

jobs:
  # Prepare the cache of Go modules to share it will the other jobs.
  # This maximizes cache hits and minimizes the time spent downloading Go modules.
  # Note 1: @actions/cache is very sensitive and it's easy to mess up. Things to know:
  #          - doing it after @actions/checkout is required for all the metadata to be available;
  #          - sharing the cache with windows requires backslashes to be used in the path;
  #          - sharing the cache between OSes requires the base path to be the same, so a relative one is used;
  #          - as of writing this doc, @actions/cache doest work inside docker containers, so had to design so
  #            containerized jobs around this problem, by restoring the cache in the runner and mounting it in the
  #            container ourselves.
  # Note 2: a lot of time was spent on making caching work across macos, linux,
  # windows and golang containers. So this is very sensitive and should be
  # validated again in case of changes. To do so, you can click on the @actions/cache
  # actions logs and look for the "Cache hit" or "Cache miss" messages.
  go-mod-caching:
    name: Prepare Go modules cache
    runs-on: ubuntu-latest-16-cores
    outputs:
      key: ${{ steps.cfg.outputs.key }}
      path: ${{ steps.cfg.outputs.path }}
    steps:
      - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
        with:
          ref: ${{ inputs.ref || github.ref }}

      - name: Compute cache configuration
        id: cfg
        run: |
          echo "key=go-pkg-mod-${{ hashFiles('**/go.sum') }}" >> "$GITHUB_OUTPUT"
          echo "path=go_pkg_mod_cache" >> "$GITHUB_OUTPUT"

      - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
        with:
          cache: false

      - name: Cache Go modules
        id: cache
        uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
        with:
          path: ${{ steps.cfg.outputs.path }}
          key: ${{ steps.cfg.outputs.key }}
          enableCrossOsArchive: true
          lookup-only: true

      - name: Download Go modules
        if: steps.cache.outputs.cache-hit != 'true'
        env:
          GOMODCACHE: ${{ github.workspace }}/${{ steps.cfg.outputs.path }}
        run: go mod download -x

  macos:
    name: ${{ matrix.runs-on }} go${{ matrix.go-version }}
    runs-on: ${{ matrix.runs-on }}
    needs: go-mod-caching
    strategy:
      matrix:
        runs-on: [ macos-14, macos-latest ] # oldest and newest macos runners available
        go-version: [ "1.25", "1.24" ]
      fail-fast: true # saving some CI time - macos runners are too long to get
    steps:
      - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
        with:
          ref: ${{ inputs.ref || github.ref }}

      - name: Restore Go modules cache
        uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
        with:
          path: ${{ needs.go-mod-caching.outputs.path }}
          key: ${{ needs.go-mod-caching.outputs.key }}
          restore-keys: go-pkg-mod-
          enableCrossOsArchive: true
          fail-on-cache-miss: true

      - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
        with:
          go-version: ${{ matrix.go-version }}
          cache: false # we manage the caching ourselves

      # go test is being manually called multiple times here for the sake of reusing the runner.
      # Waiting runners is unfortunately so long that we decided to do so for things only requiring recompilation or
      # reruns under different settings.
      - name: go test
        shell: bash
        env:
          GOMODCACHE: ${{ github.workspace }}/${{ needs.go-mod-caching.outputs.path }}
        run: |
          set -euxo pipefail
          cgocheck="$GOEXPERIMENT,cgocheck2"
          report_error=0
          for cgo in "0" "1"; do
            for appsec_enabled_env in "" "DD_APPSEC_ENABLED=true" "DD_APPSEC_ENABLED=false"; do
                for cgocheck_env in "${GOEXPERIMENT}" "$cgocheck"; do
                  # shellcheck disable=SC2086
                  if ! env CGO_ENABLED="$cgo" $appsec_enabled_env GOEXPERIMENT="$cgocheck_env" go test -v $PACKAGES; then
                    echo "Failed: env CGO_ENABLED=$cgo $appsec_enabled_env GOEXPERIMENT=$cgocheck_env go test -v $PACKAGES"
                  fi

                  # shellcheck disable=SC2086
                  for submodule in $SUBMODULES; do
                    if [[ $cgocheck_env =~ "cgocheck2" && $submodule == "./contrib/gin-gonic/gin" ]]; then
                      echo "Skipped: env CGO_ENABLED=$cgo $appsec_enabled_env GOEXPERIMENT=$cgocheck_env go test -v . (submodule: $submodule, reason: gin-gonic/gin depends on leodido/go-urn that causes an internal compiler when GOEXPERIMENT=cgocheck2)"
                      continue
                    fi

                    cd "$submodule"
                    # shellcheck disable=SC2086
                    if ! env CGO_ENABLED="$cgo" $appsec_enabled_env GOEXPERIMENT="$cgocheck_env" go test -v .; then
                      echo "Failed: env CGO_ENABLED=$cgo $appsec_enabled_env GOEXPERIMENT=$cgocheck_env go test -v . (submodule: $submodule)"
                      report_error=1
                    fi
                    cd -
                  done
                done
            done
          done
          exit $report_error

  # Tests cases were appsec end up being disabled at compilation time
  disabled:
    name: ${{ matrix.runs-on }} (AppSec disabled)
    needs: go-mod-caching
    runs-on: ${{ matrix.runs-on }}
    strategy:
      fail-fast: false
      matrix:
        runs-on: [ macos-latest, windows-latest, ubuntu-latest-16-cores ]
    steps:
      - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
        with:
          ref: ${{ inputs.ref || github.ref }}

      - name: Restore Go modules cache
        uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
        with:
          path: ${{ needs.go-mod-caching.outputs.path }}
          key: ${{ needs.go-mod-caching.outputs.key }}
          restore-keys: go-pkg-mod-
          enableCrossOsArchive: true
          fail-on-cache-miss: true

      - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
        with:
          go-version: stable
          cache: false # we manage the caching ourselves

      - run: go env -w GOMODCACHE="${{ github.workspace }}\${{ needs.go-mod-caching.outputs.path }}"
        if: runner.os == 'Windows'
      - run: go env -w GOMODCACHE=${{ github.workspace }}/${{ needs.go-mod-caching.outputs.path }}
        if: runner.os != 'Windows'

      - name: go test
        shell: bash
        run: |
          set -euxo pipefail
          for appsec_enabled_env in "" "DD_APPSEC_ENABLED=true" "DD_APPSEC_ENABLED=false"; do
            for go_tags in "" "-tags datadog.no_waf"; do
              # shellcheck disable=SC2086
              if ! env $appsec_enabled_env go test -v $go_tags $PACKAGES; then
                echo "Failed: env $appsec_enabled_env go test -v $go_tags $PACKAGES"
              fi

              # shellcheck disable=SC2086
              for submodule in $SUBMODULES; do
                cd "$submodule"
                # shellcheck disable=SC2086
                if ! env $appsec_enabled_env go test -v $go_tags .; then
                  echo "Failed: env $appsec_enabled_env go test -v $go_tags . (submodule: $submodule)"
                fi
                cd -
              done
            done
          done

      # Check for changes in the supported configurations
      - name: Supported Configurations Diff Check
        if: always()
        uses: ./.github/actions/supported_configurations_validation

  # Same tests but on the official golang container for linux
  golang-linux-container:
    name: ${{ matrix.platform }} golang:${{ matrix.go-version }}-${{ matrix.distribution }}
    # We use ARM runners when needed to avoid the performance hit of QEMU
    runs-on: ${{ matrix.platform == 'linux/amd64' && 'ubuntu-latest-16-cores' || 'ubuntu-24.04-arm' }}
    needs: go-mod-caching
    strategy:
      matrix:
        go-version: [ "1.25", "1.24" ]
        distribution: [ trixie, bookworm, alpine ]
        platform: [ linux/amd64, linux/arm64 ]

      fail-fast: false
    steps:
      - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
        with:
          ref: ${{ inputs.ref || github.ref }}

      - name: Restore Go modules cache
        uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
        with:
          path: ${{ needs.go-mod-caching.outputs.path }}
          key: ${{ needs.go-mod-caching.outputs.key }}
          restore-keys: go-pkg-mod-
          enableCrossOsArchive: true
          fail-on-cache-miss: true

      # Docker is not present on early-access ARM runners
      - name: Prepare ARM Runner
        if: runner.arch == 'ARM64' || runner.arch == 'ARM'
        run: |-
          for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove -y $pkg || echo "Not present: $pkg"; done

          sudo apt update
          sudo apt install -y ca-certificates curl
          sudo install -m 0755 -d /etc/apt/keyrings
          sudo curl -fsSL "https://download.docker.com/linux/ubuntu/gpg" -o /etc/apt/keyrings/docker.asc
          sudo chmod a+r /etc/apt/keyrings/docker.asc
          echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list
          sudo apt update
          sudo apt install -y docker-ce docker-ce-cli containerd.io

      - name: Create container
        env:
          GOMODCACHE: ${{ github.workspace }}/${{ needs.go-mod-caching.outputs.path }}
        run: |-
          sudo docker run                                                       \
            --rm                                                                \
            -di                                                                 \
            --name test.runner                                                  \
            -v "${GOMODCACHE}:${GOMODCACHE}"                                    \
            -e "GOMODCACHE=${GOMODCACHE}"                                       \
            -v "$PWD:$PWD"                                                      \
            -w "$PWD"                                                           \
            -e "DD_APPSEC_WAF_TIMEOUT=${{ env.DD_APPSEC_WAF_TIMEOUT }}"         \
            -e "GOEXPERIMENT=${{ env.GOEXPERIMENT }}"                           \
            golang:${{ matrix.go-version }}-${{ matrix.distribution }}
      - name: Install pre-requisites on Alpine
        if: matrix.distribution == 'alpine'
        run: sudo docker exec -i test.runner apk add gcc musl-dev libc6-compat bash
      - name: Output go env
        run: sudo docker exec -i test.runner go env

      - name: NOCGO, undefined appsec state
        run: |
          # shellcheck disable=SC2086
          sudo docker exec -i -e CGO_ENABLED=0 test.runner go test -v $PACKAGES
          # shellcheck disable=SC2086
          for submodule in $SUBMODULES; do
            if ! sudo docker exec -i -e CGO_ENABLED=0 test.runner bash -c "cd $submodule && go test -v ."; then
              echo "Failed: env CGO_ENABLED=0 bash -c \"cd $submodule && go test -v .\" (submodule: $submodule)"
            fi
          done
      - name: NOCGO, appsec disabled
        run: |
          # shellcheck disable=SC2086
          sudo docker exec -i -e CGO_ENABLED=0 -e DD_APPSEC_ENABLED=false test.runner go test -v $PACKAGES
          # shellcheck disable=SC2086
          for submodule in $SUBMODULES; do
            if ! sudo docker exec -i -e CGO_ENABLED=0 -e DD_APPSEC_ENABLED=false test.runner bash -c "cd $submodule && go test -v ."; then
              echo "Failed: env CGO_ENABLED=0 DD_APPSEC_ENABLED=false bash -c \"cd $submodule && go test -v .\" (submodule: $submodule)"
            fi
          done
      - name: NOCGO, appsec enabled
        run: |
          # shellcheck disable=SC2086
          sudo docker exec -i -e CGO_ENABLED=0 -e DD_APPSEC_ENABLED=true test.runner go test -v $PACKAGES
          # shellcheck disable=SC2086
          for submodule in $SUBMODULES; do
            if ! sudo docker exec -i -e CGO_ENABLED=0 -e DD_APPSEC_ENABLED=true test.runner bash -c "cd $submodule && go test -v ."; then
              echo "Failed: env CGO_ENABLED=0 DD_APPSEC_ENABLED=true bash -c \"cd $submodule && go test -v .\" (submodule: $submodule)"
            fi
          done
      - name: CGO, undefined appsec state
        run: |
          # shellcheck disable=SC2086
          sudo docker exec -i -e CGO_ENABLED=1 test.runner go test -v $PACKAGES
          # shellcheck disable=SC2086
          for submodule in $SUBMODULES; do
            if ! sudo docker exec -i -e CGO_ENABLED=1 test.runner bash -c "cd $submodule && go test -v ."; then
              echo "Failed: env CGO_ENABLED=1 bash -c \"cd $submodule && go test -v .\" (submodule: $submodule)"
            fi
          done
      - name: CGO, appsec disabled
        run: |
          # shellcheck disable=SC2086
          sudo docker exec -i -e CGO_ENABLED=1 -e DD_APPSEC_ENABLED=false test.runner go test -v $PACKAGES
          # shellcheck disable=SC2086
          for submodule in $SUBMODULES; do
            if ! sudo docker exec -i -e CGO_ENABLED=1 -e DD_APPSEC_ENABLED=false test.runner bash -c "cd $submodule && go test -v ."; then
              echo "Failed: env CGO_ENABLED=1 DD_APPSEC_ENABLED=false bash -c \"cd $submodule && go test -v .\" (submodule: $submodule)"
            fi
          done
      - name: CGO, appsec enabled
        run: |
          # shellcheck disable=SC2086
          sudo docker exec -i -e CGO_ENABLED=1 -e DD_APPSEC_ENABLED=true test.runner go test -v $PACKAGES
          # shellcheck disable=SC2086
          for submodule in $SUBMODULES; do
            if ! sudo docker exec -i -e CGO_ENABLED=1 -e DD_APPSEC_ENABLED=true test.runner bash -c "cd $submodule && go test -v ."; then
              echo "Failed: env CGO_ENABLED=1 DD_APPSEC_ENABLED=true bash -c \"cd $submodule && go test -v .\" (submodule: $submodule)"
            fi
          done

      # Check for changes in the supported_configurations.json file
      - name: Supported Configurations Diff Check
        if: always()
        run: |
          sudo docker exec -i test.runner bash -c "go run ./scripts/configinverter/main.go check"

      - name: Clean up
        if: always()
        run: sudo docker rm --force test.runner || echo "Could not remove container"

  test-app-smoke-tests:
    name: Smoke Tests
    if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.owner == 'DataDog'
    uses: DataDog/appsec-go-test-app/.github/workflows/smoke-tests.yml@main
    with:
      dd-trace-go-version: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || '' }}
