Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/codeql/codeql-config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# SPDX-License-Identifier: MPL-2.0
# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath)
#
# CodeQL configuration for hyperpolymath/standards.
#
# ⚠ THIS FILE IS NOT PICKED UP AUTOMATICALLY. `codeql.yml` names it explicitly
# via the reusable's `config-file` input, because codeql-action v4.38.0 does not
# auto-discover `.github/codeql/codeql-config.yml`: `getConfigFileInput()` in
# codeql-action/src/config/file.ts returns `undefined` unless the `config-file`
# input (or a repository property, or default setup) supplies a path, and
# `determineUserConfig()` then logs "No configuration file was provided".
#
# ── Why `paths-ignore` for the archive ─────────────────────────────────────
# `9-archive/` holds *vendored snapshots* of other repositories
# (rhodium-standard-repositories and its satellites/examples), kept as
# provenance for the de-vendoring campaign (standards#493). Two facts make
# alerting on it actively harmful here:
#
# 1. The code is not built, shipped or executed by this repository. A
# finding there describes software this project does not produce.
# 2. It cannot be fixed. Editing a vendored snapshot destroys the fidelity
# that is the archive's entire purpose (see the SOLE-SOUND-COPY rows in
# `.machine_readable/estate-residue-ledger.tsv`), so every alert raised
# inside it becomes either permanent noise or a dismissal.
#
# The measured effect of leaving it in scope is that archived third-party code
# competes for triage attention with the code that IS shipped β€” the failure
# mode `docs/audits/2026-08-07-estate-cicd-debt.adoc` records at estate scale
# (545 alerts, 106 gating repositories).
#
# This is an explicit scope boundary, not a suppression: nothing outside
# `paths-ignore` is excluded, and the archive remains covered by the
# text-level content rules in `.hypatia-baseline.json` and the workflow
# staleness gate. De-vendoring (rather than hiding) remains the tracked exit:
# standards#493.
name: "standards CodeQL configuration"

