GitHub Threat Detector persists everything it collects and detects in a single PostgreSQL database. Six tables form a clear pipeline: five are populated by collectors ingesting raw GitHub API data, and one —Documentation Index
Fetch the complete documentation index at: https://mintlify.com/morwn/github-threat-detector/llms.txt
Use this file to discover all available pages before exploring further.
findings — is written exclusively by analyzers when a detection rule fires. Of these six tables, five (workflow_runs, workflow_files, repo_contributors, push_commits, and findings) are created by db/schema.sql via apply_schema(). The sixth — github_events — must already exist in the database before schema.sql is applied; schema.sql references it only via a foreign key on findings.event_id and does not contain a CREATE TABLE github_events statement. Understanding the schema helps you write efficient analyzer queries, interpret evidence blobs, and build your own dashboards on top of the data.
Data Flow
The diagram below shows how data moves through the system, from the GitHub API to thefindings table.
Tables
github_events
The central fact table. Every raw event returned by the GitHub Events API is stored here, including synthetic ReleaseEvent rows produced by the releases collector. Analyzers query this table more than any other.
github_events is not created by db/schema.sql. It must already exist in the database before apply_schema() is called. schema.sql references this table only through the findings.event_id foreign key constraint (REFERENCES github_events(id)). Attempting to run schema.sql against a database where github_events does not yet exist will raise a foreign key violation error.GitHub’s own opaque event ID (e.g.
"32891044123"). The releases collector synthesises IDs with a release- prefix to avoid collisions.GitHub event class name:
PushEvent, PullRequestEvent, ReleaseEvent, CreateEvent, etc.The GitHub username of the user who triggered the event. Extracted from
actor.login in the raw payload.Numeric GitHub user ID. Useful for correlating actors who have renamed their accounts.
Repository in
owner/repo format, e.g. "acme-corp/payments-service".Organization login when the event was collected from an org-level endpoint.
NULL for repo-scoped events.The full event payload as a JSONB document. Analyzers use operators like
->>, ->, and ILIKE on this column (e.g. payload->>'action', payload::text ILIKE '%.github/workflows/%').GitHub’s timestamp for when the event occurred.
Identifies the collection run, e.g.
"acme-corp_payments-service_events". Useful for auditing what was collected when.Free-form annotation field. Not populated by any current collector; reserved for manual imports.
collectors/events.py (repo and org events), collectors/releases.py (synthetic release events).
Deduplication: ON CONFLICT (id) DO NOTHING — re-running collection for the same repo is idempotent.
workflow_runs
Stores GitHub Actions workflow run records collected from the Actions API. The event column is the trigger type (pull_request, push, pull_request_target, etc.), which is the primary signal used by CI-abuse analyzers.
The workflow run’s numeric ID cast to text.
Repository in
owner/repo format.Human-readable workflow name from the YAML
name: field.Relative path to the workflow file, e.g.
.github/workflows/ci.yml.Branch that triggered the run.
Commit SHA the run executed against.
The GitHub event that triggered this run. The
pull_request_target value is particularly significant for supply-chain attack detection.Current run status:
queued, in_progress, or completed.Terminal outcome:
success, failure, cancelled, action_required, etc. NULL when still running.The user who triggered the run (from
triggering_actor.login, falling back to actor.login).Timestamp of when the workflow run actually started executing, as reported by the GitHub API.
Timestamp of when the workflow run record was created in GitHub.
Timestamp of the last status update to this workflow run in GitHub. Refreshed on every upsert.
Full workflow run JSON from the GitHub API, including the
head_repository object used by fork-detection queries.Identifies the collection run that wrote this row, e.g.
"acme-corp_payments-service_actions".collectors/actions.py via collect_workflow_runs(full_name).
Indexes: repo_name, actor_login, event, created_at.
Deduplication: ON CONFLICT (id) DO UPDATE — re-running updates status, conclusion, updated_at, and payload for in-flight runs.
workflow_files
Holds the raw YAML content of every .github/workflows/*.yml and .github/workflows/*.yaml file in a repository’s default branch. This table is the data source for static-analysis and AI prompt-injection detection rules.
Repository in
owner/repo format.Workflow file path relative to the repo root, e.g.
.github/workflows/release.yml.The decoded UTF-8 workflow YAML text.
Git blob SHA at the time of fetch. Use this to detect content changes between collection runs.
Timestamp of the last successful fetch. Updated on every upsert.
(repo_name, path).
Populated by: collectors/workflow_files.py via collect_workflow_files(full_name).
Deduplication: ON CONFLICT (repo_name, path) DO UPDATE — refreshes content and SHA on re-collection.
repo_contributors
A contributor baseline used as an allowlist by heuristic analyzers. When an actor appears in this table it is treated as a known maintainer, reducing false positives on rules like workflow-run-from-fork.
Repository in
owner/repo format.GitHub username of the contributor.
Total commit-contribution count as reported by the Contributors API. Updated on re-collection.
Timestamp of the last successful fetch.
(repo_name, actor_login).
Populated by: collectors/contributors.py via collect_contributors(full_name).
Index: actor_login — analyzers join this table on actor_login frequently.
push_commits
Detailed commit metadata for every push to the default branch. While github_events records the existence of a push, this table stores the per-commit author/committer metadata fetched from the Commits API. It enables tamper-detection rules that compare author_date against committer_date or look for mismatches between author_login and committer_login.
Full 40-character commit SHA.
Repository in
owner/repo format.Foreign key back to the
github_events.id of the PushEvent that introduced this commit.GitHub username of the commit author (from
author.login).Git author timestamp embedded in the commit object.
GitHub username of the committer (may differ from author for squash/merge commits).
Email address from the git committer identity. Used to spot no-reply addresses that indicate automated commits.
Git committer timestamp. Backdated
author_date relative to committer_date can indicate history rewriting.First line of the commit message, truncated to 255 characters.
The full ref pushed, e.g.
refs/heads/main. Only commits on the default branch are collected.The SHA of the branch tip before this push. A zero-value SHA (
0000000...) indicates a force-push that rewrote history.Timestamp of when this commit record was fetched from the Commits API. Defaults to
now().(sha, repo_name).
Populated by: collectors/commits.py via collect_push_commits(full_name).
Deduplication: ON CONFLICT (sha, repo_name) DO NOTHING — each commit is fetched at most once.
findings
The output table. Every detection rule writes its results here via insert_finding() in db/queries.py. This is the only table written to by analyzers; all other tables are read-only from the analyzer perspective.
Auto-incrementing integer. Use this as a stable reference when acknowledging or suppressing a finding.
Machine-readable rule identifier matching
BaseAnalyzer.rule_id, e.g. "pr-target-abuse".One of
critical, high, medium, or low. Enforced by a CHECK constraint.The affected repository.
The GitHub username associated with the suspicious activity.
NULL for repo-level findings with no single actor.Foreign key to the source event that triggered this finding.
NULL for findings derived from workflow_runs or static analysis rather than github_events.Human-readable sentence describing the threat, e.g.
"Actor ghost pushed workflow file changes but has no prior commit history".Structured key-value data supporting the finding, such as
{"commit_count": 53} or {"head_sha": "abc123", "workflow_run_id": "9876543"}. Schema varies by rule.When the finding was written, defaulting to
now().analyzers/base.py → db/queries.py → insert_finding().
Indexes: rule_id, severity, repo_name, actor_login, created_at.
Deduplication
Runninganalyze multiple times against the same data must not create duplicate findings. The findings table enforces this with two partial unique indexes:
insert_finding() query uses ON CONFLICT DO NOTHING, so conflicting inserts are silently dropped rather than raising an error. You never need to pre-check for existing findings in an analyzer; simply call self.emit() for every match and let the database handle deduplication.