- 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>
141 lines
5.7 KiB
YAML
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)
|