Guides

Markdown

CI

Excubia is built to gate pipelines: it exits 1 when a finding is at or above --fail-on, and writes JSON, Markdown or SARIF for the job to keep. The standalone CLI does not boot the app, so a CI job needs only PHP and the project's composer.lock; no database, no .env, no composer install of the project itself.

GitHub Action

The composite action in action.yml runs excubia check, writes the Markdown report to the job summary and uploads the SARIF log to GitHub code scanning, so findings show up in the Security tab and on pull requests.

# .github/workflows/excubia.yml
name: Excubia

on:
  push:
    branches: [main]
  pull_request:
  schedule:
    - cron: '0 6 * * 1' # weekly: new advisories and EOL dates appear without code changes

permissions:
  contents: read
  security-events: write # for the SARIF upload

jobs:
  excubia:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: qiick-io/excubia-core@v1
        with:
          fail-on: critical
          github-token: ${{ github.token }}
InputDefaultMeaning
path.Project directory to scan, relative to the workspace.
fail-oncriticalcritical, high, medium, low or none, as --fail-on. Note the default is critical here and high in the CLI.
only—Comma-separated check ids to run.
skip—Comma-separated check ids to skip.
github-token—Optional. Raises the GitHub API rate limit for composer-stale. Passed to Excubia as GITHUB_TOKEN only.
upload-sariftruefalse skips the code scanning upload (and the need for security-events: write).
php-version8.4PHP used to run Excubia (8.2 or newer), not the scanned project's PHP.
OutputMeaning
sarif-filePath of the SARIF log.
exit-codeExit code of excubia check.

How it works:

  • Excubia is installed with Composer in $RUNNER_TEMP/excubia from the action's own checkout, so the version matches the ref you use (@v1, a tag or a commit SHA). Its dependencies (symfony/console, symfony/http-client, composer/semver…) come from the lock committed in .github/action-install/ and resolved for PHP 8.2, so the action runs the same reviewed versions on every run and every php-version from 8.2 up; Composer plugins and scripts are disabled. The scanned project's code and dependencies are never installed or executed.
  • The SARIF log is uploaded even when the step then fails on fail-on. Pull requests from forks get a read-only token, so the upload is skipped there.
  • excubia.json in path is read as usual, so ignore rules and thresholds apply.
  • Pin the action to a commit SHA if your policy requires it.

SARIF

--format=sarif writes a SARIF 2.1.0 log:

  • Each check id is a rule; each fail or warn finding is a result. pass and skipped rows and the upgrade plan are left out.
  • Levels: critical and high → error, medium → warning, low and info → note. The Excubia severity stays in properties.severity.
  • The location is the manifest the finding comes from (composer.json, composer.lock, package-lock.json, .nvmrc, .node-version or package.json; for the posture checks config/app.php, config/queue.php, database/migrations or routes/console.php), relative to the project. Never an absolute path.
  • partialFingerprints come from the check id, subject and advisory id, so an alert keeps its identity across runs.

Any other CI

Install Excubia as a dev dependency and run it. The exit code fails the job.

composer install --no-interaction --no-scripts   # or only: composer require --dev qiick/excubia-core
vendor/bin/excubia check --no-banner --fail-on=high

Keep the reports as artifacts:

vendor/bin/excubia check --no-banner --format=json --output=excubia.json --fail-on=none
vendor/bin/excubia check --no-banner --format=markdown --output=excubia.md

The second run reuses the response cache of the first one. Typical choices:

NeedCommand
Fail only on critical findings--fail-on=critical
Report, never fail--fail-on=none
Fail when a data source was down, rather than pass with skipped checks--strict (exit 3)
Production dependencies only--no-dev
No network in the jobRestore a cached directory set with cache.directory, then --offline
Scan a sub-directoryvendor/bin/excubia check path/to/app

GitLab CI example:

excubia:
  image: php:8.4-cli
  before_script:
    - curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
    - composer install --no-interaction --no-scripts --no-progress
  script:
    - vendor/bin/excubia check --no-banner --format=markdown --output=excubia.md
  artifacts:
    when: always
    paths: [excubia.md]

Exit codes: 0 clean, 1 finding at or above --fail-on, 2 usage or configuration error, 3 data-source failure in strict mode. Precedence 2 > 1 > 3. See the README.

Push from CI

To send the report to your own excubia-app after a deploy, see Push to excubia-app.

Posture checks in CI

The six laravel-* posture checks read the booted app, so they only run through php artisan excubia:check. In CI they would describe the CI environment, not production, so use vendor/bin/excubia there (the posture checks are skipped), and run the posture checks on the server or through the scheduled push.


Source: docs/guides/ci.md in excubia-core.