Back to Intelligence

Version Control DFIR: GitHub, GitLab, Bitbucket, and Azure DevOps Detection & Incident Readiness Guide

SA
Security Arsenal Team
August 28, 2026
12 min read

Your source code is no longer just an intellectual property asset — it is an attack surface, a persistence mechanism, and increasingly, the initial access vector. The recent wave of supply-chain intrusions, from the tj-actions GitHub Actions compromise in March 2025 to the Salesloft Drift OAuth token theft campaign and the Shai-Hulud npm worm that self-propagated through maintainer credentials later in 2025, all share a common denominator: attackers treated version control systems (VCS) as both the target and the delivery mechanism. Wiz's recent DFIR cheatsheet for GitHub, GitLab, Bitbucket, and Azure DevOps lands at exactly the right time, because most organizations still have far better telemetry from their endpoints than from the platforms holding their crown jewels.

This post translates that guidance into an operational defensive program: what logs each platform actually gives you, what attacker behavior looks like in those logs, hunt queries you can run today, and how to get your VCS estate incident-ready before you need it.

Why VCS Is the DFIR Blind Spot

In every IR engagement I've led involving developer platforms, the same pattern emerges: the SOC has excellent EDR coverage, decent identity telemetry, and almost nothing from the VCS layer. Attackers know this. A compromised personal access token (PAT) or OAuth grant gives an adversary quiet, API-driven access that looks functionally identical to legitimate CI/CD traffic. Repository exfiltration over git protocol or REST API leaves no endpoint artifact if the attacker works entirely through the API from their own infrastructure.

The threat model spans four attacker objectives:

  1. Source code theft — mass cloning or API-based archive downloads of private repositories.
  2. Credential harvesting — secrets, cloud keys, and tokens embedded in code or CI/CD environment variables (the tj-actions compromise dumped CI/CD secrets into workflow logs).
  3. Supply-chain poisoning — malicious commits to dependencies, tampered workflow files, or backdoored releases (the SolarWinds playbook, repeated at npm scale).
  4. Persistence — rogue OAuth applications, deploy keys, SSH keys, webhooks, and self-hosted runners that survive password resets and even IdP-level remediation.

Technical Analysis: Log Visibility by Platform

Detection fidelity in VCS DFIR is entirely a function of licensing tier and configuration. This is the uncomfortable truth: on several platforms, the audit events you need most are paywalled or off by default.

GitHub

  • Audit log (Organization and Enterprise tiers): records authentication events, repository creation/destruction, permission changes, PAT and OAuth application lifecycle, SSO changes, and audit log configuration tampering. Critical: git operations (clone, push, fetch) are only visible when audit log streaming is enabled on Enterprise Cloud — they are not retained in the standard audit log UI.
  • Audit log streaming to S3, Azure Blob/Event Hubs, Splunk, or Datadog is the single highest-value control. Stream it to your SIEM.
  • Key event actions: personal_access_token.create, oauth_application.create, repo.destroy, repo.create, protected_branch.destroy, ssh_key.create, deploy_key.create, hook.create, organization.add_member, org_credential_authorization.deauthorize.

GitLab

  • Audit events (Premium/Ultimate): group and project-level audit events cover membership changes, protected branch modifications, deploy token creation, and failed logins. Instance-level audit events require self-managed Ultimate.
  • Self-managed instances: the real forensics live in /var/log/gitlab/gitlab-rails/audit_json.log, nginx/gitlab_access.log (look for git-over-HTTP operations and API calls), and gitaly/ logs for repository-level RPC access.
  • Key signals: mass GET /api/v4/projects/:id/repository/archive requests, personal_access_tokens API calls, runner registration events.

Bitbucket (Cloud)

  • Audit logging is comparatively thin. Organization-level audit logs capture membership and permission changes; workspace-level events cover repository access changes. For git operation visibility, you largely depend on network-layer telemetry (proxy/DNS/NetFlow) and Atlassian Access logs.

Azure DevOps

  • Audit log (organization settings → Auditing, must be explicitly enabled): captures PAT creation, permission changes, pipeline modifications, and repository policy changes.
  • The high-fidelity sources are Entra ID sign-in logs (for interactive access), the Azure DevOps audit stream, and pipeline run logs. Watch Git.Clone API patterns and service connection modifications — pipeline tampering is the primary Azure DevOps attack path.

The Attack Chain You'll Actually See

