GitHub audit log¶
GitHub keeps an audit log for every enterprise and organization: repositories created, archived, transferred and deleted, members invited and removed, branch protections and rulesets changed, secret scanning, Dependabot, push protection and the two-factor requirement turned off, secrets added, self-hosted runners registered, and audit log streams changed or paused. Signalith makes each event searchable and runs the SigmaHQ and Splunk GitHub rules on it.
It reads the audit log of GitHub Enterprise Cloud (an enterprise, or an organization on the Enterprise plan) and GitHub Enterprise Server 3.x, the same events GitHub shows under Settings > Audit log.
How it works¶
Signalith doesn't sign in to GitHub. GitHub's events reach it one of three ways; pick the first that fits:
| Way | What you need | How soon events arrive |
|---|---|---|
| Audit log streaming to an Azure event hub | GitHub Enterprise Cloud or Server, an Azure Event Hubs namespace, the OpenTelemetry Collector | Within a minute |
| A script that reads the REST API | An owner's token, a machine that runs a job every few minutes | As often as the script runs |
| Files that streaming writes to Amazon S3, Azure Blob Storage or Google Cloud Storage | A storage bucket GitHub already streams to | As often as you send them |
GitHub can also stream to Splunk's HTTP Event Collector. Signalith has no HTTP Event Collector endpoint, so use one of the ways above instead.
Every way sends GitHub's own JSON for each event, unchanged. Under the github tag,
Signalith reads any event with an action such as repo.destroy, a time, and who or
where. Under another tag (except one that names another source, such as linux. or
windows.) it recognizes an event by GitHub's _document_id, or an actor_id with an
organization, enterprise or repository, so other applications' audit logs are never read
as GitHub's. It stores an event sent twice once, so a script that reads a little of the
last round again doesn't make copies.
What you need¶
- For streaming: an enterprise owner (or, on GitHub Enterprise Server, a site admin).
- For the REST API: an organization owner or enterprise owner, and a personal
access token (classic) with the
read:audit_logscope. The organization API needs the Enterprise plan. - An agent token to send events to Signalith, made on
Add data under Agent tokens, or with
signalith tokens create.
Stream to an Azure event hub¶
This is the same path as Entra ID and Azure: GitHub writes each event to an event hub, and the OpenTelemetry Collector reads it and sends it to Signalith. If you already send Entra ID's logs that way, GitHub can use the same event hub.
- Make an event hub. In the Azure portal, create an Event Hubs namespace (Standard
tier) and, in it, an event hub named
signalith, or use the one you made for Entra ID. - Make a key GitHub can write with. In the event hub's Shared access policies,
add a policy named
githubwith Send only, and copy its Connection string-primary key. - Make a key the Collector can read with. Add another policy with Listen only, and copy its connection string too. (Skip this if the Collector already reads this event hub for Entra ID.)
- Turn on streaming in GitHub. As an enterprise owner, open your enterprise's
Settings, then Audit log, then Log streaming. Choose Configure stream,
then Azure Event Hubs. Enter the event hub's name (
signalith) as Azure Event Hubs Instance, and thegithubpolicy's connection string. Choose Check endpoint, then Save. On GitHub Enterprise Server, the same page is under the enterprise's Settings > Audit log > Log streaming in the site admin's account. - Run the Collector. On the Add data page, choose Entra ID and Azure and
follow its steps with the Listen connection string: it installs the Collector and
writes its configuration. The receiver passes each message on whole (
format: raw), which is what Signalith reads.
Events usually arrive within a minute. GitHub keeps trying for a while when the event hub can't be reached, and the event hub keeps messages for its retention time, so a Collector that was off catches up.
Read the REST API with a script¶
GitHub's REST API serves the audit log a page at a time: GET /orgs/{org}/audit-log for
an organization and GET /enterprises/{enterprise}/audit-log for an enterprise (on GitHub
Enterprise Server, under https://<your server>/api/v3). This script, run every five
minutes by cron or a scheduled task, reads the last 15 minutes of an organization's audit
log with the GitHub CLI and sends it to Signalith:
#!/bin/sh
# Read the last 15 minutes of an organization's audit log and send it to Signalith.
# GH_TOKEN: a token (classic) with read:audit_log. SIGNALITH_TOKEN: an agent token.
set -eu
ORG=example-org
SIGNALITH=https://signalith.example.com:7779
SINCE=$(date -u -d '15 minutes ago' +%Y-%m-%dT%H:%M:%SZ) # macOS: date -u -v-15M ...
gh api --paginate "/orgs/$ORG/audit-log?include=all&per_page=100&phrase=created:>=$SINCE" \
--jq '.[] | tojson' > /tmp/github-audit.ndjson
if [ -s /tmp/github-audit.ndjson ]; then
curl --fail --silent --show-error \
-H "Authorization: Bearer $SIGNALITH_TOKEN" -H "X-Signalith-Tag: github" \
--data-binary @/tmp/github-audit.ndjson "$SIGNALITH/api/ingest"
fi
For an enterprise, use /enterprises/<enterprise>/audit-log instead. With GitHub
Enterprise Server, set GH_HOST to your server's name. include=all adds git events
(clones and pushes) on GitHub Enterprise Cloud; leave it out to send only the web and API
events. The script reads 15 minutes each time it runs every 5, so a late run misses
nothing, and the events it reads again are stored once.
Send events to the agent port (7779), with the agent
token. One request can hold up to 200,000 events and 16 MB; a script that reads more at a
time should send them in parts (split -l 50000). Signalith takes one JSON event per
line, as here, or a JSON array of events as each page comes from the API.
Send files from a storage bucket¶
When GitHub already streams to Amazon S3, Azure Blob Storage or Google Cloud Storage, it writes the events in files of one JSON event per line, compressed with gzip. Send a file the same way as the script does:
gunzip -c 2026-10-11T09-12-31Z.json.gz | curl --fail --silent --show-error \
-H "Authorization: Bearer $SIGNALITH_TOKEN" -H "X-Signalith-Tag: github" \
--data-binary @- https://signalith.example.com:7779/api/ingest
A job that downloads new files (aws s3 sync, azcopy sync or gcloud storage rsync)
and sends each one keeps Signalith up to date. An export from the audit log page
(Export > JSON) is a JSON array of events, and can be sent as it is.
The OpenTelemetry Collector works too: any log record whose body is GitHub's JSON for one event, events one per line, or an array of them, is read as GitHub's.
What you get¶
Each event becomes one event with sourcetype=github:cloud:audit, the sourcetype of
Splunk's add-on for GitHub. Its fields keep GitHub's own names, with nested objects
flattened into dotted names and each value in a list by Splunk's name for it
(events{}). A few of GitHub's names are Signalith's own fields with another meaning,
so those are kept under another name:
| Field | Example |
|---|---|
vendor_action |
GitHub's action: repo.destroy, org.disable_two_factor_requirement |
github_actor, actor_id, actor_ip, actor_is_bot |
GitHub's actor: who did it, from where |
actor_location.country_code |
US |
business, org, repo, team |
the enterprise, organization, repository and team |
github_user, github_user_id |
GitHub's user: the account the action was about, such as the member added |
visibility, operation_type |
private, create, modify, remove |
github_document_id, created_at |
GitHub's _document_id, and its time in milliseconds |
github_name, github_reason, github_user_agent |
GitHub's name, reason and user_agent |
Any other name GitHub gives an action's own fields (ruleset_name, runner_name,
hashed_token, config.url) is kept as GitHub writes it, unless Signalith uses it for
something else, when it is kept as github_<name>. Every value is kept as text. A
webhook address's user name, password and query values (https://user:pass@ci.example.com/hook?token=...)
are replaced with [redacted] before the event is stored. Each
event also gets the data model fields of what it is:
- A change (almost every action) is a Change event, with
action(created, modified or deleted, by the action's words:repo.create,org.add_member,repo.destroy,org.remove_outside_collaborator,repo.archived),change_type(the action's category, such asrepoororg), andobjectandobject_category(the repository, team, user, organization or enterprise it changed). - A sign-in to GitHub Enterprise Server (
user.login,user.failed_login, two-factor challenges) is an Authentication event, withactionsuccess or failure. - Git operations, API requests and workflow runs (
git.*,api.*,workflows.*) are Other events.
All of them have user and src_user (the actor), src (the actor's address),
signature (the action), dest (the organization, or the enterprise), vendor_account
(the enterprise, or the organization), app (GitHub), status and id (the event's
_document_id). Some searches to start with:
sourcetype=github:cloud:audit vendor_action=repo.destroy | stats count by user, repo
datamodel=Change sourcetype=github:cloud:audit action=deleted | stats count by signature, user
sourcetype=github:cloud:audit vendor_action=*disable* | stats count by vendor_action, org
Detections¶
The SigmaHQ pack has 12 GitHub rules (see Detections): delete
actions (codespaces, environments, projects, repositories), high-risk settings turned off
(advanced security, OAuth app restrictions, the two-factor requirement), Dependabot and
vulnerability alerts turned off, forking of private repositories allowed or cleared, new
secrets, outside collaborators, push protection bypassed or turned off, repositories and
organizations transferred, secret scanning turned off, self-hosted runner changes and SSH
certificate settings changed. Three SigmaHQ GitHub rules are left out:
github_new_org_member is informational, and github_pages_site_changed_to_public and
github_repository_archive_status_changed are experimental.
The Splunk Security Content pack has 18 GitHub rules, from its
github_enterprise searches (by GitHub's own names, action=repo.destroy) and its
github_organizations searches (Splunk's add-on's, vendor_action=repo.destroy): branch
rulesets and classic branch protection deleted, the two-factor requirement turned off,
Dependabot turned off, audit log streams deleted, changed or paused, the IP allow list
turned off, self-hosted runners registered, organizations removed from the enterprise,
and repositories archived and deleted: every production rule of both macros. Splunk's
other rules with GitHub in their name are hunting searches of endpoint and MCP server
data. Turn them on from each pack's
page like any other rules.
When nothing arrives¶
- Search for the events.
sourcetype=github:cloud:auditover the last day. If nothing is there, search for an action you expect as text ("repo.create"): events that arrived but weren't recognized, such as a file that wraps each event in another object, are stored as plain events. - Streaming. On GitHub's Log streaming page, a stream that can't deliver shows
an error; Check endpoint tests the connection string again. Check the Collector's
log for
azure_event_hub. - The script.
gh apinames what's wrong with the token: a token withoutread:audit_log, an account that isn't an owner, or an organization not on the Enterprise plan.curl --failprints Signalith's answer when it refuses a request, such as an agent token it doesn't know.