# Send events to OpenWAF

> Send each CI job's destinations, blocks and detections to your organization's Egress events with GitHub OIDC, without storing a secret in the repository.

By default the action keeps everything in the run: the job summary and `report.json`. With `send-events: on` it also sends each job's destinations to your OpenWAF organization, where they can be read from the API across repositories and runs.

No secret is stored in the repository. The job proves who it is with a GitHub OIDC token, which OpenWAF exchanges for a token that only lets it send that run's events.

## Before you start

- **Early access.** Egress is turned on per organization; until it is, registration answers `403 feature_disabled`. Write to support@openwaf.dev to join.
- **An organization-wide `admin`** to register repositories. Reading the registered repositories needs any role of the whole organization.
- **The repository's GitHub ids**, which pin the registration to that repository: a repository renamed, transferred or deleted and created again with the same name is not the same repository and must be registered again.

```sh
gh api repos/acme/app --jq '.id, .owner.id'
```

## 1. Register the repository

A GitHub repository can be registered in one organization only. `refs` limits which refs may send events (exact refs, no wildcards, up to 20); empty means any.

**API**

```sh
curl -sS -X POST "https://dash.openwaf.dev/v1/orgs/$ORG/egress/ci/repositories" \
  -H "Authorization: Bearer $OPENWAF_TOKEN" -H "Content-Type: application/json" \
  -H "X-Change-Reason: CI egress events for acme/app" \
  -d '{"repository": "acme/app", "repository_id": 123456789, "repository_owner_id": 987654, "refs": ["refs/heads/main"]}'
```

Answers `201` with the registration (`id`, `repository`, `repository_id`, `repository_owner_id`, `refs`, `created_at`, `created_by`). `GET` on the same path lists them under `items`; `DELETE …/egress/ci/repositories/{id}` removes one (`204`), and the tokens it was given stop working at once.
**CLI**

```sh
openwaf-cloud ctl egress ci-repos add acme/app --repository-id 123456789 --owner-id 987654 \
  --ref refs/heads/main --reason "CI egress events for acme/app"
openwaf-cloud ctl egress ci-repos list
openwaf-cloud ctl egress ci-repos rm acme/app --reason "repository archived"
```

`--dry-run` shows the change without making it.


Registrations are recorded in the [audit log](https://developers.openwaf.dev/change-control/audit-log/) with their reason.

## 2. Turn it on in the workflow

```yaml
permissions:
  contents: read
  id-token: write   # lets the job ask GitHub for an OIDC token
jobs:
  test:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v5
      - uses: openwaf-dev/egress-action@v0.3.2
        with:
          send-events: "on"
      - run: make test
```

At the end of the job the summary says how many destinations were sent, for example `14 destinations sent to OpenWAF Egress events (https://dash.openwaf.dev).`

## What is sent

One event per destination of the job, with the run's identity. A name that was only resolved, with nothing connected, refused or detected, is left out.

| Part | Fields |
|---|---|
| Run | Repository, run id and attempt, workflow, job, commit SHA, ref, event name, runner name |
| Summary | Mode, number of destinations, blocked, would block, detections, DNS queries |
| Each destination | Host, first port, path (`proxy`, `direct`, `container`, `udp`, `dns`), container name, decision (`allow`, `audit`, `block`), reason, detection ids, connections, bytes up and down |

Never request bodies, headers, URLs or the secrets a detection found.

## How it works

1. The post step asks GitHub for an OIDC token with the audience `openwaf-egress-ci`. It never prints or logs it.
2. It sends it to `POST /v1/egress/ci/token` on `openwaf-api` (default `https://dash.openwaf.dev`). OpenWAF checks GitHub's signature, the issuer, the audience, the time and that the token has not been used before, and that the repository, its id and its owner's id match a registration (and the ref, when `refs` is set). It answers with a token (`OWE_CI_…`) valid for 15 minutes for that repository only.
3. It sends the events to `POST /v1/egress/ci/events` in batches of up to 1000, with up to three attempts per batch. A batch carries a `batch_id`, so a retry never counts twice.

A failure never fails the job: the summary and the log get a warning such as `Events not sent to OpenWAF: token exchange: 403 repository_not_registered.`

| Error | Meaning and fix |
|---|---|
| `GitHub OIDC token: … (the job needs permissions: id-token: write)` | Add `id-token: write` to the workflow's or the job's `permissions`. Pull requests from forks never get it. |
| `401 invalid_token` | The OIDC token was refused (signature, issuer, audience, time or used twice); or, for events, the OpenWAF token expired. |
| `403 repository_not_registered` | Register the repository; again if it was renamed, transferred or created again. |
| `403 ref_not_allowed` | The run's ref is not in the registration's `refs`. |
| `403 feature_disabled` | Egress is not enabled for the organization. |
| `403 repository_mismatch` | The run's repository is not the token's. |
| `429 rate_limited`, `503 unavailable` | Try again later; the job is not affected. |

## See the events

CI events have their own place, apart from the refusals of your AI agents (the dashboard's **Blocked requests** view and the MCP tool `egress_events` show only those):

- **Dashboard:** AI egress › **CI** lists the last 7 days grouped by repository and run, with each destination's job, path, container, decision, reason, detections and traffic, and a link to the run on GitHub. Filter by repository, decision or free text.
- **MCP:** the tool `egress_ci_events` takes `repo`, `workflow`, `job`, `run`, `decision` (`allow`, `audit`, `block`) and `range` (`1h`, `24h`, `7d`, `30d`), and summarizes the events per repository and destination, with how many were blocked or would have been.
- **API:**

```sh
curl -sS "https://dash.openwaf.dev/v1/orgs/$ORG/egress/events?attack_type=egress_ci&ci_repo=acme/app&limit=100" \
  -H "Authorization: Bearer $OPENWAF_TOKEN"
```

`attack_type=egress_ci` selects CI events. `ci_repo`, `ci_workflow`, `ci_job` and `ci_run` select by repository, workflow, job and run id (exact, repeatable; `not_ci_…` leaves them out), and combine with `action`, `host`, `rule_name` and `q`. `…/egress/events/stream` takes the same filters and follows new events as Server-Sent Events. In each event:

| Field | Value |
|---|---|
| `action` | `allow`, `log` (audit: enforce would have refused it) or `block` |
| `rule_name` | The reason, such as `not_allowed` or `dlp` |
| `host` | The destination (and `ip` when the destination is an address) |
| `method` | The path: `proxy`, `direct`, `container`, `udp` or `dns` |
| `path` | Port, container and counts, such as `:443 container=db connections=3 bytes_up=812 bytes_down=6200` |
| `user_agent` | `ci:<owner>/<repo>/<workflow>/<job>#<run id>` |
| `msg` | The detection ids |