A representative 2025–2026 intrusion looks like this: developer phished or token leaked in a public repo → attacker authenticates via REST API from a cloud VPS or residential proxy → enumerates organizations and repositories → creates a PAT or OAuth app for persistence → mass-clones or downloads repo archives → harvests secrets → either exits or escalates to supply-chain poisoning via a tampered workflow file or dependency commit. Every stage of that chain is detectable if you have the right logs centralized.

Detection & Response

The detections below target the highest-signal behaviors: persistence creation, audit log tampering, and bulk exfiltration. Tune thresholds (clone counts, distinct repos per actor per hour) to your environment's baseline — a developer laptop and a CI runner behave very differently.

Sigma Rules

YAML
---
title: Version Control Persistence Mechanism Created
description: Detects creation of persistence-capable objects in GitHub or GitLab audit logs — personal access tokens, OAuth applications, SSH/deploy keys, and webhooks. These objects survive password resets and IdP-level account remediation and are a standard attacker persistence technique following developer credential compromise.
references:
  - https://www.wiz.io/blog/vcs-dfir-threat-hunting-github-gitlab-azure-devops
  - https://attack.mitre.org/techniques/T1098/001/
author: Security Arsenal
date: 2026/01/09
id: 3f8a1c42-7b2d-4e91-a6c5-9d1e2f3a4b5c
status: experimental
logsource:
  product: github
  service: audit
detection:
  selection_github:
    action:
      - 'personal_access_token.create'
      - 'oauth_application.create'
      - 'oauth_authorization.create'
      - 'ssh_key.create'
      - 'deploy_key.create'
      - 'hook.create'
  selection_gitlab:
    event_name:
      - 'personal_access_token_created'
      - 'deploy_key_added'
      - 'gpg_key_added'
  condition: 1 of selection_*
falsepositives:
  - Legitimate developer token and key provisioning; alert on off-hours creation, creation from unusual geographies, or creation immediately following a new session from an unrecognized IP
level: medium
---
title: Version Control Audit Logging Disabled or Stream Removed
description: Detects disabling or destruction of VCS audit logging and log streaming configurations — a hallmark defense-evasion behavior observed in supply-chain intrusions before destructive or exfiltration actions. On GitHub this includes audit log streaming updates; on GitLab, audit event feature changes; on Azure DevOps, auditing being turned off at the organization level.
references:
  - https://www.wiz.io/blog/vcs-dfir-threat-hunting-github-gitlab-azure-devops
  - https://attack.mitre.org/techniques/T1562/008/
author: Security Arsenal
date: 2026/01/09
id: 8c4d2e17-9a3b-4f56-b8d2-1e7f6a5c3d09
status: experimental
logsource:
  product: github
  service: audit
detection:
  selection_github:
    action|contains:
      - 'audit_log_streaming'
      - 'business.audit_log'
  selection_gitlab:
    event_name:
      - 'audit_event_streaming_destination_deleted'
      - 'audit_events_streaming_http_destination_destroyed'
  selection_azure:
    EventType:
      - 'AuditingDisabled'
      - 'AuditStreamingDisabled'
  condition: 1 of selection_*
falsepositives:
  - Legitimate SIEM migration or streaming destination reconfiguration; correlate with change tickets. Any occurrence outside a planned change window should be treated as high severity
level: high
---
title: Repository Destruction or Protected Branch Tampering
description: Detects destructive and integrity-reducing actions on source repositories — repository deletion, protected branch removal, and force-push-enabling changes. These actions precede or accompany supply-chain poisoning and ransomware-style extortion against code assets.
references:
  - https://www.wiz.io/blog/vcs-dfir-threat-hunting-github-gitlab-azure-devops
  - https://attack.mitre.org/techniques/T1485/
author: Security Arsenal
date: 2026/01/09
id: 5e2b9d04-1f7a-4c38-9e6b-2a4d8c1f7b36
status: experimental
logsource:
  product: github
  service: audit
detection:
  selection:
    action:
      - 'repo.destroy'
      - 'protected_branch.destroy'
      - 'protected_branch.update_admin_enforced'
      - 'repo.transfer'
      - 'project.delete'
falsepositives:
  - Repository lifecycle housekeeping and legitimate transfers; volume and actor context matter — a single deletion by a repo admin is routine, multiple deletions across repositories by one actor in a short window is not
level: high

KQL — Microsoft Sentinel

These queries assume GitHub audit log streaming into Sentinel via the GitHub connector or a custom log pipeline (GitHubAuditLog_CL), and endpoint telemetry from Defender for Endpoint. Adapt field names to your ingestion schema.

