High Number of Closed Pull Requests by User

Detects a high number of closed pull requests by a single user within a short time frame. Adversaries may close multiple pull requests to disrupt development workflows or hide malicious changes.

Elastic rule (View on GitHub)

  1[metadata]
  2creation_date = "2025/12/16"
  3integration = ["github"]
  4maturity = "production"
  5updated_date = "2026/01/12"
  6
  7[rule]
  8author = ["Elastic"]
  9description = """
 10Detects a high number of closed pull requests by a single user within a short time frame. Adversaries may
 11close multiple pull requests to disrupt development workflows or hide malicious changes.
 12"""
 13from = "now-9m"
 14interval = "8m"
 15language = "esql"
 16license = "Elastic License v2"
 17name = "High Number of Closed Pull Requests by User"
 18note = """ ## Triage and analysis
 19
 20> **Disclaimer**:
 21> This investigation guide was created using generative AI technology and has been reviewed to improve its accuracy and relevance. While every effort has been made to ensure its quality, we recommend validating the content and adapting it to suit your specific environment and operational needs.
 22
 23### Investigating High Number of Closed Pull Requests by User
 24
 25This rule flags a single user rapidly closing many pull requests in a short window, a disruptive pattern that suppresses review history, delays releases, and masks unauthorized changes. An attacker with stolen maintainer access mass-closes pull requests across multiple repositories, then force-pushes branches and opens new pull requests that sidestep earlier review threads, making malicious edits appear routine amid churn.
 26
 27### Possible investigation steps
 28
 29- Determine if the actor is a bot or sanctioned maintenance by confirming account type, scheduled workflows, and change advisories from repo/org owners.
 30- Open a sample of the closed PRs to review comments, labels, linked issues, and whether closure coincided with branch deletions, force-pushes, or unusual commit history in the target branches.
 31- Correlate the closure burst with audit events for permission changes, role assignments, repository settings edits, or protection rule modifications to detect potential sabotage.
 32- Validate the actor’s IPs, geolocation, and user agents against baselines and check for recent PAT creations, OAuth app grants, or SSO anomalies indicating credential theft.
 33- Identify whether closed PRs were immediately replaced by new PRs carrying similar diffs that bypass prior review threads and required checks, and verify branch protection remained enforced.
 34
 35### False positive analysis
 36
 37- A maintainer or org-owned bot performs scheduled backlog hygiene, closing stale, duplicate, or superseded PRs across multiple repositories after a default branch rename or policy update, resulting in a high closure count from one account.
 38- During a planned migration or archival, a release manager closes PRs tied to deprecated branches and consolidates work into new targets, legitimately generating a burst of closures attributed to a single user.
 39
 40### Response and remediation
 41
 42- Immediately contain by removing the user from teams with Triage/Write permissions on affected repositories, revoking their personal access tokens from Tokens & keys, and tightening branch protection by disallowing force-pushes and restricting who can push to main and release branches.
 43- Trigger escalation to Security Incident Response if closed pull requests span more than five repositories within one hour, coincide with branch deletions or forced pushes, or originate from a new user agent/IP, and disable the account at the identity provider while contacting GitHub Support.
 44- Eradicate impact by reopening legitimate PRs via each closed PR URL, using Restore branch or recreating the head branch from the last known commit SHA, and reapplying required labels and reviewers.
 45- Recover repository state by comparing diffs of closed PRs to any newly opened PRs by the same user, reverting unauthorized commits in target branches with git revert, and re-running required status checks before merging.
 46- Harden controls by enforcing branch protection rules (require two approvals, restrict who can dismiss reviews, require signed commits), enabling CODEOWNERS for critical paths, and turning off Allow deletions on default and release branches.
 47- Prevent recurrence by disabling classic PATs and requiring short-lived fine-grained PATs, revoking unusual OAuth app grants, mandating SSO with hardware-backed MFA, and installing a GitHub App/Action that notifies on PR closures with PR URLs, repos, and branches and requires a reason-coded label per policy.
 48"""
 49references = [
 50    "https://www.wiz.io/blog/shai-hulud-2-0-ongoing-supply-chain-attack",
 51    "https://trigger.dev/blog/shai-hulud-postmortem",
 52    "https://posthog.com/blog/nov-24-shai-hulud-attack-post-mortem",
 53]
 54risk_score = 47
 55rule_id = "098bd5cc-fd55-438f-b354-7d6cd9856a08"
 56severity = "medium"
 57tags = [
 58    "Domain: Cloud",
 59    "Use Case: Threat Detection",
 60    "Tactic: Impact",
 61    "Tactic: Exfiltration",
 62    "Data Source: Github",
 63    "Resources: Investigation Guide",
 64]
 65timestamp_override = "event.ingested"
 66type = "esql"
 67query = '''
 68from logs-github.audit-* metadata _id, _index, _version
 69| where
 70  data_stream.dataset == "github.audit" and
 71  github.category == "pull_request" and
 72  event.type == "change" and
 73  event.action == "pull_request.close"
 74| stats
 75  Esql.document_count = COUNT(*),
 76  Esql.github_org_values = values(github.org),
 77  Esql.github_repo_values = values(github.repo),
 78  Esql.github_user_agent_values = values(github.user_agent),
 79  Esql.github_pull_request_url_values = values(github.pull_request_url),
 80  Esql.user_name_values = values(user.name),
 81  Esql.agent_id_values = values(agent.id),
 82  Esql.event_dataset_values = values(event.dataset),
 83  Esql.data_stream_namespace_values = values(data_stream.namespace)
 84
 85  by user.name
 86
 87| keep Esql.*
 88
 89| where
 90  Esql.document_count >= 10
 91'''
 92
 93[[rule.threat]]
 94framework = "MITRE ATT&CK"
 95
 96[[rule.threat.technique]]
 97id = "T1485"
 98name = "Data Destruction"
 99reference = "https://attack.mitre.org/techniques/T1485/"
