GitHub Advanced Security: Hardened Apps

TT
GitHub Advanced Security: Hardened Apps

GitHub Advanced Security: Hardened Apps

GitHub Advanced Security (GHAS) is a suite of security features built into GitHub that catches vulnerabilities before they reach production. CodeQL finds code-level security flaws through semantic analysis, Dependabot surfaces and patches vulnerable dependencies, secret scanning blocks accidentally committed credentials, and security policies enforce review requirements. Together they shift security left—catching issues in pull requests, not production incidents.

GHAS is free for public repositories. For private repositories, it requires GitHub Enterprise or GitHub Team with GHAS add-on.


CodeQL: Semantic Code Analysis

CodeQL compiles your source code into a database and runs queries against it. Unlike regex-based linters, CodeQL understands data flow—it can trace a value from a user-supplied HTTP parameter through three function calls to a SQL query and flag the injection.

Enable CodeQL in Actions

yaml
# .github/workflows/codeql.yml
name: CodeQL Analysis

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 2 * * 1'  # Weekly full scan on Monday 2am

jobs:
  analyze:
    name: Analyze (${{ matrix.language }})
    runs-on: ubuntu-latest
    permissions:
      security-events: write  # required to upload results
      actions: read
      contents: read

    strategy:
      fail-fast: false
      matrix:
        language: [javascript-typescript, python]
        # Supported: c-cpp, csharp, go, java-kotlin, javascript-typescript, python, ruby, swift

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Initialize CodeQL
        uses: github/codeql-action/init@v3
        with:
          languages: ${{ matrix.language }}
          queries: security-extended  # security-and-quality for broader coverage

      - name: Build (auto-detect or manual)
        uses: github/codeql-action/autobuild@v3
        # For compiled languages, replace with your actual build command:
        # run: |
        #   mvn package -DskipTests
        #   ./gradlew build

      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@v3
        with:
          category: /language:${{ matrix.language }}
          upload: always

Custom CodeQL Queries

You can write custom queries for application-specific patterns:

ql
// .github/codeql/custom-queries/hardcoded-password.ql
/**
 * @name Hardcoded password in variable assignment
 * @description Detects password-like variable names assigned string literals
 * @kind problem
 * @problem.severity error
 * @security-severity 9.0
 * @id js/hardcoded-password
 * @tags security
 */

import javascript

from AssignExpr assign, StringLiteral str
where
  assign.getRhs() = str and
  str.getStringValue().length() > 6 and
  assign.getLhs().(PropAccess).getPropertyName().toLowerCase().matches([
    "password", "passwd", "secret", "apikey", "api_key", "token"
  ])
select assign, "Possible hardcoded credential: " + assign.getLhs().(PropAccess).getPropertyName()
yaml
# Register custom queries in codeql-config.yml
name: Custom CodeQL Config
queries:
  - uses: ./.github/codeql/custom-queries
disable-default-queries: false
query-filters:
  - exclude:
      tags: experimental

Understanding CodeQL Results

CodeQL results appear in the Security → Code scanning tab. Each alert shows:

text
Alert: SQL Injection
Severity: Critical
File: src/api/users.ts:47
Description: This query depends on a user-provided value.

Data flow:
  req.params.id (line 23, user input)
    → userId (line 27, assignment)
      → `SELECT * FROM users WHERE id = ${userId}` (line 47, SQL sink)

Fix pattern:

typescript
// Flagged: string interpolation in SQL
const result = await db.query(`SELECT * FROM users WHERE id = ${req.params.id}`);

// Fixed: parameterized query
const result = await db.query('SELECT * FROM users WHERE id = $1', [req.params.id]);

Dependabot: Automated Dependency Updates

Dependabot Alerts vs Dependabot Security Updates

  • Alerts: GitHub detects a CVE in your dependencies and notifies you. Informational only.
  • Security Updates: Dependabot automatically opens a PR to bump the vulnerable dependency to a patched version.
  • Version Updates: Dependabot opens PRs to keep all dependencies up to date on a schedule (not just security fixes).

Configure Dependabot