KQL — Microsoft Sentinel / Defender
// Hunt 1: Persistence objects created from IPs with no prior 30-day history for that actor
let KnownActorIPs = GitHubAuditLog_CL
    | where TimeGenerated > ago(30d) and TimeGenerated < ago(1d)
    | summarize by actor_s, actor_ip_s;
GitHubAuditLog_CL
| where TimeGenerated > ago(1d)
| where action_s in ("personal_access_token.create", "oauth_application.create", "ssh_key.create", "deploy_key.create", "hook.create")
| join kind=leftanti (KnownActorIPs) on actor_s, actor_ip_s
| project TimeGenerated, actor_s, actor_ip_s, action_s, repo_s, user_agent_s
| sort by TimeGenerated desc;

// Hunt 2: Mass repository access — single actor touching an abnormal number of distinct repos
GitHubAuditLog_CL
| where TimeGenerated > ago(1h)
| where action_s has_any ("repo.clone", "repo.download", "git.clone") or action_s has "archive"
| summarize DistinctRepos = dcount(repo_s), RepoList = make_set(repo_s, 25) by actor_s, actor_ip_s
| where DistinctRepos > 20  // tune to your baseline; CI service accounts should be excluded by allowlist
| sort by DistinctRepos desc;

// Hunt 3: Audit log tampering followed by destructive action from the same actor
let AuditTamper = GitHubAuditLog_CL
    | where TimeGenerated > ago(24h)
    | where action_s has_any ("audit_log_streaming", "destroy")
    | project TamperTime = TimeGenerated, actor_s;
GitHubAuditLog_CL
| where TimeGenerated > ago(24h)
| where action_s in ("repo.destroy", "protected_branch.destroy", "repo.transfer")
| join kind=inner (AuditTamper) on actor_s
| where TimeGenerated > TamperTime
| project TimeGenerated, actor_s, action_s, repo_s, TamperTime;

// Hunt 4 (endpoint): git operations executed by non-developer parent processes
DeviceProcessEvents
| where TimeGenerated > ago(7d)
| where FileName in~ ("git.exe", "git-remote-https.exe", "ssh.exe")
| where InitiatingProcessFileName in~ ("winword.exe", "excel.exe", "powershell.exe", "wscript.exe", "cscript.exe", "rundll32.exe", "mshta.exe")
| project TimeGenerated, DeviceName, InitiatingProcessFileName, FileName, ProcessCommandLine, AccountName;

Velociraptor VQL

Endpoint forensics matters when the compromise touches developer workstations directly — token theft from credential helpers, malicious clones pulled by malware, or a developer machine used as the pivot. This artifact hunts git credential material and suspicious git execution in one sweep.

VQL — Velociraptor
-- Hunt for exposed git credential stores and anomalous git process execution
-- Combines file artifacts (.git-credentials, credential helpers) with live process data

SELECT * FROM foreach(
row={
    SELECT FullPath, Size, Mtime, Btime
    FROM glob(globs=[
        "C:/Users/*/.git-credentials",
        "C:/Users/*/.config/git/credentials",
        "/home/*/.git-credentials",
        "/root/.git-credentials"
    ])
    WHERE NOT IsDir
},
query={
    SELECT "CREDENTIAL_FILE" AS ArtifactType,
           FullPath, Size, Mtime
    FROM scope()
})

-- Separately: running git processes with token-bearing or clone command lines
SELECT Pid, Name, CommandLine, Exe, Username, CreateTime
FROM pslist()
WHERE Name =~ '(?i)git|ssh'
  AND (
    CommandLine =~ '(?i)clone|archive|bundle'
    OR CommandLine =~ '(?i)oauth2:|ghp_|gho_|github_pat_|glpat-'
  )

The credential-file glob is intentionally broad for hunting, not alerting — .git-credentials files storing plaintext tokens are exactly what infostealers (and the tooling behind the 2025 token-theft campaigns) target. Any hit justifies a deeper look and mandatory token rotation.

Readiness and Hardening Script

This script audits a GitHub organization's incident-readiness posture: SSO enforcement, 2FA requirement, secret scanning, and pending OAuth app visibility. Run it with a token holding admin:org scope, and treat the output as a finding list.

Bash / Shell
#!/bin/bash
# GitHub Org DFIR Readiness Audit — requires gh CLI authenticated with admin:org scope
# Usage: ./github-readiness-audit.sh <org-name>

ORG="$1"
if [ -z "$ORG" ]; then echo "Usage: $0 <org-name>"; exit 1; fi

