Techniques
Sample rules
GitHub Token Clone Push or ZIP from Unusual ASN
- source: elastic
- technicques:
- T1078
- T1213
- T1550
- T1567
Description
Detects git clone, git push, or ZIP download authenticated with a classic PAT, fine-grained PAT, or OAuth access token from an autonomous system (ASN) not previously seen for that user in the last 7 days. The source IP is resolved to its ASN at query time, so address churn inside the same ISP or cloud provider does not produce new alerts. A token used from an unfamiliar ASN can indicate credential theft or use from an unauthorized location.
Detection logic
FROM logs-github.audit-*
| WHERE event.dataset == "github.audit"
AND event.action IN ("git.clone", "git.push", "repo.download_zip")
AND github.programmatic_access_type IN ("OAuth access token", "Fine-grained personal access token", "Personal access token")
AND user.name IS NOT NULL AND github.actor_ip IS NOT NULL
| IP_LOCATION asn = github.actor_ip WITH { "database_file": "GeoLite2-ASN.mmdb", "properties": ["asn", "organization_name"] }
| IP_LOCATION geo = github.actor_ip WITH { "properties": ["country_iso_code"] }
| WHERE asn.asn IS NOT NULL
| STATS Esql.event_count = COUNT(*),
Esql.first_seen = MIN(@timestamp),
Esql.last_seen = MAX(@timestamp),
Esql.source_ip_values = VALUES(github.actor_ip),
Esql.country_values = VALUES(geo.country_iso_code),
Esql.repo_values = VALUES(github.repo),
Esql.event_action_values = VALUES(event.action),
Esql.programmatic_access_type_values = VALUES(github.programmatic_access_type),
Esql.user_agent_values = VALUES(github.user_agent),
Esql_priv.token_id_values = VALUES(github.token_id),
Esql_priv.hashed_token_values = VALUES(github.hashed_token)
BY user.name, asn.asn, asn.organization_name
// The 7-day history is the rule look-back (from = now-10085m). A group is new when its first event
// falls inside the current interval; the 10m bound allows for the 5m interval plus scheduling drift.
// Novelty is keyed on user + ASN only, so a new IP inside an already-seen ASN does not re-alert.
| WHERE DATE_DIFF("minute", Esql.first_seen, NOW()) <= 10
| EVAL Esql.source_as_number = asn.asn,
Esql.source_as_organization_name = asn.organization_name
| KEEP user.name, Esql.*, Esql_priv.*