yaml
# .github/dependabot.yml
version: 2
updates:
  # npm/yarn
  - package-ecosystem: npm
    directory: /
    schedule:
      interval: weekly
      day: monday
      time: "08:00"
      timezone: "America/New_York"
    open-pull-requests-limit: 5
    labels:
      - dependencies
      - javascript
    ignore:
      # Pin major versions manually
      - dependency-name: react
        update-types: [version-update:semver-major]
    groups:
      # Bundle minor/patch updates to reduce PR noise
      dev-dependencies:
        dependency-type: development
        update-types:
          - minor
          - patch

  # Python
  - package-ecosystem: pip
    directory: /backend
    schedule:
      interval: weekly
    labels:
      - dependencies
      - python

  # Docker base images
  - package-ecosystem: docker
    directory: /
    schedule:
      interval: monthly

  # GitHub Actions
  - package-ecosystem: github-actions
    directory: /
    schedule:
      interval: weekly

Auto-Merge Dependabot PRs

For patch-level dependency updates, auto-merge after CI passes:

yaml
# .github/workflows/dependabot-auto-merge.yml
name: Auto-merge Dependabot PRs

on:
  pull_request:
    types: [opened, synchronize, reopened]

permissions:
  contents: write
  pull-requests: write

jobs:
  auto-merge:
    runs-on: ubuntu-latest
    if: github.actor == 'dependabot[bot]'
    steps:
      - name: Fetch Dependabot metadata
        id: metadata
        uses: dependabot/fetch-metadata@v2
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}

      - name: Auto-merge patch and minor dev-dependency updates
        if: |
          steps.metadata.outputs.update-type == 'version-update:semver-patch' ||
          (steps.metadata.outputs.update-type == 'version-update:semver-minor' &&
           steps.metadata.outputs.dependency-type == 'direct:development')
        run: gh pr merge --auto --squash "$PR_URL"
        env:
          PR_URL: ${{ github.event.pull_request.html_url }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Secret Scanning and Push Protection

Secret scanning detects over 200 token patterns from major service providers (AWS, GitHub, Slack, Stripe, etc.) committed to repositories.

Push Protection

Push protection blocks pushes containing detected secrets before they ever reach the repository:

text
$ git push origin feature/payment-integration
remote: Resolving deltas: 100% (3/3), done.
remote: error: GH013: Repository rule violations found for refs/heads/feature/payment-integration.
remote:
remote: - Push cannot contain secrets
remote:   —————————————————————————————————————————
remote:    GITHUB PUSH PROTECTION
remote:   —————————————————————————————————————————
remote:    Detected secrets in commits:
remote:    - Stripe Secret Key in src/config/payment.ts (line 4)
remote:   
remote:   To skip push protection use:
remote:   --no-verify (if you are certain this is a false positive)
remote:   or use the GitHub interface at https://github.com/.../security/secret-scanning/unblock-secret/...
To https://github.com/org/repo.git
 ! [remote rejected] feature → feature (push declined due to repository rule violations)

Enable via Repository Settings

text
Settings → Security → Code security and analysis:
☑ Dependabot alerts
☑ Dependabot security updates  
☑ CodeQL analysis
☑ Secret scanning
☑ Push protection

Or via API for bulk organization enablement:

bash
# Enable GHAS for all repos in an organization
gh api \
  -X PATCH \
  /orgs/{org}/repos \
  --field security_and_analysis='{"advanced_security":{"status":"enabled"},"secret_scanning":{"status":"enabled"},"secret_scanning_push_protection":{"status":"enabled"}}'

Custom Secret Patterns

Add patterns for internal tokens your org uses:

text
Settings → Security → Code security and analysis → Custom patterns → New pattern

Name: Internal API Token
Secret format: (MYAPP-[A-Z0-9]{32})
Test string: MYAPP-K9X2M4L8N1R5T7W3Q6Y0P2V4J8H6F1A3

Security Policy

A SECURITY.md file tells researchers how to responsibly disclose vulnerabilities:

markdown
<!-- SECURITY.md -->
# Security Policy

## Supported Versions

| Version | Supported          |
| ------- | ------------------ |
| 2.x     | :white_check_mark: |
| 1.x     | :x:                |

## Reporting a Vulnerability

**Do not open a public GitHub issue for security vulnerabilities.**

Please report security issues to security@yourcompany.com. Include:
- Description of the vulnerability
- Steps to reproduce
- Potential impact
- Suggested fix (optional)

We will acknowledge receipt within 48 hours and provide a fix timeline within 7 days.

## Security Advisories

Published security advisories are available at:
https://github.com/org/repo/security/advisories

GitHub's Private Vulnerability Reporting feature (enabled in Settings → Security) lets researchers submit reports directly in GitHub without disclosing publicly:

text
Settings → Security → Private vulnerability reporting: Enabled

Branch Protection with Security Requirements

text
Settings → Branches → Add rule → Branch name pattern: main

☑ Require a pull request before merging
  ☑ Require approvals: 2
  ☑ Dismiss stale pull request approvals when new commits are pushed
  ☑ Require review from Code Owners

☑ Require status checks to pass before merging
  Required checks:
  - CodeQL Analysis (javascript-typescript)
  - Dependabot check
  - Security scan

☑ Require conversation resolution before merging
☑ Require signed commits
☑ Include administrators

CODEOWNERS for Security Files

text
# .github/CODEOWNERS

# Security-sensitive files require security team review
/.github/workflows/        @org/security-team @org/platform-team
/src/auth/                 @org/security-team
/src/payment/              @org/security-team @org/payments-team
SECURITY.md                @org/security-team
docker-compose.yml         @org/platform-team

Security Dashboard and Metrics

text
Security tab → Overview:
- Dependabot: 3 critical, 7 high, 12 medium
- Code scanning: 2 high (CodeQL), 0 medium
- Secret scanning: 0 active secrets

Organization Security Overview (Enterprise):
- View all repos with open alerts
- Filter by alert type, severity, language
- Track mean time to remediation

Useful GitHub CLI commands for security monitoring:

bash
# List all Dependabot alerts for a repo
gh api repos/{owner}/{repo}/dependabot/alerts --jq '.[] | select(.state=="open") | {package: .dependency.package.name, severity: .security_advisory.severity, fixed_in: .security_vulnerability.first_patched_version.identifier}'

# List code scanning alerts
gh api repos/{owner}/{repo}/code-scanning/alerts --jq '.[] | select(.state=="open") | {rule: .rule.id, severity: .rule.severity, file: .most_recent_instance.location.path}'

# Dismiss a false-positive alert
gh api -X PATCH repos/{owner}/{repo}/code-scanning/alerts/{alert_number} \
  -f state=dismissed \
  -f dismissed_reason=false_positive \
  -f dismissed_comment="This is an internal test endpoint not reachable from production"

Frequently Asked Questions

Q: Does CodeQL slow down pull request checks significantly?

For most projects CodeQL adds 5–15 minutes to the CI pipeline. You can optimize by running CodeQL only on PRs targeting main (not feature branches), using autobuild for compiled languages, and enabling query caching. The security-extended query suite is slower than security-and-quality but catches more issues. For very large monorepos, run CodeQL as a separate parallel job with its own job-level concurrency group so it does not block the main CI feedback loop.

Q: Can I configure Dependabot to ignore certain CVEs?

Yes. Use the ignore key in .github/dependabot.yml to skip specific packages or CVE IDs:

yaml
ignore:
  - dependency-name: lodash
    versions: [">= 4.0.0"]          # ignore all 4.x updates
  - dependency-name: "*"
    update-types: [version-update:semver-major]  # no major bumps for anything

You can also dismiss individual Dependabot alerts in the Security tab with a reason (tolerable risk, not applicable, or false positive). Dismissed alerts do not re-open unless a new advisory is published for the same package.

Q: What happens if a developer bypasses push protection with --no-verify?

The push still goes through but GitHub logs the bypass event. Secret scanning continues to run on the repository content and will create an alert for the exposed secret. The alert appears in the Security tab and triggers email notifications. To prevent bypasses, you can restrict the bypass permission to security team members only under Settings → Security → Push protection → Who can bypass.

Q: Is GHAS useful for internal tools and private repositories without external users?

Absolutely. Internal tools are frequent targets in supply chain attacks—compromising a developer's internal tooling is how attackers pivot to production systems. Dependabot alerts on internal repos catch vulnerabilities in libraries used by internal tools before they become pivot points. CodeQL on internal repos prevents security-insecure patterns (SQL injection, SSRF) from being copy-pasted into customer-facing systems. Secret scanning on internal repos is where push protection provides the most value, since developers are more likely to commit secrets to "private" repos assuming no one is watching.