Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .github/actions/composer-audit/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# 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; 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

> Replace `<current-sha>` 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@<current-sha>
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 |
| `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

| 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.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

- 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.
125 changes: 125 additions & 0 deletions .github/actions/composer-audit/action.yml
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All others have the severity threshold. Should this have also. It maybe impossible to get all minors fixed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Composer (2.8+) has inverted logic for this, it has ignore-severity instead of setting the lowest level to target. Added this ignore-severity input to composer-audit/action.yml and threaded it through php-security-scan.yml. Bumped the minimum Composer version check from 2.4 to 2.8 accordingly. ubuntu-latest ships 2.8+ so no practical impact for consumers.

Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
name: "Composer Audit"
description: >
Run composer audit against a project's composer.lock to detect known
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.

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"
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:
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 }}
IGNORE_SEVERITY: ${{ inputs.ignore-severity }}
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.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 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+=("--ignore-severity=${SEV}")
done

{
echo "## Composer Audit"
echo ""
echo "| Setting | Value |"
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 "${SEVERITY_FLAGS[@]}" 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 "${SEVERITY_FLAGS[@]}" 2>&1 || true)"

{
echo "**${TOTAL} vulnerability/vulnerabilities found.**"
echo ""
echo "<details><summary>Audit output</summary>"
echo ""
echo '```'
echo "${HUMAN_OUTPUT}"
echo '```'
echo ""
echo "</details>"
} >> "${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
45 changes: 45 additions & 0 deletions .github/actions/guarddog-php-scan/README.md
Original file line number Diff line number Diff line change
@@ -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 `<current-sha>` 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@<current-sha>
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.
81 changes: 81 additions & 0 deletions .github/actions/guarddog-php-scan/action.yml
Original file line number Diff line number Diff line change
@@ -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 "<details><summary>Guarddog output</summary>"
echo ""
echo '```'
cat /tmp/guarddog-php-output.txt
echo '```'
echo ""
echo "</details>"
} >> "${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
82 changes: 82 additions & 0 deletions .github/workflows/php-security-scan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# 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 `<current-sha>` 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.8) | 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@<current-sha>
```

### With custom options

```yaml
jobs:
security:
uses: orangitfi/platform-tooling/.github/workflows/php-security-scan.yml@<current-sha>
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@<current-sha>

test:
needs: security
uses: orangitfi/platform-tooling/.github/workflows/php-ci.yml@<current-sha>
```

## 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 |
| `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

- **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).
- 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.
Loading
Loading