Chapter 23Lesson 02~215 minutes

CodeQL, Code Scanning, SARIF, Custom Queries, Autofix, and Security Gates: Guided Hands-On Workflow and Core Operations

You will build a public disposable repository, enable CodeQL default setup on harmless JavaScript, upload a synthetic-but-valid SARIF finding through a pinned GitHub action, inspect the resulting alert through UI and REST, and place a code-scanning rule on a disposable target branch. Every state transition is independently observable.

Default setupSARIF uploadREST evidenceRulesetsAlert lifecycle

Learning objectives

  • Create a disposable public JavaScript repository and prove its pre-scan state.
  • Enable CodeQL default setup and inspect the generated analysis without maintaining a CodeQL workflow.
  • Upload a valid synthetic SARIF result with immutable action pins and least-privilege permissions.
  • Observe one finding, rerun the same analysis identity clean, and prove the alert lifecycle through UI and REST.
  • Apply a repository-level code-scanning rule to a disposable target branch and distinguish analysis from policy.
Availability and role: this mandatory path uses a GitHub.com public repository and standard GitHub-hosted runners. Repository administration is required to enable CodeQL/default setup and create a repository ruleset. No paid GitHub Code Security subscription is required for the public lab.
Safety boundary: the source file is harmless. The “High” finding is generated by a synthetic SARIF training tool and explicitly says it is not a vulnerability claim. Do not copy deliberately exploitable examples into public repositories merely to obtain a red badge.

1. Preflight: predict the hosted objects

Write these predictions before running commands: (1) creating the repository changes hosted Git state but creates no code-scanning analysis; (2) enabling default setup creates GitHub-managed CodeQL configuration, not a workflow file committed by you; (3) uploading SARIF creates a separate third-party-tool analysis stream; (4) the synthetic finding will be High because its SARIF rule carries security score 8.0; and (5) a ruleset can block a PR without changing the alert itself.

gh auth status --hostname github.com
OWNER=$(gh api -H "X-GitHub-Api-Version: 2026-03-10" user --jq .login)
REPO="$OWNER/code-scanning-evidence-lab"

gh repo create "$REPO" --public --add-readme --clone
cd code-scanning-evidence-lab
DEFAULT_BRANCH=$(git branch --show-current)
printf 'Repository: %s\nDefault branch: %s\n' "$REPO" "$DEFAULT_BRANCH"
gh repo view "$REPO" --json nameWithOwner,visibility,viewerPermission,defaultBranchRef

2. Commit supported but harmless source

Default setup only analyzes CodeQL-supported languages. JavaScript/TypeScript is supported, so a tiny JavaScript file is enough to make the configuration observable without adding dependencies or executing untrusted package scripts.

mkdir -p src training
cat > src/app.js <<'JS'
function greet(name) {
  return `Hello, ${name}`;
}
console.log(greet("academy"));
JS

git add src/app.js
git commit -m "Add harmless JavaScript sample"
git push origin HEAD
COMMIT_SHA=$(git rev-parse HEAD)
printf 'Committed source SHA: %s\n' "$COMMIT_SHA"

Before enabling scanning, open the repository Security/code-scanning view and confirm there is no CodeQL analysis yet. The source commit exists; the hosted security-analysis objects do not.

3. Enable CodeQL default setup and inspect coverage

In the current GitHub UI, open Settings → Advanced Security. Under CodeQL analysis, choose Set up → Default. Confirm JavaScript/TypeScript is selected and start with the Default query suite. Enabling default setup is a repository security-configuration mutation, so do it only in this disposable repository.

Default setup is GitHub-managed. Do not expect a new .github/workflows/codeql.yml commit. Wait for the initial analysis, then inspect tool status and workflow/run evidence.

# Read the managed default-setup configuration.
gh api -H "X-GitHub-Api-Version: 2026-03-10" \
  "repos/$REPO/code-scanning/default-setup"

# Inspect current analyses.
gh api --paginate \
  -H "X-GitHub-Api-Version: 2026-03-10" \
  "repos/$REPO/code-scanning/analyses?per_page=100" \
  --jq '.[] | {id,ref,commit_sha,tool:.tool.name,category,created_at}'