echo "=== GitHub DFIR Readiness Audit: $ORG ==="

echo -e "\n[1] Organization security settings"
gh api "orgs/$ORG" --jq '{
  two_factor_required: .two_factor_requirement_enabled,
  default_repo_permission: .default_repository_permission,
  members_can_create_repos: .members_can_create_repositories,
  members_can_fork_private: .members_can_fork_private_repositories
}'

echo -e "\n[2] Secret scanning & push protection status (per repo)"
gh api "orgs/$ORG/repos" --paginate --jq '.[] | {
  repo: .name,
  secret_scanning: .security_and_analysis.secret_scanning.status,
  push_protection: .security_and_analysis.secret_scanning_push_protection.status
}' 2>/dev/null | head -50

echo -e "\n[3] Outside collaborators (review for stale access)"
gh api "orgs/$ORG/outside_collaborators" --jq '.[] | .login'

echo -e "\n[4] Recently created PATs and OAuth grants (last 100 audit events)"
gh api "orgs/$ORG/audit-log" --paginate \
  -q '.[] | select(.action | test("personal_access_token|oauth")) |
      {time: ."@timestamp", actor: .actor, action: .action, repo: .repo}' | head -40

echo -e "\n[5] Webhooks across org repos (rogue webhook = exfil channel)"
for REPO in $(gh api "orgs/$ORG/repos" --paginate --jq '.[].name'); do
  gh api "repos/$ORG/$REPO/hooks" --jq ".[] | {repo: \"$REPO\", url: .config.url, active: .active}" 2>/dev/null
done

echo -e "\n[6] Self-hosted runners (review for unauthorized registrations)"
gh api "orgs/$ORG/actions/runners" --jq '.runners[] | {name: .name, os: .os, status: .status, labels: [.labels[].name]}'

echo -e "\nREMINDER: Verify audit log STREAMING is enabled — the API/UI audit log alone does not retain git clone/fetch events."

Remediation and Incident Readiness Checklist

There is no CVE to patch here — the remediation is architectural. Prioritize in this order:

Immediate (this week):

  1. Enable and stream audit logs everywhere. GitHub: enable audit log streaming to your SIEM (Enterprise Cloud). Azure DevOps: turn on organization auditing and configure the audit stream. GitLab self-managed: ship audit_json.log and nginx access logs to the SIEM. Without this, every other control on this list is blind.
  2. Enforce SSO and 2FA at the organization level, and disable PATs where feasible in favor of fine-grained tokens with expiration. For the tokens that must exist, mandate 90-day maximum lifetimes.
  3. Inventory and revoke: OAuth applications, deploy keys, SSH keys, webhooks, and self-hosted runners. Anything you can't attribute to an owner gets revoked. In the Drift/Salesloft campaign, stale OAuth grants were the entire intrusion mechanism.

Short-term (this quarter):

  1. Baseline git-operation telemetry per actor and per service account so mass-clone detection thresholds are meaningful. CI service accounts get allowlisted by IP and identity.
  2. Protect the pipeline layer: branch protection on default branches, required reviews for workflow file changes (.github/workflows/, .gitlab-ci.yml, azure-pipelines.yml), and OIDC-based cloud federation instead of long-lived secrets in CI variables. The tj-actions incident demonstrated that a single poisoned workflow file exposes every secret in the pipeline.
  3. Turn on secret scanning and push protection on every repository; a leaked credential blocked at push time never becomes an IR case.

Incident response prep:

  1. Write the VCS IR runbook now: who can mass-revoke tokens at the org level, how to force-expire all sessions, how to enumerate every persistence object an attacker could have created during dwell time, and how to validate repository integrity (signed commits, tag verification against known-good hashes).
  2. Rehearse the "developer credential compromised" scenario end-to-end at least once. The discovery question that matters: can you enumerate every action a given token took, across every repository, in under an hour? If the answer is no, fix the logging before an adversary tests it for you.

Final Word

The Wiz cheatsheet's real message is one I've seen proven across a decade and a half of IR work: the platforms your engineers live in every day are the platforms your adversaries are studying hardest. Endpoint telemetry will catch the phish — but only VCS telemetry will catch what happens after the token is stolen. Centralize the logs, baseline the behavior, and build the detections above into your SOC before the next supply-chain headline has your organization's name in it.

Related Resources

Security Arsenal Penetration Testing Services AlertMonitor Platform Book a SOC Assessment vulnerability-management Intel Hub

Is your security operations ready?

Get a free SOC assessment or see how AlertMonitor cuts through alert noise with automated triage.