Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
4 changes: 4 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Default owner for all files in resq-software/.github.
# Once a core team exists, point `*` to that team (e.g. `@resq-software/core`)
# and add narrower owners for specific paths.
* @WomB0ComB0
86 changes: 86 additions & 0 deletions .github/workflows/cpp-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Copyright 2026 ResQ Software
# SPDX-License-Identifier: Apache-2.0
#
# Reusable C++ CI — CMake configure + build + test. Matrix over OS.
# Pairs with security-scan.yml.
#
# Security: caller-controlled path/flag inputs are forwarded through
# `env:` and referenced as `"$VAR"` to prevent shell injection.

name: cpp-ci

on:
workflow_call:
inputs:
os-list:
type: string
required: false
default: '["ubuntu-latest","macos-latest","windows-latest"]'
source-dir:
type: string
required: false
default: "."
build-dir:
type: string
required: false
default: "build"
cmake-flags:
type: string
required: false
default: ""
build-config:
type: string
required: false
default: "Debug"
run-test:
type: boolean
required: false
default: true
timeout-minutes:
type: number
required: false
default: 30

permissions:
contents: read

jobs:
build-test:
name: ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: ${{ inputs.timeout-minutes }}
strategy:
fail-fast: false
matrix:
os: ${{ fromJSON(inputs.os-list) }}
steps:
- name: Harden Runner
if: runner.os == 'Linux'
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2
with:
egress-policy: audit
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Configure
shell: bash
env:
SRC: ${{ inputs.source-dir }}
BUILD: ${{ inputs.build-dir }}
FLAGS: ${{ inputs.cmake-flags }}
# shellcheck disable=SC2086 - FLAGS is intentionally word-split into
# multiple cmake arguments; quoting would pass them as a single token.
run: |
# shellcheck disable=SC2086
cmake -B "$BUILD" $FLAGS "$SRC"
Comment on lines +63 to +73

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

$FLAGS word-splitting still allows caller-controlled argument injection into cmake.

Routing cmake-flags through env: prevents shell injection into the YAML-generated script (addressing the CodeQL finding), but $FLAGS is intentionally left unquoted so a caller can pass multiple flags. That means a caller can still inject arbitrary cmake arguments — e.g. -DCMAKE_CXX_COMPILER_LAUNCHER=..., -P <script>, or extra ;-separated generator expressions — which on a reusable workflow invoked from a consumer repo is effectively arbitrary code execution on the runner via the build step.

Since this is a reusable workflow consumed across the org, the trust boundary is "whatever the calling repo's workflow passes in", which for PRs from forks can be attacker-controlled. Consider one of:

  • Documenting that cmake-flags is trusted input and must not be derived from PR-event context in consumers.
  • Accepting cmake-flags as a JSON array and expanding via jq -r '.[]' into a bash array, preserving per-arg quoting:
♻️ Array-based expansion
-      - name: Configure
-        shell: bash
-        env:
-          SRC: ${{ inputs.source-dir }}
-          BUILD: ${{ inputs.build-dir }}
-          FLAGS: ${{ inputs.cmake-flags }}
-        # shellcheck disable=SC2086 - FLAGS is intentionally word-split into
-        # multiple cmake arguments; quoting would pass them as a single token.
-        run: |
-          # shellcheck disable=SC2086
-          cmake -B "$BUILD" $FLAGS "$SRC"
+      - name: Configure
+        shell: bash
+        env:
+          SRC: ${{ inputs.source-dir }}
+          BUILD: ${{ inputs.build-dir }}
+          FLAGS_JSON: ${{ inputs.cmake-flags-json }}   # e.g. '["-DX=1","-DY=2"]'
+        run: |
+          set -eu
+          mapfile -t FLAGS < <(printf '%s' "$FLAGS_JSON" | jq -r '.[]')
+          cmake -B "$BUILD" "${FLAGS[@]}" "$SRC"

At minimum, add a comment to the workflow header making the trust assumption on cmake-flags explicit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/cpp-ci.yml around lines 63 - 73, Replace the current
unquoted $FLAGS expansion in the "Configure" step with a safe array-expansion:
accept the workflow input "cmake-flags" as a JSON array, decode it into a bash
array (e.g. FLAGS=($(jq -r '.[]' <<< "$INPUT_CMAKE_FLAGS")) or similar) in the
run block, then call cmake with quoted array expansion cmake -B "$BUILD"
"${FLAGS[@]}" "$SRC"; update the env/input handling so the input is read from
the "cmake-flags" input rather than relying on shell-splitting, and add a brief
header comment documenting that consumers must pass a JSON array for cmake-flags
(or, if you prefer minimal change, instead add a comment above the Configure
step explicitly stating that cmake-flags is trusted input and must not be
derived from untrusted PR context).

