GitHub Advanced Security: Secret Scanning, CodeQL, and Dependabot at Scale
Problem
GitHub Advanced Security (GHAS) bundles three security capabilities into the GitHub platform: secret scanning, code scanning with CodeQL, and Dependabot. All three are available for private repositories on GitHub Enterprise and GitHub.com with GHAS licences; secret scanning and Dependabot are free for public repositories.
Most organisations that have GHAS licences run it at defaults. The defaults miss a substantial fraction of what GHAS can find:
- Secret scanning ships with ~230 partner patterns. Custom patterns for internal API key formats, internal tokens, and legacy secret formats are not configured. Push protection is disabled by default for enterprise-wide rollouts. Alerts are created but no one is paged.
- CodeQL scans are triggered only on PRs and pushes to the default branch. The default query suite excludes security-extended and experimental queries that find real vulnerabilities. Custom queries for organisation-specific patterns are never written.
- Dependabot creates PRs for direct dependency updates. Indirect (transitive) dependency vulnerabilities are reported in the Dependabot alerts tab but not auto-remediated. Auto-merge rules are rarely configured, leading to alert fatigue and stale PRs that are never reviewed.
The result: a GHAS licence that generates noise but doesn’t prevent incidents.
By 2026, GHAS integration with GitHub Actions, CodeQL analysis across hundreds of repositories, and organisation-wide security policies via advanced-security org settings have made configuration-at-scale the primary challenge.
Target systems: GitHub Enterprise Cloud or GitHub Enterprise Server 3.10+; GHAS licence; repositories in Go, Python, JavaScript, TypeScript, Java, C#, Ruby, Kotlin, Swift (CodeQL language support as of 2026).
Threat Model
- Adversary 1 — Secret in commit history: A developer commits an API key or credential to a repository. Without push protection or scanning, the secret is in git history indefinitely and may be cloned by anyone with repo access (or all of the internet for public repos).
- Adversary 2 — Vulnerable dependency exploitation: A third-party library has a known CVE. The team is unaware because Dependabot alerts are noise and unenforced. An attacker targeting a known CVE in the library succeeds.
- Adversary 3 — SQL injection / XSS / RCE in application code: A CodeQL query would have found the vulnerability, but the scan uses only the default suite and missed the pattern. The vulnerability reaches production.
- Adversary 4 — Supply chain via transitive dependency: A direct dependency pulls in a transitive dependency with a critical CVE. Dependabot alerts on the direct dependency but not on the transitive chain without specific configuration.
- Adversary 5 — Developer bypasses push protection: Push protection blocks a commit containing a secret, but the developer clicks “bypass” because it’s a “test” credential. The bypass is not reviewed.
- Access level: Adversary 1 needs read access to the repo (or public access). Adversaries 2 and 3 target the deployed application. Adversary 4 has transitive supply chain access. Adversary 5 is an insider bypass.
- Objective: Obtain credentials, exploit application vulnerabilities, introduce malicious dependencies.
- Blast radius: A committed AWS secret in a public repo leads to account compromise within minutes (bots scan GitHub continuously). A critical CVE in production with no remediation SLA = indefinite exposure window.
Configuration
Step 1: Enable GHAS Organisation-Wide
Enable GHAS for all repositories in an organisation via the API (not just manually per-repo):
# Enable GHAS for all repos in an org via GitHub API.
# This requires org admin or security manager role.
gh api \
--method PATCH \
-H "Accept: application/vnd.github+json" \
/orgs/{org}/settings/security \
-f advanced_security_enabled_for_new_repositories=true \
-f secret_scanning_enabled_for_new_repositories=true \
-f secret_scanning_push_protection_enabled_for_new_repositories=true \
-f dependabot_alerts_enabled_for_new_repositories=true \
-f dependabot_security_updates_enabled_for_new_repositories=true
# Enable on existing repositories (batch).
gh repo list YOUR_ORG --limit 200 --json nameWithOwner -q '.[].nameWithOwner' \
| xargs -I {} gh api \
--method PATCH \
/repos/{} \
-f security_and_analysis[advanced_security][status]=enabled \
-f security_and_analysis[secret_scanning][status]=enabled \
-f security_and_analysis[secret_scanning_push_protection][status]=enabled
Alternatively, use Terraform:
resource "github_repository" "app" {
name = "application"
security_and_analysis {
advanced_security {
status = "enabled"
}
secret_scanning {
status = "enabled"
}
secret_scanning_push_protection {
status = "enabled"
}
}
}
Step 2: Custom Secret Scanning Patterns
Add organisation-specific patterns for internal token formats that GitHub’s built-in patterns don’t recognise:
# .github/secret-scanning.yml (or via org-level security policy repo)
# Custom patterns: define using regex in PCRE2 format.
patterns:
- name: "Internal API Key"
pattern: "myorg_(?:live|test)_[a-zA-Z0-9]{32}"
additional_match:
- "myorg"
test_strings:
- "myorg_live_abc123def456ghi789jkl012mno345pqr"
negative_test_strings:
- "myorg_dev_short"
- name: "Legacy JWT Secret"
pattern: "jwt_secret\\s*=\\s*[\"'][A-Za-z0-9+/]{43,}={0,2}[\"']"
additional_match:
- "jwt_secret"
- name: "Database Connection String with Password"
pattern: "(?:postgres|mysql|mongodb)://[^:]+:([^@]{8,})@"
additional_match:
- "://"
- name: "Internal SSH Private Key Header"
pattern: "-----BEGIN (?:RSA|EC|OPENSSH) PRIVATE KEY-----"
Publish custom patterns via the org-level security configuration repository (if using GitHub Enterprise):
# Create the org-level security configuration repo.
gh repo create YOUR_ORG/.github --private --description "Org security configuration"
# Push the secret-scanning.yml there; it applies org-wide.
Step 3: Push Protection and Bypass Audit
Push protection blocks commits containing detected secrets before they enter git history. Configure bypass policies and ensure bypasses are audited:
# Enable push protection org-wide.
gh api \
--method PATCH \
/orgs/{org}/settings/secret_scanning \
-f push_protection_enabled=true
# Configure bypass actors (who can bypass push protection).
# Options: repository admins, security managers, or nobody.
gh api \
--method PATCH \
/orgs/{org}/settings/secret_scanning \
-f push_protection_bypass_role_ids[]="security-managers-team-id"
Alert on every bypass via a webhook:
# GitHub webhook configuration → security-alerts receiver.
# Event: secret_scanning_alert
# Receiver (example in Python/FastAPI):
@app.post("/webhook/ghas")
async def ghas_webhook(payload: dict, x_github_event: str = Header(None)):
if x_github_event == "secret_scanning_alert":
if payload["action"] == "created":
alert_security_team(
f"Secret detected: {payload['alert']['secret_type']} "
f"in {payload['repository']['full_name']}"
)
if payload["action"] == "bypass":
alert_security_team(
f"Push protection BYPASSED by {payload['alert']['resolved_by']['login']} "
f"in {payload['repository']['full_name']} — requires review"
)
Enforce: every bypass must be resolved (verified as a false positive, secret rotated, or reported as a risk acceptance) within your defined SLA.
Step 4: CodeQL — Extended Query Suite
The default CodeQL query suite catches common CWEs. The security-extended suite adds patterns with lower confidence that are nonetheless worth reviewing in code review:
# .github/workflows/codeql.yml
name: CodeQL
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
schedule:
- cron: "0 3 * * 1" # Weekly full scan on Monday 3am.
jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: ubuntu-latest
permissions:
security-events: write
contents: read
strategy:
matrix:
language: [python, javascript, go]
steps:
- uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# Use security-extended for more coverage; higher false-positive rate.
queries: security-extended
# Add custom queries from your org's query pack.
packs: your-org/codeql-security-queries
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{ matrix.language }}"
# Fail the workflow if any HIGH or CRITICAL alerts are introduced.
# (Requires GHAS + code scanning enforcement)
Custom CodeQL query for organisation-specific patterns:
// queries/insecure-random-in-token-gen.ql
/**
* @name Cryptographically insecure random in token generation
* @description Uses math.random() or similar for security tokens.
* @kind problem
* @problem.severity error
* @security-severity 8.0
* @precision high
* @id js/insecure-random-token
* @tags security
*/
import javascript
from CallExpr call
where
call.getCalleeName() = "random" and
call.getCalleeNode().toString().matches("Math.random") and
exists(Assignment assign |
assign.getRhs() = call and
assign.getLhs().toString().matches(["token", "secret", "key", "nonce", "salt", "csrf"])
)
select call, "Math.random() is not cryptographically secure; use crypto.randomBytes()."
Step 5: Code Scanning Enforcement in Branch Protection
Block PRs from merging if they introduce new CodeQL alerts:
# Via GitHub API: add code scanning as a required status check.
gh api \
--method PUT \
/repos/{owner}/{repo}/branches/main/protection \
--input - <<'EOF'
{
"required_status_checks": {
"strict": true,
"contexts": [
"CodeQL / Analyze (python)",
"CodeQL / Analyze (javascript)",
"CodeQL / Analyze (go)"
]
},
"enforce_admins": true,
"required_pull_request_reviews": {
"required_approving_review_count": 1,
"dismiss_stale_reviews": true
},
"restrictions": null
}
EOF
For GHAS Enterprise, use the code scanning merge protection rule (available in org settings):
# Enable code scanning merge protection: block PRs that introduce CRITICAL/HIGH alerts.
gh api \
--method POST \
/orgs/{org}/code-scanning/default-setup \
-f state=configured \
-f query_suite=security-extended \
-f languages[]="python" \
-f languages[]="javascript"
Step 6: Dependabot at Scale
Configure Dependabot for every repository with ecosystem-appropriate settings:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: weekly
day: monday
time: "03:00"
open-pull-requests-limit: 10
groups:
# Group minor/patch updates into one PR to reduce noise.
minor-and-patch:
patterns: ["*"]
update-types: ["minor", "patch"]
# Auto-merge patch updates that pass CI.
auto-merge: true
- package-ecosystem: pip
directory: "/"
schedule:
interval: weekly
ignore:
# Ignore major version bumps for framework deps (breaking changes).
- dependency-name: "django"
update-types: ["version-update:semver-major"]
- package-ecosystem: gomod
directory: "/"
schedule:
interval: daily # Go module updates are low-noise; daily is manageable.
groups:
all-go-modules:
patterns: ["*"]
update-types: ["minor", "patch"]
Configure auto-merge for security updates:
# .github/workflows/dependabot-automerge.yml
name: Dependabot auto-merge
on: pull_request
permissions:
contents: write
pull-requests: write
jobs:
auto-merge:
runs-on: ubuntu-latest
if: github.actor == 'dependabot[bot]'
steps:
- uses: actions/checkout@v4
- name: Get Dependabot metadata
id: meta
uses: dependabot/fetch-metadata@v2
- name: Auto-merge patch and minor security updates
if: |
steps.meta.outputs.update-type == 'version-update:semver-patch' ||
(steps.meta.outputs.update-type == 'version-update:semver-minor' &&
steps.meta.outputs.dependency-type == 'direct:production')
run: gh pr merge --auto --squash "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Step 7: Remediation SLAs via GitHub Issues
Track GHAS alert remediation with automated SLA enforcement:
# .github/workflows/ghas-sla-check.yml
name: GHAS SLA Enforcement
on:
schedule:
- cron: "0 9 * * *" # Daily at 9am.
jobs:
sla-check:
runs-on: ubuntu-latest
steps:
- name: Check overdue GHAS alerts
run: |
# Find CRITICAL code scanning alerts open > 7 days.
gh api /repos/${{ github.repository }}/code-scanning/alerts \
--paginate \
-q '.[] | select(.state=="open" and .rule.security_severity_level=="critical") |
{number: .number, rule: .rule.id, created: .created_at, url: .html_url}' \
| jq -r '. | select(
(now - (.created | fromdateiso8601)) > (7 * 86400)
) | "OVERDUE: \(.rule) alert #\(.number) \(.url)"' \
| tee overdue-alerts.txt
if [ -s overdue-alerts.txt ]; then
echo "::warning::Overdue CRITICAL alerts found"
cat overdue-alerts.txt | while read line; do
echo "Alerting: $line"
# Post to Slack or create a GitHub issue.
done
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Step 8: Telemetry
ghas_secret_scanning_alerts_open{repo, secret_type} gauge
ghas_secret_scanning_bypasses_total{repo, user} counter
ghas_code_scanning_alerts_open{repo, severity, rule} gauge
ghas_dependabot_alerts_open{repo, ecosystem, severity} gauge
ghas_dependabot_prs_open{repo, ecosystem} gauge
ghas_dependabot_prs_merged_total{repo, update_type} counter
ghas_alert_mean_time_to_remediate{severity} histogram
Alert on:
ghas_secret_scanning_alerts_opennon-zero — any unresolved secret scanning alert is a credential at risk.ghas_secret_scanning_bypasses_totalnon-zero — push protection bypassed; requires manual review.ghas_code_scanning_alerts_open{severity="critical"}> 0 for > 7 days — SLA breach; escalate.ghas_dependabot_alerts_open{severity="critical"}> 0 for > 14 days — critical vulnerability unpatched.
Expected Behaviour
| Signal | Default GHAS | Hardened GHAS |
|---|---|---|
| Internal API key committed | Not detected (not a built-in pattern) | Detected by custom pattern; push blocked |
| Developer bypasses push protection | Silent | Webhook fires; security team alerted within minutes |
| SQL injection introduced in PR | Possibly caught by default suite | Caught by security-extended suite; PR blocked |
| Critical CVE in dependency | Alert created; no one acts | Auto-PR created; auto-merged if tests pass; escalated if > 14 days |
| CodeQL scan scope | PR + default branch only | PR + weekly full scan + custom queries |
| Alert remediation SLA | None enforced | Daily SLA check; Slack/issue on breach |
Trade-offs
| Aspect | Benefit | Cost | Mitigation |
|---|---|---|---|
| security-extended query suite | More vulnerabilities found | Higher false-positive rate (some queries are medium-confidence) | Triage false positives and dismiss with rationale; retain signal-to-noise ratio. |
| Push protection org-wide | Secrets blocked before entering history | Breaks workflows that test with live credentials (bad practice) | Replace test credentials with dummy values or mocked endpoints; fix the workflow, not the policy. |
| Dependabot auto-merge | Zero-lag patch application | Automated merges can break on API changes in minor updates | Require CI to pass before auto-merge; restrict auto-merge to patch updates only. |
| Custom CodeQL queries | Org-specific vulnerability detection | Query development effort | Start with SAST queries from the CodeQL community; adapt to your codebase. |
| SLA enforcement automation | Compliance pressure to remediate | May generate noise if not tuned | Start with CRITICAL-only SLAs; add HIGH after workflow is established. |
Failure Modes
| Failure | Symptom | Detection | Recovery |
|---|---|---|---|
| Secret scanning pattern too broad | High false-positive rate; developers dismiss all alerts | Alert dismissal rate > 80% | Tighten regex; add additional_match context; test with negative_test_strings. |
| CodeQL fails to build | Scan silently skipped; no alerts generated | GitHub Actions shows “Autobuild failed”; no code scanning alerts for days | Add explicit build steps to the CodeQL workflow instead of relying on autobuild. |
| Dependabot PR conflicts | PR becomes stale; never merges | Dependabot PRs open > 30 days | Rebase or recreate PRs weekly via Dependabot’s “Rebase this PR” comment. |
| Push protection misconfigured for monorepo | Some sub-directories not scanned | Secret in sub-directory committed without alert | Verify scanning covers all directories; push protection is repo-level, not directory-level. |
| GHAS alerts not surfaced to security team | Alerts accumulate unreviewed | Alert age metrics show growing backlog | Route alerts to a dedicated security queue; assign triage rotation. |
| Auto-merge breaks production | A minor version bump contains a breaking change | CI failure post-merge | Require full test suite on Dependabot PRs before auto-merge; pin major versions where APIs are unstable. |