# +-------------------------------------------------------------------------+ # | Copyright (C) 2004-2026 The Cacti Group | # +-------------------------------------------------------------------------+ name: Security Posture on: push: branches: [develop] pull_request: branches: [develop] merge_group: workflow_dispatch: schedule: - cron: '0 5 * * 1' permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true defaults: run: shell: bash jobs: trufflehog: name: TruffleHog secret scan runs-on: ubuntu-24.04 timeout-minutes: 20 steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - name: TruffleHog scan uses: trufflesecurity/trufflehog@31ce31ee1abbcc7dc8eee5764c17121c3044c8b2 # v3.99.7 with: extra_args: --only-verified semgrep: name: Semgrep security scan runs-on: ubuntu-24.04 timeout-minutes: 20 permissions: contents: read security-events: write steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Run Semgrep run: | set -euo pipefail docker run --rm \ --user "$(id -u):$(id -g)" \ --env HOME=/tmp \ --volume "${PWD}:/src" \ --workdir /src \ semgrep/semgrep@sha256:e04d2cb132288d90035db8791d64f610cb255b21e727b94db046243b30c01ae9 \ semgrep scan --config p/ci --sarif --output semgrep.sarif . || true - name: Upload Semgrep SARIF # Fork PRs get a read-only GITHUB_TOKEN without security-events: write, so # the upload fails with "Resource not accessible by integration". Skip it # there; the SARIF is still kept as an artifact below. if: always() && github.event.pull_request.head.repo.fork == false uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1 with: sarif_file: semgrep.sarif category: semgrep - name: Upload Semgrep artifact if: always() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: semgrep-report path: semgrep.sarif if-no-files-found: ignore scorecard: name: OpenSSF Scorecard runs-on: ubuntu-24.04 timeout-minutes: 20 steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Run Scorecard env: GITHUB_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail docker run --rm \ --env GITHUB_AUTH_TOKEN \ --volume "${PWD}:/src" \ --workdir /src \ ghcr.io/ossf/scorecard@sha256:3f24714e9366917adb7a05635382c97dfecb14b21eaef3dfa2ea48c8e23e0795 \ --repo="github.com/${{ github.repository }}" \ --format json --show-details > scorecard.json - name: Upload Scorecard artifact if: always() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: scorecard-report path: scorecard.json if-no-files-found: ignore composer-audit: name: Composer dependency audit runs-on: ubuntu-24.04 timeout-minutes: 15 # Advisories are reported as non-blocking: a newly published upstream CVE # must not red every open PR. The audit still runs and writes # composer-audit.json (uploaded below), and any advisory is surfaced as a # ::warning:: annotation. Fixes land via separate dependency-bump PRs. continue-on-error: true steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Setup PHP 8.3 uses: ./.github/actions/setup-php-composer with: php-version: '8.3' coverage: none dependency-mode: audit - name: Run Composer audit run: | set -euo pipefail # composer audit exits non-zero when it finds advisories. Capture that # exit code without tripping set -e so the JSON is always written, then # report the advisory count as a warning and exit 0. Advisories stay # visible (annotation + artifact) but do not fail the job. audit_rc=0 composer audit --locked --no-interaction --format=json | tee composer-audit.json || audit_rc=$? if [ "${audit_rc}" -ne 0 ]; then count="$(jq '(.advisories // {}) | map(length) | add // 0' composer-audit.json 2>/dev/null || echo '?')" echo "::warning title=Composer advisories::composer audit found ${count} advisory record(s); see composer-audit.json artifact. Tracked via separate dependency-bump PRs." fi - name: Upload Composer audit artifact if: always() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: composer-audit-report path: composer-audit.json if-no-files-found: ignore workflow-policy: name: Workflow policy checks runs-on: ubuntu-24.04 timeout-minutes: 10 steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Install policy checker dependencies run: | set -euo pipefail python3 -m pip install --only-binary=:all: --disable-pip-version-check pyyaml==6.0.2 - name: Enforce workflow policy run: | set -euo pipefail python3 tests/tools/test_security_review_paths.py python3 .github/scripts/check-workflow-policy.py dependency-review: name: Dependency review if: github.event_name == 'pull_request' runs-on: ubuntu-24.04 timeout-minutes: 10 permissions: contents: read steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Block newly introduced vulnerable runtime dependencies uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 with: fail-on-severity: high fail-on-scopes: runtime