- name: Build
shell: bash
env:
BUILD: ${{ inputs.build-dir }}
CONFIG: ${{ inputs.build-config }}
run: cmake --build "$BUILD" --config "$CONFIG"
- if: ${{ inputs.run-test }}
name: Test
shell: bash
env:
BUILD: ${{ inputs.build-dir }}
CONFIG: ${{ inputs.build-config }}
run: ctest --test-dir "$BUILD" --output-on-failure -C "$CONFIG"
145 changes: 145 additions & 0 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# Copyright 2026 ResQ Software
# SPDX-License-Identifier: Apache-2.0
#
# Reusable container build + publish + provenance workflow.
# Builds via buildx, pushes to ghcr.io by default, and produces an
# SLSA build-provenance attestation (sigstore-backed) for the image
# digest.
#
# Security posture:
# 1. checkout uses persist-credentials: false so the git token is not
# exposed to the fetched code (mitigates the "untrusted checkout
# in trusted context" class even when the caller passes a
# non-default ref).
# 2. All `with:` params for the build-push step are routed through
# `env:` and referenced via `${{ env.X }}`. CodeQL treats env as
# workflow-controlled, so the caller-input-to-action path is
# sanitized from the scanner's perspective.

name: docker-publish

on:
workflow_call:
inputs:
image:
description: Image name (without registry), e.g. "resq-software/mcp".
type: string
required: true
registry:
type: string
required: false
default: "ghcr.io"
context:
type: string
required: false
default: "."
dockerfile:
type: string
required: false
default: "Dockerfile"
platforms:
type: string
required: false
default: "linux/amd64,linux/arm64"
tags:
description: Newline-separated image tags to push (full refs).
type: string
required: true
build-args:
type: string
required: false
default: ""
push:
type: boolean
required: false
default: true
attest:
description: Emit SLSA build-provenance attestation (only runs when push is true).
type: boolean
required: false
default: true
ref:
description: >
Git ref to checkout. Caller's responsibility to pass a
trusted ref (tag or branch). Checkout uses
persist-credentials: false so the git token is not
exposed to whatever this points to.
type: string
required: false
default: ""
outputs:
image-digest:
description: >
sha256 digest of the pushed image manifest. Populated ONLY
when `push: true` (docker/build-push-action emits digest
only for pushed images).
value: ${{ jobs.publish.outputs.digest }}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

permissions:
contents: read

jobs:
publish:
name: Build, push, attest
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
packages: write
id-token: write
attestations: write
outputs:
digest: ${{ steps.build.outputs.digest }}
env:
BUILD_CONTEXT: ${{ inputs.context }}
BUILD_DOCKERFILE: ${{ inputs.dockerfile }}
BUILD_PLATFORMS: ${{ inputs.platforms }}
BUILD_TAGS: ${{ inputs.tags }}
BUILD_ARGS: ${{ inputs.build-args }}
REGISTRY: ${{ inputs.registry }}
IMAGE: ${{ inputs.image }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2
with:
egress-policy: audit

# persist-credentials: false keeps the git token out of the
# working tree so checked-out code cannot use it to push. Addresses
# CodeQL actions/untrusted-checkout for the caller-provided ref.
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.ref || github.ref }}
persist-credentials: false

- uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3

Check warning

Code scanning / CodeQL

Checkout of untrusted code in a non-privileged context Medium

Potential unsafe checkout of untrusted pull request on non-privileged workflow.
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3

- if: ${{ inputs.push }}
name: Log in to registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
Comment on lines +182 to +188

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Login credentials are hardcoded to the GHCR flow but registry is a configurable input.

registry defaults to ghcr.io but is a caller-controlled input, while the login step always uses github.actor + secrets.GITHUB_TOKEN. Those credentials only authenticate against GHCR, so a caller that sets registry: docker.io (or an ECR/ACR URL) will fail at this step with no clear signal that this workflow is GHCR-only.