# An empty alert list is a valid result for harmless source.
gh api --paginate \
  -H "X-GitHub-Api-Version: 2026-03-10" \
  "repos/$REPO/code-scanning/alerts?state=open&per_page=100" \
  --jq '.[] | {number,tool:.tool.name,rule:.rule.id,state}'

Record the CodeQL analysis commit SHA. It should correspond to a repository revision—not to “whatever is currently on your laptop.” If the analysis reports no alerts, that is expected for this intentionally boring source and does not mean CodeQL failed.

4. Add a separate synthetic SARIF analysis stream

Now add a third-party-style SARIF producer. The workflow needs security-events: write to upload code-scanning results and only contents: read to inspect the repository. Both executable actions are pinned to full commit SHAs resolved for this chapter. Checkout does not persist its credential.

name: Academy SARIF training
on:
  workflow_dispatch:
    inputs:
      result_set:
        description: Synthetic analysis result
        required: true
        type: choice
        options:
          - finding
          - clean
        default: finding
  pull_request:
    branches:
      - gate-target
permissions:
  contents: read
  security-events: write
jobs:
  sarif:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout exact workflow revision
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - name: Choose training result
        id: choose
        env:
          MANUAL_MODE: ${{ inputs.result_set || 'auto' }}
        shell: bash
        run: |
          mode="$MANUAL_MODE"
          if [[ "$mode" == "auto" ]]; then
            if [[ -f training/finding.flag ]]; then mode="finding"; else mode="clean"; fi
          fi
          echo "mode=$mode" >> "$GITHUB_OUTPUT"
      - name: Generate valid SARIF fixture
        env:
          RESULT_MODE: ${{ steps.choose.outputs.mode }}
        shell: bash
        run: |
          python - <<'PYFIX'
          import json, os
          finding = {
            "ruleId": "ACADEMY/TRAIN001",
            "level": "warning",
            "message": {"text": "Synthetic training finding on harmless code; not a vulnerability claim."},
            "partialFingerprints": {"primaryLocationLineHash": "academy-train-001-v1"},
            "locations": [{"physicalLocation": {
              "artifactLocation": {"uri": "src/app.js"},
              "region": {"startLine": 1, "startColumn": 1}
            }}]
          }
          doc = {
            "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
            "version": "2.1.0",
            "runs": [{
              "tool": {"driver": {
                "name": "Academy Training Static Analyzer",
                "semanticVersion": "1.0.0",
                "rules": [{
                  "id": "ACADEMY/TRAIN001",
                  "shortDescription": {"text": "Training-only security finding"},
                  "fullDescription": {"text": "A synthetic result used to teach SARIF identity and gating."},
                  "properties": {
                    "tags": ["security", "training"],
                    "precision": "very-high",
                    "security-severity": "8.0"
                  }
                }]
              }},
              "results": [finding] if os.environ["RESULT_MODE"] == "finding" else []
            }]
          }
          with open("results.sarif", "w", encoding="utf-8") as f:
            json.dump(doc, f, indent=2)
          print("Synthetic result mode:", os.environ["RESULT_MODE"])
          PYFIX
      - name: Upload SARIF
        uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 line
        with:
          sarif_file: results.sarif
          category: academy-training
mkdir -p .github/workflows
cat > .github/workflows/academy-sarif.yml <<'YAML'
name: Academy SARIF training
on:
  workflow_dispatch:
    inputs:
      result_set:
        description: Synthetic analysis result
        required: true
        type: choice
        options:
          - finding
          - clean
        default: finding
  pull_request:
    branches:
      - gate-target
permissions:
  contents: read
  security-events: write
