Back to Blog
3 min read

GitHub Code Scanning: Finding Vulnerabilities Before They Ship

GitHub Code Scanning automatically finds security vulnerabilities and coding errors in your code. Let’s explore how to set it up and get the most out of it.

Understanding Code Scanning

Code scanning uses CodeQL, a semantic code analysis engine, to find security vulnerabilities, bugs, and other errors in your codebase.

Basic Setup

Enable code scanning with a simple workflow:

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

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
  schedule:
    - cron: '0 8 * * 1'  # Every Monday at 8 AM

jobs:
  analyze:
    name: Analyze
    runs-on: ubuntu-latest
    permissions:
      actions: read
      contents: read
      security-events: write

    strategy:
      fail-fast: false
      matrix:
        language: [ 'csharp', 'javascript' ]

    steps:
    - name: Checkout repository
      uses: actions/checkout@v3

    - name: Initialize CodeQL
      uses: github/codeql-action/init@v2
      with:
        languages: ${{ matrix.language }}

    - name: Autobuild
      uses: github/codeql-action/autobuild@v2

    - name: Perform CodeQL Analysis
      uses: github/codeql-action/analyze@v2

Advanced Configuration

Configure CodeQL with a custom config file:

# .github/codeql/codeql-config.yml
name: "Security and Quality"

queries:
  - uses: security-extended
  - uses: security-and-quality
  - uses: ./custom-queries  # Local custom queries

paths:
  - src
  - lib

paths-ignore:
  - test
  - '**/*.test.js'
  - node_modules

query-filters:
  - exclude:
      id: js/unused-local-variable
  - exclude:
      tags contain: /maintainability/

Reference it in your workflow:

- name: Initialize CodeQL
  uses: github/codeql-action/init@v2
  with:
    languages: ${{ matrix.language }}
    config-file: ./.github/codeql/codeql-config.yml

Writing Custom CodeQL Queries

Create queries for your specific security needs:

// queries/hardcoded-credentials.ql
/**
 * @name Hardcoded credentials
 * @description Hardcoded passwords or API keys in source code
 * @kind problem
 * @problem.severity error
 * @security-severity 8.5
 * @precision high
 * @id custom/hardcoded-credentials
 * @tags security
 *       external/cwe/cwe-798
 */

import javascript

class HardcodedCredential extends DataFlow::Node {
  HardcodedCredential() {
    exists(StringLiteral s |
      this.asExpr() = s and
      s.getValue().regexpMatch("(?i).*(password|api.?key|secret|token).*=.*['\"][^'\"]{8,}['\"].*")
    )
  }
}

from HardcodedCredential cred
select cred, "Potential hardcoded credential found"

Integrating with Third-Party Tools

Run additional scanners alongside CodeQL:

name: Security Scan

on: [push, pull_request]

jobs:
  codeql:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: github/codeql-action/init@v2
      - uses: github/codeql-action/analyze@v2

  semgrep:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Semgrep
        uses: returntocorp/semgrep-action@v1
        with:
          config: >-
            p/security-audit
            p/secrets
            p/owasp-top-ten

  trivy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'fs'
          scan-ref: '.'
          format: 'sarif'
          output: 'trivy-results.sarif'

      - name: Upload Trivy scan results
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: 'trivy-results.sarif'

Handling Alerts

import requests

def get_code_scanning_alerts(owner, repo, token):
    url = f"https://api.github.com/repos/{owner}/{repo}/code-scanning/alerts"
    headers = {
        "Authorization": f"Bearer {token}",
        "Accept": "application/vnd.github+json"
    }

    response = requests.get(url, headers=headers, params={"state": "open"})
    alerts = response.json()

    critical_alerts = [a for a in alerts if a['rule']['security_severity_level'] == 'critical']

    print(f"Total open alerts: {len(alerts)}")
    print(f"Critical alerts: {len(critical_alerts)}")

    for alert in critical_alerts:
        print(f"\nRule: {alert['rule']['description']}")
        print(f"File: {alert['most_recent_instance']['location']['path']}")
        print(f"Line: {alert['most_recent_instance']['location']['start_line']}")

    return alerts

Branch Protection Rules

Require code scanning to pass:

# Use GitHub API or UI to set branch protection
name: Enforce Scanning
on:
  pull_request:
    branches: [main]

jobs:
  check-alerts:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/github-script@v6
        with:
          script: |
            const alerts = await github.rest.codeScanning.listAlertsForRepo({
              owner: context.repo.owner,
              repo: context.repo.repo,
              ref: context.payload.pull_request.head.ref,
              state: 'open'
            });

            const critical = alerts.data.filter(
              a => a.rule.security_severity_level === 'critical'
            );

            if (critical.length > 0) {
              core.setFailed(`PR has ${critical.length} critical vulnerabilities`);
            }

Code scanning is essential for modern secure development practices.

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.