Analyzers are where detection happens. Each analyzer encodes a single heuristic or pattern, queries the PostgreSQL tables populated by collectors, and emits structured findings when suspicious activity is found. GitHub Threat Detector ships with 17 built-in rules spread across seven analyzer modules. Every rule — built-in or custom — extends the sameDocumentation 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.
BaseAnalyzer abstract class, so the interface is consistent and new detectors slot in with minimal boilerplate.
BaseAnalyzer Interface
All detection rules inherit from analyzers/base.py. The class is abstract: you must define rule_id, severity, and implement run(). The emit() method is provided by the base class and handles the database write for you.
Class attributes
A unique, machine-readable identifier for this detection rule, e.g.
"workflow-file-change" or "pr-target-abuse". This value is stored in findings.rule_id and used by the --rules CLI flag to select individual analyzers.Risk level of findings emitted by this analyzer. Must be one of
"critical", "high", "medium", or "low" — enforced by the CHECK constraint on findings.severity.emit() — writing a finding
Call self.emit() once for each suspicious match your run() method identifies. The method delegates to insert_finding() in db/queries.py, which executes an INSERT … ON CONFLICT DO NOTHING so duplicate findings are silently dropped.
The affected repository in
owner/repo format.A human-readable sentence describing the threat, shown in the
report output. Include the actor login and any relevant context so the finding is self-explanatory without needing to look up the evidence blob.GitHub username of the actor associated with the suspicious activity. Pass
None for repo-level findings with no identifiable actor.The
github_events.id of the event that triggered this finding. When set, the deduplication index idx_findings_dedup_event on (rule_id, event_id) prevents re-emitting the same finding across runs. Pass None for findings derived from workflow_runs or static file analysis.A free-form dictionary of supporting data serialised to JSONB. Common patterns include commit SHAs, PR numbers, workflow run IDs, and timestamp strings. There is no enforced schema — choose keys that make triage easier.
run() — the detection logic
run() receives an optional repo_name. When called with None, the analyzer should scan all repositories in the database. When called with a specific owner/repo string, it should restrict its query to that repository. Return the number of findings emitted. The CLI uses this count to print a summary line after each analyzer completes.
Built-in Analyzers
The seven built-in modules and their exportedANALYZERS lists are concatenated by _load_analyzers() in cli.py:
Example: analyzers/malicious_ci.py
The malicious CI module ships three rules that illustrate the full range of the detection model:
Writing a Custom Analyzer
To add a new detection rule, create a new Python module in theanalyzers/ directory, subclass BaseAnalyzer, and export an ANALYZERS list. The steps are always the same:
- Set
rule_id— a unique kebab-case string. - Set
severity— one ofcritical,high,medium,low. - Implement
run(self, repo_name)— query the database, callself.emit()for each match, return the count. - Export
ANALYZERS— a list of instantiated analyzer objects at module level. - Register the module — add an import to
_load_analyzers()incli.py.
Complete example: LargeCommitPushAnalyzer
Registering a Custom Analyzer
Once your module is written, add it to_load_analyzers() in cli.py:
Analyzer Execution Model
Whenpython cli.py analyze is called, the CLI:
- Calls
_load_analyzers()to build the full list of analyzer instances. - Optionally filters the list by
--rules(comma-separatedrule_idvalues). - Determines the repo list from
--reposorDEFAULT_REPOS; falls back to[None](scan all repos). - Iterates over every
(analyzer, repo)pair and callsanalyzer.run(repo_name=repo). - Prints a per-rule summary and a grand total of findings emitted.
run() are caught and printed as warnings — one failing analyzer does not abort the rest of the pipeline.