jobs:
  sarif:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout exact workflow revision
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - name: Choose training result
        id: choose
        env:
          MANUAL_MODE: ${{ inputs.result_set || 'auto' }}
        shell: bash
        run: |
          mode="$MANUAL_MODE"
          if [[ "$mode" == "auto" ]]; then
            if [[ -f training/finding.flag ]]; then mode="finding"; else mode="clean"; fi
          fi
          echo "mode=$mode" >> "$GITHUB_OUTPUT"
      - name: Generate valid SARIF fixture
        env:
          RESULT_MODE: ${{ steps.choose.outputs.mode }}
        shell: bash
        run: |
          python - <<'PYFIX'
          import json, os
          finding = {
            "ruleId": "ACADEMY/TRAIN001",
            "level": "warning",
            "message": {"text": "Synthetic training finding on harmless code; not a vulnerability claim."},
            "partialFingerprints": {"primaryLocationLineHash": "academy-train-001-v1"},
            "locations": [{"physicalLocation": {
              "artifactLocation": {"uri": "src/app.js"},
              "region": {"startLine": 1, "startColumn": 1}
            }}]
          }
          doc = {
            "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
            "version": "2.1.0",
            "runs": [{
              "tool": {"driver": {
                "name": "Academy Training Static Analyzer",
                "semanticVersion": "1.0.0",
                "rules": [{
                  "id": "ACADEMY/TRAIN001",
                  "shortDescription": {"text": "Training-only security finding"},
                  "fullDescription": {"text": "A synthetic result used to teach SARIF identity and gating."},
                  "properties": {
                    "tags": ["security", "training"],
                    "precision": "very-high",
                    "security-severity": "8.0"
                  }
                }]
              }},
              "results": [finding] if os.environ["RESULT_MODE"] == "finding" else []
            }]
          }
          with open("results.sarif", "w", encoding="utf-8") as f:
            json.dump(doc, f, indent=2)
          print("Synthetic result mode:", os.environ["RESULT_MODE"])
          PYFIX
      - name: Upload SARIF
        uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 line
        with:
          sarif_file: results.sarif
          category: academy-training
YAML

git add .github/workflows/academy-sarif.yml
git commit -m "Add synthetic SARIF training workflow"
git push origin HEAD
WORKFLOW_SHA=$(git rev-parse HEAD)
printf 'Workflow commit: %s\n' "$WORKFLOW_SHA"

This workflow does not claim to discover a real vulnerability. It creates a valid SARIF security rule with score 8.0, which GitHub maps to High, and a stable category academy-training. The stable category is intentional: it lets later clean analysis update the same logical stream.

5. Produce and inspect the synthetic finding

gh workflow run academy-sarif.yml --repo "$REPO" --ref "$DEFAULT_BRANCH" -f result_set=finding

# Find the newest manual run and wait for upload processing.
RUN_ID=$(gh run list --repo "$REPO" --workflow academy-sarif.yml --event workflow_dispatch \
  --limit 1 --json databaseId --jq '.[0].databaseId')
gh run watch "$RUN_ID" --repo "$REPO" --exit-status

# Inspect structured run identity.
gh run view "$RUN_ID" --repo "$REPO" \
  --json databaseId,event,headSha,status,conclusion,url

# Inspect code-scanning alert identity.
gh api --paginate \
  -H "X-GitHub-Api-Version: 2026-03-10" \
  "repos/$REPO/code-scanning/alerts?state=open&per_page=100" \
  --jq '.[] | select(.tool.name=="Academy Training Static Analyzer") | {number,state,rule:.rule.id,security:.rule.security_severity_level,tool:.tool.name,path:.most_recent_instance.location.path,sha:.most_recent_instance.commit_sha,category:.most_recent_instance.category}'

Expected observation: one open training alert, tool name Academy Training Static Analyzer, rule ACADEMY/TRAIN001, High security severity, source path src/app.js, and category academy-training. Open the alert UI and inspect location, rule help, tool identity, and the explicit message that this is synthetic.

6. Rerun the same analysis identity clean and prove state change

A real fix means the next analysis no longer reports the finding. We model that lifecycle without changing the harmless source by re-running the synthetic analyzer in clean mode under the same tool and category.

gh workflow run academy-sarif.yml --repo "$REPO" --ref "$DEFAULT_BRANCH" -f result_set=clean
CLEAN_RUN=$(gh run list --repo "$REPO" --workflow academy-sarif.yml --event workflow_dispatch \
  --limit 1 --json databaseId --jq '.[0].databaseId')
gh run watch "$CLEAN_RUN" --repo "$REPO" --exit-status

# The open list should no longer contain TRAIN001 after processing completes.
gh api --paginate \
  -H "X-GitHub-Api-Version: 2026-03-10" \
  "repos/$REPO/code-scanning/alerts?per_page=100" \
  --jq '.[] | select(.tool.name=="Academy Training Static Analyzer") | {number,state,fixed_at,dismissed_reason,category:.most_recent_instance.category}'

