From f8a7a600fdb6e75a17c5a91da2b9014aa7c4aa6b Mon Sep 17 00:00:00 2001 From: Pekka Piispanen Date: Mon, 27 Apr 2026 15:18:03 +0300 Subject: [PATCH 1/5] feature: Add composer-audit and guarddog-php-scan actions --- .github/actions/composer-audit/README.md | 58 ++++++++++ .github/actions/composer-audit/action.yml | 107 +++++++++++++++++++ .github/actions/guarddog-php-scan/README.md | 45 ++++++++ .github/actions/guarddog-php-scan/action.yml | 81 ++++++++++++++ 4 files changed, 291 insertions(+) create mode 100644 .github/actions/composer-audit/README.md create mode 100644 .github/actions/composer-audit/action.yml create mode 100644 .github/actions/guarddog-php-scan/README.md create mode 100644 .github/actions/guarddog-php-scan/action.yml diff --git a/.github/actions/composer-audit/README.md b/.github/actions/composer-audit/README.md new file mode 100644 index 0000000..9897e76 --- /dev/null +++ b/.github/actions/composer-audit/README.md @@ -0,0 +1,58 @@ +# Composer Audit + +Composite action that runs `composer audit` against a project's `composer.lock` to detect known vulnerabilities in PHP dependencies. `composer audit` is a built-in Composer command (available since Composer 2.4) that queries the GitHub Security Advisory database and the Packagist security advisories feed — no binary download or extra installation is required. + +## Usage + +> Replace `` with the current SHA from the [root README](../../../README.md#current-sha). + +```yaml +jobs: + composer-audit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: orangitfi/platform-tooling/.github/actions/composer-audit@ + with: + working-directory: . + fail-on-findings: true +``` + +## Inputs + +| Input | Required | Default | Description | +|-------|----------|---------|-------------| +| `working-directory` | No | `.` | Directory containing `composer.json` and `composer.lock` | +| `fail-on-findings` | No | `true` | Fail the job if vulnerabilities are found | + +## Outputs + +| Output | Description | +|--------|-------------| +| `result` | `passed` or `failed` | +| `vulnerabilities-found` | Total number of vulnerabilities found | + +## Requirements + +- `composer.lock` must exist in the working directory. The action fails immediately with a clear error if it is missing. `composer audit` reads the lockfile only — it does not install dependencies. +- Composer ≥ 2.4 must be available on the runner. It is pre-installed on all GitHub-hosted `ubuntu-latest` runners. The action verifies the version at runtime and exits with a clear error if the requirement is not met. +- No packages are installed during the scan — `composer audit` operates against the committed lockfile. + +## Notes + +- The action writes a summary table to `$GITHUB_STEP_SUMMARY` on every run. When vulnerabilities are found, a collapsible block containing the full human-readable `composer audit` output is appended. +- `composer audit` requires outbound HTTPS access to `packagist.org` and the GitHub Advisory API (`api.github.com`). Ensure these endpoints are reachable from your runner. +- To suppress a specific advisory that does not apply to your usage (e.g. a vulnerability in a code path you do not exercise), add it to the `ignored-security-advisories` list in your `composer.json`: + +```json +{ + "config": { + "audit": { + "ignored": ["CVE-2024-12345"] + } + } +} +``` + +- Start with `fail-on-findings: false` when adding this action to an existing repository to understand the current baseline before enabling hard failures. diff --git a/.github/actions/composer-audit/action.yml b/.github/actions/composer-audit/action.yml new file mode 100644 index 0000000..b84320e --- /dev/null +++ b/.github/actions/composer-audit/action.yml @@ -0,0 +1,107 @@ +name: "Composer Audit" +description: > + Run composer audit against a project's composer.lock to detect known + vulnerabilities in PHP dependencies. Requires Composer >= 2.4 which is + pre-installed on GitHub-hosted ubuntu-latest runners. No binary download + is needed — composer audit is a built-in Composer command. + +inputs: + working-directory: + description: "Directory containing composer.json and composer.lock" + required: false + default: "." + fail-on-findings: + description: "Fail the job if vulnerabilities are found (default: true)" + required: false + default: "true" + +outputs: + result: + description: "Scan result: passed or failed" + value: ${{ steps.audit.outputs.result }} + vulnerabilities-found: + description: "Total number of vulnerabilities found" + value: ${{ steps.audit.outputs.vulnerabilities-found }} + +runs: + using: "composite" + steps: + - name: Run composer audit + id: audit + shell: bash + working-directory: ${{ inputs.working-directory }} + env: + FAIL_ON_FINDINGS: ${{ inputs.fail-on-findings }} + run: | + set -euo pipefail + + REPORT="/tmp/composer-audit-report.json" + + # Verify composer.lock exists + if [ ! -f "composer.lock" ]; then + echo "::error::composer.lock not found in $(pwd). composer audit requires a lockfile." + exit 1 + fi + + # Verify Composer >= 2.4 + COMPOSER_VERSION="$(composer --version --no-ansi 2>/dev/null | grep -oP '\d+\.\d+\.\d+' | head -1)" + COMPOSER_MAJOR="$(echo "${COMPOSER_VERSION}" | cut -d. -f1)" + COMPOSER_MINOR="$(echo "${COMPOSER_VERSION}" | cut -d. -f2)" + if [ "${COMPOSER_MAJOR}" -lt 2 ] || { [ "${COMPOSER_MAJOR}" -eq 2 ] && [ "${COMPOSER_MINOR}" -lt 4 ]; }; then + echo "::error::composer audit requires Composer >= 2.4. Found: ${COMPOSER_VERSION}" + exit 1 + fi + + { + echo "## Composer Audit" + echo "" + echo "| Setting | Value |" + echo "|---------|-------|" + echo "| Working directory | \`${{ inputs.working-directory }}\` |" + echo "| Composer version | \`${COMPOSER_VERSION}\` |" + echo "" + } >> "${GITHUB_STEP_SUMMARY}" + + EXIT_CODE=0 + composer audit --format=json --no-interaction 1>"${REPORT}" 2>/dev/null || EXIT_CODE=$? + + TOTAL=0 + if command -v jq >/dev/null 2>&1 && [ -f "${REPORT}" ]; then + TOTAL="$(jq '[.advisories | to_entries[] | .value[]] | length' "${REPORT}" 2>/dev/null || echo 0)" + fi + + if [ "${EXIT_CODE}" -eq 0 ]; then + echo "result=passed" >> "${GITHUB_OUTPUT}" + echo "vulnerabilities-found=0" >> "${GITHUB_OUTPUT}" + echo "No vulnerabilities found." >> "${GITHUB_STEP_SUMMARY}" + else + echo "result=failed" >> "${GITHUB_OUTPUT}" + echo "vulnerabilities-found=${TOTAL}" >> "${GITHUB_OUTPUT}" + + # Re-run without --format=json for human-readable summary output + HUMAN_OUTPUT="$(composer audit --no-interaction 2>&1 || true)" + + { + echo "**${TOTAL} vulnerability/vulnerabilities found.**" + echo "" + echo "
Audit output" + echo "" + echo '```' + echo "${HUMAN_OUTPUT}" + echo '```' + echo "" + echo "
" + } >> "${GITHUB_STEP_SUMMARY}" + + if [ "${FAIL_ON_FINDINGS}" = "true" ]; then + echo "::error::composer audit found ${TOTAL} vulnerabilities. See the job summary for details." + exit 1 + else + echo "::warning::composer audit found ${TOTAL} vulnerabilities (scan is non-blocking)." + fi + fi + + - name: Cleanup + if: always() + shell: bash + run: rm -f /tmp/composer-audit-report.json diff --git a/.github/actions/guarddog-php-scan/README.md b/.github/actions/guarddog-php-scan/README.md new file mode 100644 index 0000000..9eb5776 --- /dev/null +++ b/.github/actions/guarddog-php-scan/README.md @@ -0,0 +1,45 @@ +# Guarddog Packagist Supply-Chain Scan + +Composite action that installs [guarddog](https://github.com/DataDog/guarddog) and verifies all PHP packages declared in `composer.json` for supply-chain threats **before any dependency is installed**. + +Guarddog detects malicious packages through static heuristics including typosquatting, code injection, data exfiltration, and other indicators of compromise. + +## ⚠️ Experimental Warning + +> **guarddog's Packagist (PHP) support exists but has received less production testing than its npm and PyPI equivalents.** Before enabling `fail-on-findings: true` in production pipelines, manually run `guarddog packagist verify composer.json` against your own project to validate that results are meaningful and noise levels are acceptable. Report unexpected behaviour to the [guarddog issue tracker](https://github.com/DataDog/guarddog/issues). +> +> It is strongly recommended to start with `fail-on-findings: false` to observe guarddog output before treating failures as blocking. + +## Usage + +> Replace `` with the current SHA from the [root README](../../../README.md#current-sha). + +```yaml +jobs: + guarddog-php: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: orangitfi/platform-tooling/.github/actions/guarddog-php-scan@ + with: + working-directory: . + fail-on-findings: false # recommended until behaviour is verified +``` + +## Inputs + +| Input | Required | Default | Description | +|-------|----------|---------|-------------| +| `working-directory` | No | `.` | Directory containing `composer.json` | +| `fail-on-findings` | No | `true` | Fail the job if supply-chain threats are detected | + +## Requirements + +- `composer.json` must exist in the working directory. The action fails immediately with a clear error if it is missing. The scan runs against the manifest only — no dependencies are installed. +- `pipx` must be available on the runner. It is pre-installed on all GitHub-hosted `ubuntu-latest` runners. guarddog is installed at runtime via `pipx install guarddog`. + +## Notes + +- The action writes a summary table and an experimental caveat to `$GITHUB_STEP_SUMMARY` on every run. When threats are detected, a collapsible block containing the full guarddog output is appended. +- guarddog queries the Packagist registry to retrieve package metadata. Ensure outbound HTTPS access to `packagist.org` is permitted on your runner. diff --git a/.github/actions/guarddog-php-scan/action.yml b/.github/actions/guarddog-php-scan/action.yml new file mode 100644 index 0000000..9724464 --- /dev/null +++ b/.github/actions/guarddog-php-scan/action.yml @@ -0,0 +1,81 @@ +name: "Guarddog Packagist Supply-Chain Scan" +description: > + Install guarddog and verify PHP packages listed in composer.json for + supply-chain threats (typosquatting, code injection, exfiltration, etc.) + BEFORE any dependency is installed. + + WARNING: guarddog Packagist support is experimental. Verify behaviour + against your own composer.json before enabling fail-on-findings in + production pipelines. See README for details. + +inputs: + working-directory: + description: "Directory containing composer.json" + required: false + default: "." + fail-on-findings: + description: "Fail the step if guarddog detects issues (default: true)" + required: false + default: "true" + +runs: + using: "composite" + steps: + - name: Install guarddog + shell: bash + run: pipx install guarddog + + - name: Run guarddog packagist verify + shell: bash + working-directory: ${{ inputs.working-directory }} + env: + FAIL_ON_FINDINGS: ${{ inputs.fail-on-findings }} + run: | + set -euo pipefail + + if [ ! -f "composer.json" ]; then + echo "::error::composer.json not found in $(pwd). guarddog packagist verify requires a composer.json." + exit 1 + fi + + { + echo "## Guarddog Packagist Supply-Chain Scan" + echo "" + echo "> ⚠️ **Experimental:** guarddog Packagist support has received less production testing than npm/PyPI. Verify results manually before treating failures as blocking." + echo "" + echo "| Setting | Value |" + echo "|---------|-------|" + echo "| Working directory | \`${{ inputs.working-directory }}\` |" + echo "" + } >> "${GITHUB_STEP_SUMMARY}" + + EXIT_CODE=0 + guarddog packagist verify composer.json 2>&1 | tee /tmp/guarddog-php-output.txt || EXIT_CODE=$? + + if [ "${EXIT_CODE}" -eq 0 ]; then + echo "No supply-chain threats detected." >> "${GITHUB_STEP_SUMMARY}" + else + { + echo "**Supply-chain threats detected.**" + echo "" + echo "
Guarddog output" + echo "" + echo '```' + cat /tmp/guarddog-php-output.txt + echo '```' + echo "" + echo "
" + } >> "${GITHUB_STEP_SUMMARY}" + + if [ "${FAIL_ON_FINDINGS}" = "true" ]; then + echo "::error::guarddog detected potential supply-chain threats in Packagist packages. See the job summary for details." + exit 1 + else + echo "::warning::guarddog detected potential supply-chain threats (scan is non-blocking)." + fi + fi + + - name: Cleanup + if: always() + shell: bash + run: rm -f /tmp/guarddog-php-output.txt From 7c5673d07c5b6d7b7aca9c3e7ff951f5961c23b3 Mon Sep 17 00:00:00 2001 From: Pekka Piispanen Date: Mon, 27 Apr 2026 15:20:46 +0300 Subject: [PATCH 2/5] feature: Add php-security-scan and php-vulnerability-scan reusable workflows --- .github/workflows/php-security-scan.md | 80 +++++++++++++++ .github/workflows/php-security-scan.yml | 83 +++++++++++++++ .github/workflows/php-vulnerability-scan.md | 74 ++++++++++++++ .github/workflows/php-vulnerability-scan.yml | 101 +++++++++++++++++++ 4 files changed, 338 insertions(+) create mode 100644 .github/workflows/php-security-scan.md create mode 100644 .github/workflows/php-security-scan.yml create mode 100644 .github/workflows/php-vulnerability-scan.md create mode 100644 .github/workflows/php-vulnerability-scan.yml diff --git a/.github/workflows/php-security-scan.md b/.github/workflows/php-security-scan.md new file mode 100644 index 0000000..f0adbfe --- /dev/null +++ b/.github/workflows/php-security-scan.md @@ -0,0 +1,80 @@ +# php-security-scan + +Reusable workflow that runs three security scans **in parallel** against PHP / Symfony repositories before build or test steps run. All three jobs must pass for the workflow to succeed. + +> Replace `` with the current SHA from the [root README](../../README.md#current-sha). + +## Jobs + +| Job | Tool | What it checks | +|-----|------|----------------| +| `gitleaks` | gitleaks v8.24.3 | Secrets and credentials committed to git history | +| `composer-audit` | composer audit (built-in, Composer ≥ 2.4) | Known CVEs in PHP dependencies (via composer.lock) | +| `guarddog-php` | guarddog ⚠️ experimental | Supply-chain threats in Packagist packages (typosquatting, exfiltration, malicious code) | + +## Usage + +### Minimal — run on every push and PR + +```yaml +on: [push, pull_request] + +jobs: + security: + uses: orangitfi/platform-tooling/.github/workflows/php-security-scan.yml@ +``` + +### With custom options + +```yaml +jobs: + security: + uses: orangitfi/platform-tooling/.github/workflows/php-security-scan.yml@ + with: + working-directory: ./app + fail-on-findings: false # warn but don't block while onboarding +``` + +### Recommended position in a pipeline + +```yaml +jobs: + security: + uses: orangitfi/platform-tooling/.github/workflows/php-security-scan.yml@ + + test: + needs: security + uses: orangitfi/platform-tooling/.github/workflows/php-ci.yml@ +``` + +## Parameters + +| Input | Default | Description | +|-------|---------|-------------| +| `working-directory` | `.` | Directory containing `composer.json` and `composer.lock` | +| `fail-on-findings` | `true` | Fail the workflow if any scan detects issues | + +## What it does + +- **gitleaks**: checks full git history (`fetch-depth: 0`) for leaked API keys, tokens, passwords, and other secrets +- **composer-audit**: reads the committed `composer.lock` and queries the GitHub Security Advisory database and Packagist security advisories feed for known CVEs — no packages are installed +- **guarddog-php**: checks each package in `composer.json` against heuristic rules for supply-chain attack indicators — no packages are installed + +All three jobs run on separate runners and produce output in the GitHub Actions job summary. + +## ⚠️ Note on guarddog Packagist support + +The `guarddog-php` job uses guarddog's `packagist` subcommand, which exists but has received less real-world testing than the `npm` and `pypi` equivalents. False positives or unexpected failures are possible. It is strongly recommended to run the workflow with `fail-on-findings: false` initially and manually inspect guarddog output before enabling hard failures. See the [guarddog-php-scan action README](../actions/guarddog-php-scan/README.md) for details. + +## When it has value + +- **Pre-install gate**: both `composer-audit` and `guarddog-php` run before any `composer install`. A malicious or vulnerable package is caught before it ever executes on your runner. +- **Complementary coverage**: `composer-audit` finds CVE-tracked vulnerabilities (entries in the GitHub Advisory database); guarddog finds behavioural threats that have no CVE entry yet (obfuscated code, suspicious registry metadata, typosquatted names). Together they cover both vectors. +- **Parallel execution**: all three scans run simultaneously — total wall-clock time is the slowest scan (~30–60 s), not the sum. + +## Tips + +- `composer.lock` must be committed. `composer audit` operates against the lockfile and will fail with a clear error if it is missing. Generate it with `composer install` or `composer update`. +- If gitleaks flags a test fixture as a false positive, add an allowlist entry to `.gitleaks.toml` in the consuming repo. See the [gitleaks-scan action README](../actions/gitleaks-scan/README.md#handling-false-positives). +- To suppress a specific `composer-audit` advisory, add it to the `config.audit.ignored` list in `composer.json`. See the [composer-audit action README](../actions/composer-audit/README.md#notes). +- Start with `fail-on-findings: false` when adding to an existing repository to understand the current baseline before enabling hard failures. diff --git a/.github/workflows/php-security-scan.yml b/.github/workflows/php-security-scan.yml new file mode 100644 index 0000000..263dcaa --- /dev/null +++ b/.github/workflows/php-security-scan.yml @@ -0,0 +1,83 @@ +# PHP Security Scan +# +# Reusable workflow that runs three security scans in parallel against +# PHP / Symfony repositories: +# +# 1. gitleaks — detects secrets committed to the repository +# 2. composer-audit — detects known CVEs in PHP dependencies (via composer.lock) +# 3. guarddog-php — detects supply-chain threats in Packagist packages +# +# All three jobs run in parallel; the workflow fails if any one of them fails. +# +# WARNING: The guarddog Packagist scan is experimental. See the +# guarddog-php-scan action README for details before enabling fail-on-findings. +# +# Usage in a consuming repository: +# +# jobs: +# security: +# uses: orangitfi/platform-tooling/.github/workflows/php-security-scan.yml@ +# with: +# working-directory: . +# +# Consuming repo controls WHEN this runs. This workflow controls HOW. + +name: PHP Security Scan + +on: + workflow_call: + inputs: + working-directory: + description: "Directory containing composer.json and composer.lock" + required: false + default: "." + type: string + fail-on-findings: + description: "Fail the workflow if any scan detects issues" + required: false + default: true + type: boolean + +permissions: + contents: read + +jobs: + gitleaks: + name: Secret Scan (gitleaks) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + + - name: Run gitleaks scan + uses: orangitfi/platform-tooling/.github/actions/gitleaks-scan@REPLACE_WITH_COMMIT_SHA # pt-sha + with: + fail-on-findings: ${{ inputs.fail-on-findings }} + + composer-audit: + name: Vulnerability Scan (composer audit) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Run composer audit + uses: orangitfi/platform-tooling/.github/actions/composer-audit@REPLACE_WITH_COMMIT_SHA # pt-sha + with: + working-directory: ${{ inputs.working-directory }} + fail-on-findings: ${{ inputs.fail-on-findings }} + + guarddog-php: + name: Supply Chain Scan (guarddog) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Run guarddog Packagist scan + uses: orangitfi/platform-tooling/.github/actions/guarddog-php-scan@REPLACE_WITH_COMMIT_SHA # pt-sha + with: + working-directory: ${{ inputs.working-directory }} + fail-on-findings: ${{ inputs.fail-on-findings }} diff --git a/.github/workflows/php-vulnerability-scan.md b/.github/workflows/php-vulnerability-scan.md new file mode 100644 index 0000000..dfc4716 --- /dev/null +++ b/.github/workflows/php-vulnerability-scan.md @@ -0,0 +1,74 @@ +# php-vulnerability-scan + +Reusable workflow that generates a Software Bill of Materials (SBOM) with Syft and scans it for known CVEs with Grype. Runs two scans **in parallel**: one against the source directory and an optional one against the built Docker image. + +> Replace `` with the current SHA from the [root README](../../README.md#current-sha). + +## Jobs + +| Job | What it scans | +|-----|---------------| +| `code-scan` | Source directory — OS packages, PHP packages, and application code via Syft SBOM | +| `docker-scan` | Built Docker image — all layers, including base image and system packages (skippable) | + +## Usage + +### Source code only (no Dockerfile) + +```yaml +on: + schedule: + - cron: "0 2 * * *" # nightly + +jobs: + vuln-scan: + uses: orangitfi/platform-tooling/.github/workflows/php-vulnerability-scan.yml@ + with: + run-docker-scan: false +``` + +### Source code + Docker image + +```yaml +jobs: + vuln-scan: + uses: orangitfi/platform-tooling/.github/workflows/php-vulnerability-scan.yml@ + with: + image-name: my-php-app +``` + +### Stricter threshold + +```yaml +jobs: + vuln-scan: + uses: orangitfi/platform-tooling/.github/workflows/php-vulnerability-scan.yml@ + with: + image-name: my-php-app + fail-on-severity: medium +``` + +## Parameters + +| Input | Default | Description | +|-------|---------|-------------| +| `working-directory` | `.` | Directory to scan for source code | +| `fail-on-severity` | `high` | Minimum Grype severity to fail on: `critical`, `high`, `medium`, `low`, `negligible` | +| `run-docker-scan` | `true` | Build and scan the Docker image; set `false` for repos without a Dockerfile | +| `dockerfile-path` | `Dockerfile` | Path to the Dockerfile relative to `working-directory` | +| `image-name` | — | Docker image name (defaults to repository name if not set) | +| `image-tag` | — | Docker image tag (defaults to commit SHA if not set) | + +## When it has value + +- **Beyond composer-audit**: `composer audit` checks your declared Composer dependencies against CVE databases. Grype scans the entire source tree and container image, including the PHP runtime, base OS packages, C extensions, and system libraries that `composer audit` never sees. +- **Scheduled scanning**: new CVEs are published daily. A dependency that was safe yesterday may be vulnerable today. Nightly scanning keeps your exposure window small even without code changes. +- **Docker base image hygiene**: the most common source of container CVEs is an outdated base image (`php:8.3-fpm` from 6 months ago vs today). The Docker scan job catches these independently of your PHP dependencies. +- **SBOM for compliance**: the Syft SBOM is a machine-readable inventory accepted by auditors and security tools. + +## Tips + +- Run this workflow on a **schedule** rather than on every push — vulnerability databases update independently of your code. +- The `--only-fixed` flag in Grype means only CVEs with an available fix contribute to the failure threshold. This avoids blocking on unfixable CVEs in base images. +- If the Docker scan fails because of an outdated base image, update `FROM` in your Dockerfile to a newer patch release (e.g. `php:8.3.0-fpm` → `php:8.3.10-fpm`). +- Set `fail-on-severity: critical` for a more permissive starting threshold and tighten to `high` once the backlog of known vulnerabilities is cleared. diff --git a/.github/workflows/php-vulnerability-scan.yml b/.github/workflows/php-vulnerability-scan.yml new file mode 100644 index 0000000..3517fa5 --- /dev/null +++ b/.github/workflows/php-vulnerability-scan.yml @@ -0,0 +1,101 @@ +# PHP Vulnerability Scan +# +# Reusable workflow that generates a Software Bill of Materials (SBOM) with +# Syft and scans it for known CVEs with Grype. Runs two scans in parallel: +# one against the source directory and an optional one against the built +# Docker image. +# +# 1. code-scan — generates an SBOM for the source directory and scans it +# 2. docker-scan — builds the Docker image, generates an SBOM and scans it +# skipped when run-docker-scan is set to false +# +# Both jobs fail independently; the workflow fails if either fails. +# +# Usage in a consuming repository: +# +# # Source code only (no Dockerfile) +# jobs: +# vulnerability-scan: +# uses: orangitfi/platform-tooling/.github/workflows/php-vulnerability-scan.yml@ +# with: +# run-docker-scan: false +# +# # Source code + Docker image +# jobs: +# vulnerability-scan: +# uses: orangitfi/platform-tooling/.github/workflows/php-vulnerability-scan.yml@ +# with: +# image-name: my-app + +name: PHP Vulnerability Scan + +on: + workflow_call: + inputs: + working-directory: + description: "Directory to scan for source code vulnerabilities" + required: false + default: "." + type: string + fail-on-severity: + description: "Minimum severity to fail on: critical, high, medium, low, negligible" + required: false + default: "high" + type: string + run-docker-scan: + description: "Build and scan the Docker image (set to false for repos without a Dockerfile)" + required: false + default: true + type: boolean + dockerfile-path: + description: "Path to the Dockerfile relative to working-directory" + required: false + default: "Dockerfile" + type: string + image-name: + description: "Name for the Docker image. Defaults to the repository name." + required: false + default: "" + type: string + image-tag: + description: "Tag for the Docker image. Defaults to the commit SHA." + required: false + default: "" + type: string + +permissions: + contents: read + +jobs: + code-scan: + name: Code Vulnerability Scan (Syft + Grype) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Install Syft and Grype + uses: orangitfi/platform-tooling/.github/actions/scheduled_test_setup@REPLACE_WITH_COMMIT_SHA # pt-sha + + - name: Scan codebase + uses: orangitfi/platform-tooling/.github/actions/security-scan-code@REPLACE_WITH_COMMIT_SHA # pt-sha + with: + directory: ${{ inputs.working-directory }} + fail-on-severity: ${{ inputs.fail-on-severity }} + + docker-scan: + name: Docker Vulnerability Scan (Syft + Grype) + if: ${{ inputs.run-docker-scan }} + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Scan Docker image + uses: orangitfi/platform-tooling/.github/actions/docker-scan@REPLACE_WITH_COMMIT_SHA # pt-sha + with: + working-directory: ${{ inputs.working-directory }} + dockerfile-path: ${{ inputs.dockerfile-path }} + image-name: ${{ inputs.image-name || github.event.repository.name }} + image-tag: ${{ inputs.image-tag || github.sha }} + fail-on-severity: ${{ inputs.fail-on-severity }} From a715848612fbde798cd68449d5b724054cd2a49a Mon Sep 17 00:00:00 2001 From: Pekka Piispanen Date: Tue, 28 Apr 2026 12:07:16 +0300 Subject: [PATCH 3/5] feat: add ignore-severity input to composer-audit action and php-security-scan workflow - Bump minimum Composer version requirement from 2.4 to 2.8 (--ignore-severity was added in Composer 2.8.0, Oct 2024) - Add ignore-severity input: space-separated list of severity levels to suppress (low/medium/high/critical); builds repeated --ignore-severity flags as required by the Composer CLI - Thread ignore-severity input through php-security-scan.yml for parity with node-security-scan.yml audit-level input --- .github/actions/composer-audit/action.yml | 32 ++++++++++++++++++----- .github/workflows/php-security-scan.yml | 10 +++++++ 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/.github/actions/composer-audit/action.yml b/.github/actions/composer-audit/action.yml index b84320e..b0aaf5a 100644 --- a/.github/actions/composer-audit/action.yml +++ b/.github/actions/composer-audit/action.yml @@ -1,7 +1,7 @@ name: "Composer Audit" description: > Run composer audit against a project's composer.lock to detect known - vulnerabilities in PHP dependencies. Requires Composer >= 2.4 which is + vulnerabilities in PHP dependencies. Requires Composer >= 2.8 which is pre-installed on GitHub-hosted ubuntu-latest runners. No binary download is needed — composer audit is a built-in Composer command. @@ -14,6 +14,14 @@ inputs: description: "Fail the job if vulnerabilities are found (default: true)" required: false default: "true" + ignore-severity: + description: > + Space-separated list of severity levels to ignore. Accepted values: + low, medium, high, critical. Example: "low medium" ignores low and + medium advisories and only fails on high/critical. + Requires Composer >= 2.8. Leave empty to report all severities. + required: false + default: "" outputs: result: @@ -32,6 +40,7 @@ runs: working-directory: ${{ inputs.working-directory }} env: FAIL_ON_FINDINGS: ${{ inputs.fail-on-findings }} + IGNORE_SEVERITY: ${{ inputs.ignore-severity }} run: | set -euo pipefail @@ -43,15 +52,21 @@ runs: exit 1 fi - # Verify Composer >= 2.4 + # Verify Composer >= 2.8 (required for --ignore-severity) COMPOSER_VERSION="$(composer --version --no-ansi 2>/dev/null | grep -oP '\d+\.\d+\.\d+' | head -1)" COMPOSER_MAJOR="$(echo "${COMPOSER_VERSION}" | cut -d. -f1)" COMPOSER_MINOR="$(echo "${COMPOSER_VERSION}" | cut -d. -f2)" - if [ "${COMPOSER_MAJOR}" -lt 2 ] || { [ "${COMPOSER_MAJOR}" -eq 2 ] && [ "${COMPOSER_MINOR}" -lt 4 ]; }; then - echo "::error::composer audit requires Composer >= 2.4. Found: ${COMPOSER_VERSION}" + if [ "${COMPOSER_MAJOR}" -lt 2 ] || { [ "${COMPOSER_MAJOR}" -eq 2 ] && [ "${COMPOSER_MINOR}" -lt 8 ]; }; then + echo "::error::composer audit requires Composer >= 2.8. Found: ${COMPOSER_VERSION}" exit 1 fi + # Build --ignore-severity flags (flag is repeatable, not comma-separated) + SEVERITY_FLAGS="" + for SEV in ${IGNORE_SEVERITY}; do + SEVERITY_FLAGS="${SEVERITY_FLAGS} --ignore-severity=${SEV}" + done + { echo "## Composer Audit" echo "" @@ -59,11 +74,15 @@ runs: echo "|---------|-------|" echo "| Working directory | \`${{ inputs.working-directory }}\` |" echo "| Composer version | \`${COMPOSER_VERSION}\` |" + if [ -n "${IGNORE_SEVERITY}" ]; then + echo "| Ignored severities | \`${IGNORE_SEVERITY}\` |" + fi echo "" } >> "${GITHUB_STEP_SUMMARY}" EXIT_CODE=0 - composer audit --format=json --no-interaction 1>"${REPORT}" 2>/dev/null || EXIT_CODE=$? + # shellcheck disable=SC2086 + composer audit --format=json --no-interaction ${SEVERITY_FLAGS} 1>"${REPORT}" 2>/dev/null || EXIT_CODE=$? TOTAL=0 if command -v jq >/dev/null 2>&1 && [ -f "${REPORT}" ]; then @@ -79,7 +98,8 @@ runs: echo "vulnerabilities-found=${TOTAL}" >> "${GITHUB_OUTPUT}" # Re-run without --format=json for human-readable summary output - HUMAN_OUTPUT="$(composer audit --no-interaction 2>&1 || true)" + # shellcheck disable=SC2086 + HUMAN_OUTPUT="$(composer audit --no-interaction ${SEVERITY_FLAGS} 2>&1 || true)" { echo "**${TOTAL} vulnerability/vulnerabilities found.**" diff --git a/.github/workflows/php-security-scan.yml b/.github/workflows/php-security-scan.yml index 263dcaa..264e669 100644 --- a/.github/workflows/php-security-scan.yml +++ b/.github/workflows/php-security-scan.yml @@ -37,6 +37,15 @@ on: required: false default: true type: boolean + ignore-severity: + description: > + Space-separated severity levels to ignore in composer audit. + Accepted values: low, medium, high, critical. + Example: "low medium" — only fails on high/critical advisories. + Requires Composer >= 2.8 (pre-installed on ubuntu-latest). + required: false + default: "" + type: string permissions: contents: read @@ -68,6 +77,7 @@ jobs: with: working-directory: ${{ inputs.working-directory }} fail-on-findings: ${{ inputs.fail-on-findings }} + ignore-severity: ${{ inputs.ignore-severity }} guarddog-php: name: Supply Chain Scan (guarddog) From 6e5aeccc10ec4cc4447f2049d4f984f599e83789 Mon Sep 17 00:00:00 2001 From: Pekka Piispanen Date: Tue, 28 Apr 2026 12:18:53 +0300 Subject: [PATCH 4/5] feat: use bash array for composer audit severity flags --- .github/actions/composer-audit/action.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/actions/composer-audit/action.yml b/.github/actions/composer-audit/action.yml index b0aaf5a..b1816d9 100644 --- a/.github/actions/composer-audit/action.yml +++ b/.github/actions/composer-audit/action.yml @@ -62,9 +62,9 @@ runs: fi # Build --ignore-severity flags (flag is repeatable, not comma-separated) - SEVERITY_FLAGS="" + SEVERITY_FLAGS=() for SEV in ${IGNORE_SEVERITY}; do - SEVERITY_FLAGS="${SEVERITY_FLAGS} --ignore-severity=${SEV}" + SEVERITY_FLAGS+=("--ignore-severity=${SEV}") done { @@ -81,8 +81,7 @@ runs: } >> "${GITHUB_STEP_SUMMARY}" EXIT_CODE=0 - # shellcheck disable=SC2086 - composer audit --format=json --no-interaction ${SEVERITY_FLAGS} 1>"${REPORT}" 2>/dev/null || EXIT_CODE=$? + composer audit --format=json --no-interaction "${SEVERITY_FLAGS[@]}" 1>"${REPORT}" 2>/dev/null || EXIT_CODE=$? TOTAL=0 if command -v jq >/dev/null 2>&1 && [ -f "${REPORT}" ]; then @@ -98,8 +97,7 @@ runs: echo "vulnerabilities-found=${TOTAL}" >> "${GITHUB_OUTPUT}" # Re-run without --format=json for human-readable summary output - # shellcheck disable=SC2086 - HUMAN_OUTPUT="$(composer audit --no-interaction ${SEVERITY_FLAGS} 2>&1 || true)" + HUMAN_OUTPUT="$(composer audit --no-interaction "${SEVERITY_FLAGS[@]}" 2>&1 || true)" { echo "**${TOTAL} vulnerability/vulnerabilities found.**" From 7d8aadaa2af8f0f5a9b302cfa83895a5560b5249 Mon Sep 17 00:00:00 2001 From: Pekka Piispanen Date: Tue, 28 Apr 2026 12:25:37 +0300 Subject: [PATCH 5/5] docs: document ignore-severity input and its inverted logic vs npm audit-level --- .github/actions/composer-audit/README.md | 5 +++-- .github/workflows/php-security-scan.md | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/actions/composer-audit/README.md b/.github/actions/composer-audit/README.md index 9897e76..53436cd 100644 --- a/.github/actions/composer-audit/README.md +++ b/.github/actions/composer-audit/README.md @@ -1,6 +1,6 @@ # Composer Audit -Composite action that runs `composer audit` against a project's `composer.lock` to detect known vulnerabilities in PHP dependencies. `composer audit` is a built-in Composer command (available since Composer 2.4) that queries the GitHub Security Advisory database and the Packagist security advisories feed — no binary download or extra installation is required. +Composite action that runs `composer audit` against a project's `composer.lock` to detect known vulnerabilities in PHP dependencies. `composer audit` is a built-in Composer command (available since Composer 2.4; severity filtering requires 2.8) that queries the GitHub Security Advisory database and the Packagist security advisories feed — no binary download or extra installation is required. ## Usage @@ -25,6 +25,7 @@ jobs: |-------|----------|---------|-------------| | `working-directory` | No | `.` | Directory containing `composer.json` and `composer.lock` | | `fail-on-findings` | No | `true` | Fail the job if vulnerabilities are found | +| `ignore-severity` | No | `""` | Space-separated severity levels to ignore: `low`, `medium`, `high`, `critical`. Unlike npm's `audit-level` (minimum threshold), this is inverted — you specify what to *ignore*. To fail only on high and critical, set `"low medium"`. Leave empty to report all severities. | ## Outputs @@ -36,7 +37,7 @@ jobs: ## Requirements - `composer.lock` must exist in the working directory. The action fails immediately with a clear error if it is missing. `composer audit` reads the lockfile only — it does not install dependencies. -- Composer ≥ 2.4 must be available on the runner. It is pre-installed on all GitHub-hosted `ubuntu-latest` runners. The action verifies the version at runtime and exits with a clear error if the requirement is not met. +- Composer ≥ 2.8 must be available on the runner. It is pre-installed on all GitHub-hosted `ubuntu-latest` runners. The action verifies the version at runtime and exits with a clear error if the requirement is not met. - No packages are installed during the scan — `composer audit` operates against the committed lockfile. ## Notes diff --git a/.github/workflows/php-security-scan.md b/.github/workflows/php-security-scan.md index f0adbfe..a9f9b80 100644 --- a/.github/workflows/php-security-scan.md +++ b/.github/workflows/php-security-scan.md @@ -9,7 +9,7 @@ Reusable workflow that runs three security scans **in parallel** against PHP / S | Job | Tool | What it checks | |-----|------|----------------| | `gitleaks` | gitleaks v8.24.3 | Secrets and credentials committed to git history | -| `composer-audit` | composer audit (built-in, Composer ≥ 2.4) | Known CVEs in PHP dependencies (via composer.lock) | +| `composer-audit` | composer audit (built-in, Composer ≥ 2.8) | Known CVEs in PHP dependencies (via composer.lock) | | `guarddog-php` | guarddog ⚠️ experimental | Supply-chain threats in Packagist packages (typosquatting, exfiltration, malicious code) | ## Usage @@ -53,6 +53,7 @@ jobs: |-------|---------|-------------| | `working-directory` | `.` | Directory containing `composer.json` and `composer.lock` | | `fail-on-findings` | `true` | Fail the workflow if any scan detects issues | +| `ignore-severity` | `""` | Space-separated severity levels to ignore in composer audit: `low`, `medium`, `high`, `critical`. Inverted compared to npm's `audit-level` — you list what to ignore rather than the minimum to fail on. Example: `"low medium"` fails only on high/critical. | ## What it does @@ -77,4 +78,5 @@ The `guarddog-php` job uses guarddog's `packagist` subcommand, which exists but - `composer.lock` must be committed. `composer audit` operates against the lockfile and will fail with a clear error if it is missing. Generate it with `composer install` or `composer update`. - If gitleaks flags a test fixture as a false positive, add an allowlist entry to `.gitleaks.toml` in the consuming repo. See the [gitleaks-scan action README](../actions/gitleaks-scan/README.md#handling-false-positives). - To suppress a specific `composer-audit` advisory, add it to the `config.audit.ignored` list in `composer.json`. See the [composer-audit action README](../actions/composer-audit/README.md#notes). +- To tighten or loosen the vulnerability threshold, use `ignore-severity` (e.g. `"low medium"` to fail only on high/critical). Note the inverted logic compared to npm's `audit-level` — Composer requires you to specify which severities to suppress rather than a minimum failing level. - Start with `fail-on-findings: false` when adding to an existing repository to understand the current baseline before enabling hard failures.