Skip to main content

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.

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 — 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 the findings table.
┌──────────────────────────────────────────────────────────────────┐
│                        GitHub API                                │
│  /repos/:owner/:repo/events   /actions/runs   /releases          │
│  /contributors                /git/trees      /commits/:sha      │
└───────────────┬──────────────────────────────────────────────────┘
                │  HTTP (httpx, paginate / get)

┌──────────────────────────────────────────────────────────────────┐
│                        Collectors                                │
│  events.py   actions.py   releases.py   contributors.py          │
│  workflow_files.py         commits.py                            │
└──────┬────────────┬──────────────┬──────────────────────────────┘
       │            │              │
       ▼            ▼              ▼
┌──────────────── PostgreSQL ──────────────────────────────────────┐
│  github_events   workflow_runs   workflow_files                   │
│  repo_contributors               push_commits                    │
└──────────────────────┬───────────────────────────────────────────┘
                       │  SELECT (heuristic SQL queries)

┌──────────────────────────────────────────────────────────────────┐
│                        Analyzers                                 │
│  malicious_ci   release_tamper   social_eng   fork_bomb          │
│  push_anomaly   ai_workflow_abuse   commit_tamper                │
└──────────────────────┬───────────────────────────────────────────┘
                       │  INSERT via insert_finding()

              ┌─────────────────┐
              │    findings     │
              └─────────────────┘

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.
id
TEXT PRIMARY KEY
GitHub’s own opaque event ID (e.g. "32891044123"). The releases collector synthesises IDs with a release- prefix to avoid collisions.
event_type
TEXT
GitHub event class name: PushEvent, PullRequestEvent, ReleaseEvent, CreateEvent, etc.
actor_login
TEXT
The GitHub username of the user who triggered the event. Extracted from actor.login in the raw payload.
actor_id
BIGINT
Numeric GitHub user ID. Useful for correlating actors who have renamed their accounts.
repo_name
TEXT
Repository in owner/repo format, e.g. "acme-corp/payments-service".
org_login
TEXT
Organization login when the event was collected from an org-level endpoint. NULL for repo-scoped events.
payload
JSONB
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/%').
created_at
TIMESTAMPTZ
GitHub’s timestamp for when the event occurred.
source
TEXT
Identifies the collection run, e.g. "acme-corp_payments-service_events". Useful for auditing what was collected when.
notes
TEXT
Free-form annotation field. Not populated by any current collector; reserved for manual imports.
Populated by: 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.
id
TEXT PRIMARY KEY
The workflow run’s numeric ID cast to text.
repo_name
TEXT
Repository in owner/repo format.
workflow_name
TEXT
Human-readable workflow name from the YAML name: field.
workflow_path
TEXT
Relative path to the workflow file, e.g. .github/workflows/ci.yml.
head_branch
TEXT
Branch that triggered the run.
head_sha
TEXT
Commit SHA the run executed against.
event
TEXT
The GitHub event that triggered this run. The pull_request_target value is particularly significant for supply-chain attack detection.
status
TEXT
Current run status: queued, in_progress, or completed.
conclusion
TEXT
Terminal outcome: success, failure, cancelled, action_required, etc. NULL when still running.
actor_login
TEXT
The user who triggered the run (from triggering_actor.login, falling back to actor.login).
run_started_at
TIMESTAMPTZ
Timestamp of when the workflow run actually started executing, as reported by the GitHub API.
created_at
TIMESTAMPTZ
Timestamp of when the workflow run record was created in GitHub.
updated_at
TIMESTAMPTZ
Timestamp of the last status update to this workflow run in GitHub. Refreshed on every upsert.
payload
JSONB
Full workflow run JSON from the GitHub API, including the head_repository object used by fork-detection queries.
source
TEXT
Identifies the collection run that wrote this row, e.g. "acme-corp_payments-service_actions".
Populated by: 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.
repo_name
TEXT
Repository in owner/repo format.
path
TEXT
Workflow file path relative to the repo root, e.g. .github/workflows/release.yml.
content
TEXT
The decoded UTF-8 workflow YAML text.
sha
TEXT
Git blob SHA at the time of fetch. Use this to detect content changes between collection runs.
fetched_at
TIMESTAMPTZ
Timestamp of the last successful fetch. Updated on every upsert.
Primary key: (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.
repo_name
TEXT
Repository in owner/repo format.
actor_login
TEXT
GitHub username of the contributor.
contributions
INT
Total commit-contribution count as reported by the Contributors API. Updated on re-collection.
fetched_at
TIMESTAMPTZ
Timestamp of the last successful fetch.
Primary key: (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.
sha
TEXT
Full 40-character commit SHA.
repo_name
TEXT
Repository in owner/repo format.
push_event_id
TEXT
Foreign key back to the github_events.id of the PushEvent that introduced this commit.
author_login
TEXT
GitHub username of the commit author (from author.login).
author_date
TIMESTAMPTZ
Git author timestamp embedded in the commit object.
committer_login
TEXT
GitHub username of the committer (may differ from author for squash/merge commits).
committer_email
TEXT
Email address from the git committer identity. Used to spot no-reply addresses that indicate automated commits.
committer_date
TIMESTAMPTZ
Git committer timestamp. Backdated author_date relative to committer_date can indicate history rewriting.
message_headline
TEXT
First line of the commit message, truncated to 255 characters.
ref
TEXT
The full ref pushed, e.g. refs/heads/main. Only commits on the default branch are collected.
before_sha
TEXT
The SHA of the branch tip before this push. A zero-value SHA (0000000...) indicates a force-push that rewrote history.
fetched_at
TIMESTAMPTZ
Timestamp of when this commit record was fetched from the Commits API. Defaults to now().
Primary key: (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.
id
SERIAL PRIMARY KEY
Auto-incrementing integer. Use this as a stable reference when acknowledging or suppressing a finding.
rule_id
TEXT
Machine-readable rule identifier matching BaseAnalyzer.rule_id, e.g. "pr-target-abuse".
severity
TEXT
One of critical, high, medium, or low. Enforced by a CHECK constraint.
repo_name
TEXT
The affected repository.
actor_login
TEXT
The GitHub username associated with the suspicious activity. NULL for repo-level findings with no single actor.
event_id
TEXT REFERENCES github_events(id)
Foreign key to the source event that triggered this finding. NULL for findings derived from workflow_runs or static analysis rather than github_events.
description
TEXT
Human-readable sentence describing the threat, e.g. "Actor ghost pushed workflow file changes but has no prior commit history".
evidence
JSONB
Structured key-value data supporting the finding, such as {"commit_count": 53} or {"head_sha": "abc123", "workflow_run_id": "9876543"}. Schema varies by rule.
created_at
TIMESTAMPTZ
When the finding was written, defaulting to now().
Populated by: all analyzer modules via analyzers/base.py → db/queries.py → insert_finding(). Indexes: rule_id, severity, repo_name, actor_login, created_at.

Deduplication

Running analyze multiple times against the same data must not create duplicate findings. The findings table enforces this with two partial unique indexes:
-- Findings tied to a specific event: one finding per rule+event pair
CREATE UNIQUE INDEX IF NOT EXISTS idx_findings_dedup_event
    ON findings(rule_id, event_id)
    WHERE event_id IS NOT NULL;

-- Findings NOT tied to an event: one finding per rule+repo+actor combination
CREATE UNIQUE INDEX IF NOT EXISTS idx_findings_dedup_no_event
    ON findings(rule_id, repo_name, COALESCE(actor_login, ''))
    WHERE event_id IS NULL;
Both indexes are partial — they are mutually exclusive and together cover all rows. The 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.

Build docs developers (and LLMs) love