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.
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.
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?
Uploading code-scanning results changes the repository’s code-scanning analysis state. The job does not need repository contents write permission to do that.
Why is the source intentionally harmless?
The lesson is about evidence identity and governance. A synthetic SARIF result teaches the lifecycle without publishing reusable vulnerable code.
What proves that the training alert was fixed rather than dismissed?
A later clean analysis with the same tool/category no longer reports the result, so GitHub records a fixed lifecycle rather than a human dismissal reason.
Why target gate-target instead of the default
branch?
It keeps merge-policy experimentation inside a disposable namespace and reduces the chance that a training rule blocks unrelated work.
If the workflow is green but the PR is blocked, what should you inspect?
Inspect the code-scanning ruleset result, required tool, severity threshold, and latest analysis identity. Code-scanning merge protection is not the same as an arbitrary status check.
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
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.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.