paths-ignore:
- 9-archive/**
21 changes: 17 additions & 4 deletions .github/workflows/changelog-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,12 @@ jobs:
- name: Mode = commit-back β€” commit regenerated CHANGELOG.md
if: ${{ inputs.mode == 'commit-back' && github.event_name == 'push' }}
working-directory: caller
env:
# Never interpolate `${{ github.ref_name }}` into the shell body
# (CodeQL actions/command-injection): a git ref may legally contain
# `$`, `(`, `)`, `;`, `|` and `&`, so a branch named `$(cmd)` is
# executed by the runner. Passing it through `env:` makes it data.
BRANCH: ${{ github.ref_name }}
run: |
set -euo pipefail
if [ -f CHANGELOG.md ] && diff -q CHANGELOG.md CHANGELOG.md.new >/dev/null; then
Expand All @@ -206,13 +212,16 @@ jobs:
See standards/templates/cliff.toml for the canonical config.

Closes part of the 2026-05-26 CHANGELOG gap (standards#197 audit)."
git push origin HEAD:${{ github.ref_name }}
git push origin "HEAD:$BRANCH"

- name: Mode = pr-back β€” open PR with regenerated CHANGELOG.md
if: ${{ inputs.mode == 'pr-back' && github.event_name == 'push' }}
working-directory: caller
env:
GH_TOKEN: ${{ github.token }}
# Same reason as the commit-back step: ref names reach the shell as
# data, never as template expansion (actions/command-injection).
BASE_BRANCH: ${{ github.ref_name }}
run: |
set -euo pipefail
if [ -f CHANGELOG.md ] && diff -q CHANGELOG.md CHANGELOG.md.new >/dev/null; then
Expand All @@ -233,7 +242,7 @@ jobs:
# against the same base so we don't accumulate duplicate PRs if a
# maintainer hasn't merged the previous one before another push lands.
for pr in $(gh pr list --repo "${{ github.repository }}" \
--base "${{ github.ref_name }}" \
--base "$BASE_BRANCH" \
--head 'bot/changelog-' \
--state open \
--json number,headRefName \
Expand All @@ -255,16 +264,20 @@ jobs:
Please do not remove that marker when editing the description.

Closes part of the 2026-05-26 CHANGELOG gap (standards#197 audit)." \
--base "${{ github.ref_name }}" \
--base "$BASE_BRANCH" \
--head "$branch"

- name: Mode = release-only β€” attach to release
if: ${{ inputs.mode == 'release-only' && github.event_name == 'release' }}
env:
GH_TOKEN: ${{ github.token }}
# Release tag names are event data and may contain shell
# metacharacters; pass through env, never inline
# (actions/command-injection).
TAG: ${{ github.event.release.tag_name }}
run: |
set -euo pipefail
tag="${{ github.event.release.tag_name }}"
tag="$TAG"
gh release upload "$tag" caller/CHANGELOG.md.new \
--clobber \
--repo "${{ github.repository }}"
Expand Down
65 changes: 46 additions & 19 deletions .github/workflows/check-suite-monitor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,35 +33,47 @@ jobs:

- name: Get Check Suite Details
id: check-suite
env:
# Event payload through `env:`, never inlined into the shell body
# (CodeQL actions/command-injection). `check_suite` fires for every
# check suite in the repository, including suites belonging to
# fork pull requests, so its fields are attacker-adjacent data and
# must never be re-parsed as shell syntax. They also cross into
# $GITHUB_OUTPUT, where one embedded newline would forge a second
# output line.
CHECK_SUITE_ID: ${{ github.event.check_suite.id }}
EVENT_ACTION: ${{ github.event.action }}
PR_ID: ${{ github.event.check_suite.pull_requests[0].id }}
PR_NUMBER: ${{ github.event.check_suite.pull_requests[0].number }}
HEAD_SHA: ${{ github.event.check_suite.head_sha }}
CONCLUSION: ${{ github.event.check_suite.conclusion }}
REPO: ${{ github.repository }}
run: |
echo "πŸ” Retrieving check suite information..."

# Get the check suite that triggered this workflow
CHECK_SUITE_ID="${{ github.event.check_suite.id }}"
REPO="${{ github.repository }}"

if [ -n "$CHECK_SUITE_ID" ]; then
echo "check_suite_id=$CHECK_SUITE_ID" >> $GITHUB_OUTPUT
echo "check_suite_id=$CHECK_SUITE_ID" >> "$GITHUB_OUTPUT"

# Get check suite details via GitHub API
# Note: This would require a token with appropriate permissions
# For now, we'll use the context information

echo "repository=$REPO" >> $GITHUB_OUTPUT
echo "event_type=${{ github.event.action }}" >> $GITHUB_OUTPUT
echo "repository=$REPO" >> "$GITHUB_OUTPUT"
echo "event_type=$EVENT_ACTION" >> "$GITHUB_OUTPUT"

# Extract branch information
if [ "${{ github.event.check_suite.pull_requests[0].id }}" != "" ]; then
echo "is_pr=true" >> $GITHUB_OUTPUT
echo "pr_number=${{ github.event.check_suite.pull_requests[0].number }}" >> $GITHUB_OUTPUT
if [ "$PR_ID" != "" ]; then
echo "is_pr=true" >> "$GITHUB_OUTPUT"
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
else
echo "is_pr=false" >> $GITHUB_OUTPUT
echo "is_pr=false" >> "$GITHUB_OUTPUT"
fi

echo "head_sha=${{ github.event.check_suite.head_sha }}" >> $GITHUB_OUTPUT
echo "conclusion=${{ github.event.check_suite.conclusion }}" >> $GITHUB_OUTPUT
echo "head_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT"
echo "conclusion=$CONCLUSION" >> "$GITHUB_OUTPUT"
else
echo "check_suite_id=unknown" >> $GITHUB_OUTPUT
echo "check_suite_id=unknown" >> "$GITHUB_OUTPUT"
fi

- name: Analyze Check Suite Results
Expand Down Expand Up @@ -171,15 +183,30 @@ jobs:

- name: Get Workflow Run Information
id: workflow-info
env:
# `workflow_run` is triggered by *other* workflows, including ones a
# fork pull request started. Everything here is data (env), never
# template text spliced into the shell body β€” same reason as the
# check-suite job above (CodeQL actions/command-injection).
#
# `github.event.workflow` is the workflow OBJECT, not its name; the
# old spelling stringified to "[object Object]" and, worse, made the
# `contains()` guard on the next step permanently false, so the GATE
# analysis after it never ran. The name lives at `.name`.
WORKFLOW_NAME: ${{ github.event.workflow.name }}
WORKFLOW_ID: ${{ github.event.workflow_run.id }}
CONCLUSION: ${{ github.event.workflow_run.conclusion }}
RUN_NUMBER: ${{ github.event.workflow_run.run_number }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
echo "workflow_name=${{ github.event.workflow }}" >> $GITHUB_OUTPUT
echo "workflow_id=${{ github.event.workflow_run.id }}" >> $GITHUB_OUTPUT
echo "conclusion=${{ github.event.workflow_run.conclusion }}" >> $GITHUB_OUTPUT
echo "run_number=${{ github.event.workflow_run.run_number }}" >> $GITHUB_OUTPUT
echo "head_sha=${{ github.event.workflow_run.head_sha }}" >> $GITHUB_OUTPUT
echo "workflow_name=$WORKFLOW_NAME" >> "$GITHUB_OUTPUT"
echo "workflow_id=$WORKFLOW_ID" >> "$GITHUB_OUTPUT"
echo "conclusion=$CONCLUSION" >> "$GITHUB_OUTPUT"
echo "run_number=$RUN_NUMBER" >> "$GITHUB_OUTPUT"
echo "head_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT"

- name: Analyze GATE Workflow Results
if: contains(github.event.workflow, 'πŸ”΄ GATE:')
if: contains(github.event.workflow.name, 'πŸ”΄ GATE:')
run: |
CONCLUSION="${{ steps.workflow-info.outputs.conclusion }}"
WORKFLOW="${{ steps.workflow-info.outputs.workflow_name }}"
Expand Down
18 changes: 18 additions & 0 deletions .github/workflows/codeql-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,20 @@ on:
type: string
required: false
default: ubuntu-latest
config-file:
description: >-
Optional path to a CodeQL configuration file, relative to the checked-out
repository (e.g. ./.github/codeql/codeql-config.yml). Empty by default,
which is the CodeQL Action's own default and means NO configuration file:
the Action does NOT auto-discover `.github/codeql/codeql-config.yml`
(verified against codeql-action v4.38.0 β€” `getConfigFileInput()` returns
undefined unless this input, a repository property, or default setup
supplies a path), so a caller that wants one must name it here. An empty
value is passed through as undefined by the Action, so existing callers
are unaffected.
type: string
required: false
default: ''

permissions:
actions: read
Expand Down Expand Up @@ -98,6 +112,10 @@ jobs:
with:
languages: ${{ inputs.language }}
build-mode: ${{ inputs.build-mode }}
# Empty renders as '', which the Action's getOptionalInput() converts
# back to undefined β€” i.e. exactly today's behaviour for every caller
# that does not set the input.
config-file: ${{ inputs.config-file }}

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 (4.38.1 blocked estate-wide; nexia-list#100)
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,15 @@ jobs:
uses: ./.github/workflows/codeql-reusable.yml
with:
language: javascript-typescript
# Scope the analysis to code this repository actually ships. The named
# file is the only way to reach it: codeql-action v4.38.0 does NOT
# auto-discover `.github/codeql/codeql-config.yml` (getConfigFileInput()
# returns undefined without this input). See the file for why the
# vendored archive is out of scope.
config-file: ./.github/codeql/codeql-config.yml

analyze-actions:
uses: ./.github/workflows/codeql-reusable.yml
with:
language: actions
config-file: ./.github/codeql/codeql-config.yml
6 changes: 5 additions & 1 deletion .github/workflows/debt-measure.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ jobs:
if: steps.measure.outputs.changed == 'yes'
env:
GH_TOKEN: ${{ github.token }}
# Ref names are event data, not shell syntax: a branch or tag may
# legally contain `$`, `(`, `)`, `;`, `|` and `&`, and inlining
# `${{ github.ref_name }}` executes it (actions/command-injection).
BASE_BRANCH: ${{ github.ref_name }}
run: |
set -euo pipefail
BRANCH="chore/debt-measure"
Expand All @@ -113,7 +117,7 @@ jobs:
if gh pr view "$BRANCH" --json number >/dev/null 2>&1; then
gh pr edit "$BRANCH" --body "$BODY"
else
gh pr create --head "$BRANCH" --base "${{ github.ref_name }}" \
gh pr create --head "$BRANCH" --base "$BASE_BRANCH" \
--title "chore(debt): re-measure Debtfile probes" --body "$BODY"
fi

Expand Down
16 changes: 13 additions & 3 deletions .github/workflows/propagate-hooks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,15 @@ jobs:

- name: Configure
id: config
env:
# workflow_dispatch inputs are user-supplied strings, not shell
# syntax. Inlining `${{ github.event.inputs.* }}` executes whatever a
# dispatcher types (actions/command-injection).
DRY_RUN_DASH: ${{ github.event.inputs.dry-run }}
DRY_RUN_UNDERSCORE: ${{ github.event.inputs.dry_run }}
run: |
# Check if this is a dry run
if [ "${{ github.event.inputs.dry-run }}" = "true" ] || [ "${{ github.event.inputs.dry_run }}" = "true" ]; then
if [ "$DRY_RUN_DASH" = "true" ] || [ "$DRY_RUN_UNDERSCORE" = "true" ]; then
echo "dry_run=true" >> $GITHUB_OUTPUT
echo "::notice::Running in DRY RUN mode - no changes will be made"
else
Expand All @@ -87,15 +93,19 @@ jobs:
# for live repo reads. Without a token every probe fails silently
# and every repo is misclassified as hookless.
GH_TOKEN: ${{ secrets.HYPATIA_SCAN_PAT }}
# Dispatch input as data (actions/command-injection). A repo name is
# also untrusted enough to want quoting-by-construction: it reaches
# `jq --arg` below and must never be re-parsed by the shell.
TARGET_INPUT: ${{ github.event.inputs.target-repo }}
run: |
echo "πŸ” Identifying repositories that need hook updates..."

# Get the SHA of the current commit (triggering commit)
TRIGGER_SHA="${{ github.sha }}"

# Check if specific repo was requested
if [ -n "${{ github.event.inputs.target-repo }}" ]; then
TARGET="${{ github.event.inputs.target-repo }}"
if [ -n "$TARGET_INPUT" ]; then
TARGET="$TARGET_INPUT"
# -c: compact, single-line JSON β€” a multiline value breaks
# $GITHUB_OUTPUT below (same defect class as root cause 2, #807).
REPOS_JSON=$(jq -cn --arg repo "$TARGET" '[$repo]')
Expand Down
11 changes: 11 additions & 0 deletions 2-protocols/k9-coordination/generator/generate.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ function parseK9Kennel(text) {
return parseYamlLikeBlock(contentLines, 0).value;
}

// Keys come straight out of the document being parsed, so they must never be
// assigned blindly: `result["__proto__"] = value` rewrites the object's
// prototype, and `constructor`/`prototype` are the same defect one level up
// (CWE-915, CodeQL js/prototype-polluting-assignment). A K9 document has no
// legitimate use for any of the three, so reject rather than silently skip β€”
// a file that tried to smuggle one in should fail loudly.
const FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"]);

/**
* Recursively parse indented YAML-like blocks.
* Returns { value, linesConsumed }.
Expand Down Expand Up @@ -88,6 +96,9 @@ function parseYamlLikeBlock(lines, baseIndent) {
}

const key = trimmed.substring(0, colonIdx).trim();
if (FORBIDDEN_KEYS.has(key)) {
throw new Error(`forbidden key "${key}" in K9 document (prototype pollution)`);
}
const afterColon = trimmed.substring(colonIdx + 1).trim();

if (afterColon === "|") {
Expand Down
35 changes: 35 additions & 0 deletions 2-protocols/k9-coordination/generator/generate_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ function parseK9Kennel(text) {
return parseYamlLikeBlock(contentLines, 0).value;
}

// Keys come straight out of the document being parsed, so they must never be
// assigned blindly: `result["__proto__"] = value` rewrites the object's
// prototype, and `constructor`/`prototype` are the same defect one level up
// (CWE-915, CodeQL js/prototype-polluting-assignment). A K9 document has no
// legitimate use for any of the three, so reject rather than silently skip β€”
// a file that tried to smuggle one in should fail loudly.
const FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"]);

function parseYamlLikeBlock(lines, baseIndent) {
const result = {};
let i = 0;
Expand All @@ -66,6 +74,9 @@ function parseYamlLikeBlock(lines, baseIndent) {
if (colonIdx === -1) { i++; continue; }

const key = trimmed.substring(0, colonIdx).trim();
if (FORBIDDEN_KEYS.has(key)) {
throw new Error(`forbidden key "${key}" in K9 document (prototype pollution)`);
}
const afterColon = trimmed.substring(colonIdx + 1).trim();

if (afterColon === "|") {
Expand Down Expand Up @@ -295,6 +306,30 @@ project:
assertEquals(data.project.license, "PMPL-1.0-or-later");
});

Deno.test("Parser: rejects prototype-polluting keys", () => {
// CWE-915: a key read out of the document must never be assigned blindly.
// `result["__proto__"] = {...}` replaces the prototype of the parsed map, so
// a document could make `data.admin` resolve through Object.prototype (and,
// one level up, pollute it). All three magic keys are refused.
for (const key of ["__proto__", "constructor", "prototype"]) {
assertThrows(
() => parseK9Kennel(`K9!\n---\n${key}:\n polluted: true\n`),
Error,
"prototype pollution",
);
}
// A nested occurrence is refused too, not only a top-level one.
assertThrows(
() => parseK9Kennel(`K9!\n---\nproject:\n __proto__:\n polluted: true\n`),
Error,
"prototype pollution",
);
// And the guard leaves ordinary keys alone.
const ok = parseK9Kennel(`K9!\n---\nproject:\n name: "constructor-safe"\n`);
assertEquals(ok.project.name, "constructor-safe");
assertEquals(Object.prototype.polluted, undefined);
});

Deno.test("Parser: handles K9 without --- separator", () => {
const data = parseK9Kennel(`K9!
# Just a comment
Expand Down
Loading
Loading