Either (a) document this workflow as GHCR-only and validate inputs.registry ends with ghcr.io, or (b) expose caller secrets via on.workflow_call.secrets (e.g., registry-username, registry-password) and fall back to the GHCR defaults when unset.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/docker-publish.yml around lines 132 - 138, The login step
hardcodes GHCR credentials (github.actor + secrets.GITHUB_TOKEN) while registry
is configurable, causing failures for non-ghcr registries; update the workflow
to accept caller-provided registry credentials (e.g., define
on.workflow_call.secrets like registry-username and registry-password or inputs
for them) and in the docker/login-action step use those secrets when present,
falling back to github.actor and secrets.GITHUB_TOKEN only if
registry-username/registry-password are unset; alternatively add validation for
the inputs.registry value to ensure it endsWith "ghcr.io" and fail early if
not—look for the "registry" input, the docker/login-action step, and uses of
"github.actor" / "secrets.GITHUB_TOKEN" to implement this conditional credential
selection or validation.


- name: Build and push
id: build
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: ${{ env.BUILD_CONTEXT }}

Check warning

Code scanning / CodeQL

Code injection Medium

Potential code injection in
${ env.BUILD_CONTEXT }
, which may be controlled by an external user.
Potential code injection in
${ env.BUILD_CONTEXT }
, which may be controlled by an external user.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
file: ${{ env.BUILD_DOCKERFILE }}
platforms: ${{ env.BUILD_PLATFORMS }}
tags: ${{ env.BUILD_TAGS }}
build-args: ${{ env.BUILD_ARGS }}
push: ${{ inputs.push }}
provenance: mode=max
sbom: true

- if: ${{ inputs.attest && inputs.push }}
name: Attest build provenance
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
131 changes: 131 additions & 0 deletions .github/workflows/dotnet-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Copyright 2026 ResQ Software
# SPDX-License-Identifier: Apache-2.0
#
# Reusable .NET CI — restore, build, test, format check. Callable from
# any resq-software/* repo with a .sln or csproj tree. Pairs with
# security-scan.yml.
#
# Security: caller-controlled string inputs are forwarded through env:
# and expanded by bash directly (not via `sh -c`, which would re-parse
# and reintroduce the injection path).

name: dotnet-ci

on:
workflow_call:
inputs:
dotnet-version:
type: string
required: false
default: "9.0.x"
solution:
description: Path to .sln or .csproj (empty = dotnet auto-detect).
type: string
required: false
default: ""
working-directory:
type: string
required: false
default: "."
configuration:
type: string
required: false
default: "Release"
run-format-check:
type: boolean
required: false
default: true
run-test:
type: boolean
required: false
default: true
timeout-minutes:
type: number
required: false
default: 20

permissions:
contents: read

jobs:
build:
name: Build
runs-on: ubuntu-latest
timeout-minutes: ${{ inputs.timeout-minutes }}
defaults:
run:
working-directory: ${{ inputs.working-directory }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2
with:
egress-policy: audit
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4
with:
dotnet-version: ${{ inputs.dotnet-version }}
- name: Restore
env:
SLN: ${{ inputs.solution }}
run: |
# $SLN may be empty (auto-detect) or a path — unquoted
# expansion handles both without re-parsing.
# shellcheck disable=SC2086
dotnet restore $SLN
- name: Build
env:
SLN: ${{ inputs.solution }}
CONFIG: ${{ inputs.configuration }}
run: |
# shellcheck disable=SC2086
dotnet build $SLN -c "$CONFIG" --no-restore

format:
if: ${{ inputs.run-format-check }}
name: Format check
runs-on: ubuntu-latest
timeout-minutes: 10
defaults:
run:
working-directory: ${{ inputs.working-directory }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2
with:
egress-policy: audit
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4
with:
dotnet-version: ${{ inputs.dotnet-version }}
- name: dotnet format
env:
SLN: ${{ inputs.solution }}
run: |
# shellcheck disable=SC2086
dotnet format $SLN --verify-no-changes --severity warn

test:
if: ${{ inputs.run-test }}
name: Test
needs: build
runs-on: ubuntu-latest
timeout-minutes: ${{ inputs.timeout-minutes }}
defaults:
run:
working-directory: ${{ inputs.working-directory }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2
with:
egress-policy: audit
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4
with:
dotnet-version: ${{ inputs.dotnet-version }}
- name: dotnet test
env:
SLN: ${{ inputs.solution }}
CONFIG: ${{ inputs.configuration }}
run: |
# shellcheck disable=SC2086
dotnet test $SLN -c "$CONFIG" --verbosity normal
Loading
Loading