100
101[rule.threat.tactic]
102id = "TA0040"
103name = "Impact"
104reference = "https://attack.mitre.org/tactics/TA0040/"
105
106[[rule.threat]]
107framework = "MITRE ATT&CK"
108
109[[rule.threat.technique]]
110id = "T1020"
111name = "Automated Exfiltration"
112reference = "https://attack.mitre.org/techniques/T1020/"
113
114[[rule.threat.technique]]
115id = "T1567"
116name = "Exfiltration Over Web Service"
117reference = "https://attack.mitre.org/techniques/T1567/"
118
119[[rule.threat.technique.subtechnique]]
120id = "T1567.001"
121name = "Exfiltration to Code Repository"
122reference = "https://attack.mitre.org/techniques/T1567/001/"
123
124[rule.threat.tactic]
125id = "TA0010"
126name = "Exfiltration"
127reference = "https://attack.mitre.org/tactics/TA0010/"

Triage and analysis

Disclaimer: This investigation guide was created using generative AI technology and has been reviewed to improve its accuracy and relevance. While every effort has been made to ensure its quality, we recommend validating the content and adapting it to suit your specific environment and operational needs.

Investigating High Number of Closed Pull Requests by User

This rule flags a single user rapidly closing many pull requests in a short window, a disruptive pattern that suppresses review history, delays releases, and masks unauthorized changes. An attacker with stolen maintainer access mass-closes pull requests across multiple repositories, then force-pushes branches and opens new pull requests that sidestep earlier review threads, making malicious edits appear routine amid churn.

Possible investigation steps

  • Determine if the actor is a bot or sanctioned maintenance by confirming account type, scheduled workflows, and change advisories from repo/org owners.
  • Open a sample of the closed PRs to review comments, labels, linked issues, and whether closure coincided with branch deletions, force-pushes, or unusual commit history in the target branches.
  • Correlate the closure burst with audit events for permission changes, role assignments, repository settings edits, or protection rule modifications to detect potential sabotage.
  • Validate the actor’s IPs, geolocation, and user agents against baselines and check for recent PAT creations, OAuth app grants, or SSO anomalies indicating credential theft.
  • Identify whether closed PRs were immediately replaced by new PRs carrying similar diffs that bypass prior review threads and required checks, and verify branch protection remained enforced.

False positive analysis

  • A maintainer or org-owned bot performs scheduled backlog hygiene, closing stale, duplicate, or superseded PRs across multiple repositories after a default branch rename or policy update, resulting in a high closure count from one account.
  • During a planned migration or archival, a release manager closes PRs tied to deprecated branches and consolidates work into new targets, legitimately generating a burst of closures attributed to a single user.

Response and remediation

  • Immediately contain by removing the user from teams with Triage/Write permissions on affected repositories, revoking their personal access tokens from Tokens & keys, and tightening branch protection by disallowing force-pushes and restricting who can push to main and release branches.
  • Trigger escalation to Security Incident Response if closed pull requests span more than five repositories within one hour, coincide with branch deletions or forced pushes, or originate from a new user agent/IP, and disable the account at the identity provider while contacting GitHub Support.
  • Eradicate impact by reopening legitimate PRs via each closed PR URL, using Restore branch or recreating the head branch from the last known commit SHA, and reapplying required labels and reviewers.
  • Recover repository state by comparing diffs of closed PRs to any newly opened PRs by the same user, reverting unauthorized commits in target branches with git revert, and re-running required status checks before merging.
  • Harden controls by enforcing branch protection rules (require two approvals, restrict who can dismiss reviews, require signed commits), enabling CODEOWNERS for critical paths, and turning off Allow deletions on default and release branches.
  • Prevent recurrence by disabling classic PATs and requiring short-lived fine-grained PATs, revoking unusual OAuth app grants, mandating SSO with hardware-backed MFA, and installing a GitHub App/Action that notifies on PR closures with PR URLs, repos, and branches and requires a reason-coded label per policy.

References

Related rules

to-top