The significant difference is fixed versus dismissed. The clean analysis provides evidence that this tool/category no longer reports the result. You did not suppress the alert or change a triage label to obtain green state.

7. Add a non-production code-scanning gate

Create a disposable base branch named gate-target, then use a repository ruleset that targets only that branch. This isolates policy testing from the repository’s default branch.

git switch -c gate-target
git push -u origin gate-target
git switch "$DEFAULT_BRANCH"

In Settings → Rules → Rulesets, create a new branch ruleset named chapter23-training-gate. Target only gate-target. Under Branch protections select Require code scanning results. Add Academy Training Static Analyzer as the required tool, set ordinary Alerts to None, and Security alerts to High or higher. Set enforcement to Active only for this disposable target branch.

Create a feature branch that adds the marker file. On pull requests to gate-target, the SARIF workflow interprets that marker as finding.

git switch -c lab/gated-finding gate-target
printf 'training-only\n' > training/finding.flag
git add training/finding.flag
git commit -m "Trigger synthetic high finding"
git push -u origin HEAD
PR_URL=$(gh pr create --repo "$REPO" --base gate-target --head lab/gated-finding \
  --title "Lab: code scanning gate" \
  --body "Disposable PR that exercises synthetic code-scanning merge protection.")
printf '%s\n' "$PR_URL"

After the PR analysis completes, inspect the PR merge box and ruleset evaluation. The alert threshold—not the existence of a generic workflow check—is the gate condition. Then remove training/finding.flag, push the same branch, wait for a clean analysis, and confirm the rule is satisfied before merging or closing the PR. Closing without merge is sufficient for the lab.

8. Challenge: choose the correct surface

For each request, choose the smallest correct control: (A) “scan a normal public JavaScript repository with low maintenance,” (B) “run a company-specific taint query pack,” (C) “ingest results from a separate static-analysis product,” and (D) “block High-or-Critical security findings on one protected branch.”

Need Correct surface Reason
A CodeQL default setup Managed setup is sufficient; do not create an advanced workflow without a requirement.
B Advanced setup + versioned custom CodeQL query pack/suite Custom query suites require advanced control and ownership.
C SARIF upload with stable tool/category identity SARIF is the interoperability contract; CodeQL need not be the analyzer.
D Repository ruleset → Require code scanning results The gate consumes code-scanning severity evidence and is distinct from scanning itself.

Knowledge check

Why does the SARIF uploader need security-events: write?

Why is the source intentionally harmless?

What proves that the training alert was fixed rather than dismissed?

Why target gate-target instead of the default branch?

If the workflow is green but the PR is blocked, what should you inspect?

9. Verification and cleanup/rollback

Preserve the run IDs, commit SHAs, SARIF tool/category, alert number/state, and a screenshot or JSON record of the ruleset evaluation if you need evidence. Then close the disposable PR, delete the training ruleset, and remove the temporary branch if desired. Disabling CodeQL default setup is optional; archiving the disposable repository preserves evidence without leaving an active training workflow.

gh pr list --repo "$REPO" --state open --json number,title,headRefName,baseRefName
gh api -H "X-GitHub-Api-Version: 2026-03-10" "repos/$REPO/code-scanning/default-setup"
gh api --paginate -H "X-GitHub-Api-Version: 2026-03-10" \
  "repos/$REPO/code-scanning/alerts?per_page=100" \
  --jq '.[] | {number,state,tool:.tool.name,rule:.rule.id,fixed_at}'

# Reversible cleanup after reviewing the repository in the UI.
gh repo archive "$REPO" --yes
Do not delete code-scanning analyses or dismiss alerts merely as cleanup. Those are security-sensitive evidence mutations. This lab uses branch/ruleset cleanup and repository archival instead.

Summary

You proved three separate states: CodeQL managed analysis, a third-party-style SARIF analysis stream, and ruleset merge governance. You also proved that alert remediation state can come from a new analysis rather than a manual suppression.

Next, you will decide how much control and policy to add in production without turning a scanner into an unmaintainable platform.

Next lesson

CodeQL, Code Scanning, SARIF, Custom Queries, Autofix, and Security Gates: Configuration, Design Choices, and Tradeoffs

Official references

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.