Files
james.maesandClaude Opus 5.5 27b5d96741 feat(ci): reusable PR, security, and CodeQL workflows and conformance checker
- pr.yml: Conventional Commit title, DCO, dependency review
- security.yml: gitleaks (full history), Trivy + OSV-Scanner (malicious
  packages), zizmor + actionlint; all pinned by SHA or digest
- codeql.yml: security-extended with a high-severity gate
- this repo's CI, Scorecard, Dependabot, CODEOWNERS
- templates for repository files and caller workflows
- tools/Test-RepoConformance.ps1: checks K22-REPO-* and K22-CI-* via the
  GitHub API and a local clone, honoring the exception register

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
2026-10-09 07:53:21 -05:00

141 lines
5.7 KiB
YAML

# Static analysis (K22-CI-22): CodeQL with the security-extended suite, failing on
# alerts of high severity or above. Called as:
#
# jobs:
# codeql:
# uses: KofTwentyTwo/standards/.github/workflows/codeql.yml@<sha> # vX.Y.Z
# permissions:
# contents: read
# security-events: write
# with:
# languages: '["csharp", "actions"]'
# build-mode: manual
# build-command: dotnet build MySolution.slnx -p:Platform=x64
# runs-on: windows-latest
#
# producing `codeql / analyze (<language>)` for each language. The CodeQL action
# uploads results but never fails a build on them, so the last step reads the SARIF
# and fails on any result whose rule has security-severity >= the threshold.
# Suppress a false positive with a query filter in a CodeQL config file passed as
# `config-file`, citing the exception record in a comment.
name: codeql
on:
workflow_call:
inputs:
languages:
description: JSON array of CodeQL languages, e.g. '["csharp","actions"]'.
type: string
required: true
build-mode:
description: CodeQL build mode for compiled languages (none, autobuild, manual). Interpreted languages always use none.
type: string
default: autobuild
build-command:
description: Command run for build-mode manual.
type: string
default: ""
runs-on:
description: Runner label; Windows-only builds (WinUI, WPF) need windows-latest.
type: string
default: ubuntu-24.04
setup-dotnet:
description: Install the .NET SDK from global.json before building.
type: boolean
default: false
config-file:
description: Optional path to a CodeQL configuration file in the calling repository.
type: string
default: ""
fail-on-security-severity:
description: Minimum CVSS-style security-severity that fails the job (7.0 = high).
type: string
default: "7.0"
permissions: {}
jobs:
analyze:
name: analyze (${{ matrix.language }})
runs-on: ${{ inputs.runs-on }}
timeout-minutes: 60
permissions:
contents: read
security-events: write # upload SARIF results to code scanning
strategy:
fail-fast: false
matrix:
language: ${{ fromJSON(inputs.languages) }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up .NET SDK
if: inputs.setup-dotnet
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
global-json-file: global.json
- name: Initialize CodeQL
uses: github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
with:
languages: ${{ matrix.language }}
build-mode: ${{ contains(fromJSON('["actions","javascript-typescript","python","ruby"]'), matrix.language) && 'none' || inputs.build-mode }}
queries: security-extended
config-file: ${{ inputs.config-file }}
- name: Build (manual mode)
if: inputs.build-mode == 'manual' && !contains(fromJSON('["actions","javascript-typescript","python","ruby"]'), matrix.language)
shell: bash
env:
BUILD_COMMAND: ${{ inputs.build-command }}
run: |
if [[ -z "$BUILD_COMMAND" ]]; then
echo "::error::build-mode is manual but build-command is empty"
exit 1
fi
eval "$BUILD_COMMAND"
- name: Analyze
uses: github/codeql-action/analyze@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
with:
category: /language:${{ matrix.language }}
output: codeql-sarif
# Fork pull requests have a read-only token; analyze still runs and gates.
upload: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) && 'never' || 'always' }}
- name: Gate on security severity
shell: python
env:
SARIF_DIR: codeql-sarif
THRESHOLD: ${{ inputs.fail-on-security-severity }}
run: |
import json, os, pathlib, sys
threshold = float(os.environ["THRESHOLD"])
blocking = []
for path in pathlib.Path(os.environ["SARIF_DIR"]).glob("*.sarif"):
for run in json.loads(path.read_text(encoding="utf-8"))["runs"]:
rules = {}
for component in [run["tool"]["driver"], *run["tool"].get("extensions", [])]:
for rule in component.get("rules", []):
rules[rule["id"]] = rule
for result in run.get("results", []):
if result.get("suppressions"):
continue
rule = rules.get(result.get("ruleId"), {})
severity = float(rule.get("properties", {}).get("security-severity", 0) or 0)
if severity >= threshold:
location = result.get("locations", [{}])[0].get("physicalLocation", {})
where = "{}:{}".format(
location.get("artifactLocation", {}).get("uri", "?"),
location.get("region", {}).get("startLine", "?"))
blocking.append((severity, result.get("ruleId"), where, result["message"]["text"]))
for severity, rule_id, where, message in sorted(blocking, reverse=True):
print(f"::error file={where.split(':')[0]},title={rule_id} ({severity})::{message}")
print(f"{len(blocking)} CodeQL result(s) at security-severity >= {threshold}")
sys.exit(1 if blocking else 0)