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
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"
209 changes: 209 additions & 0 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
# 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. The caller-supplied ref is checked out with
# `persist-credentials: false` so the git token is isolated from
# the fetched code.
# 2. All caller-controlled string inputs (image, registry, context,
# dockerfile, platforms, tags, build-args) are validated against
# allowlist regexes in a dedicated `Validate caller inputs` step,
# then re-exported to `$GITHUB_ENV` with a `SAFE_` prefix.
# Downstream steps reference `${{ env.SAFE_X }}` only — CodeQL
# treats SAFE_* as sanitized after explicit validation.
# 3. The caller-controlled `ref` input is the documented trust
# boundary; the actions/untrusted-checkout/medium alert is
# dismissed with rationale because this is a reusable-workflow
# model where the caller is a sibling resq-software/* repo.

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; persist-credentials:false is set on checkout
so the git token is not exposed.
type: string
required: false
default: ""
outputs:
image-digest:
description: >
sha256 digest of the pushed image manifest. Populated ONLY
when `push: true`.
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 }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2
with:
egress-policy: audit

- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.ref || github.ref }}
persist-credentials: false

- name: Validate caller inputs
env:
IN_IMAGE: ${{ inputs.image }}
IN_REGISTRY: ${{ inputs.registry }}
IN_CONTEXT: ${{ inputs.context }}
IN_DOCKERFILE: ${{ inputs.dockerfile }}
IN_PLATFORMS: ${{ inputs.platforms }}
IN_TAGS: ${{ inputs.tags }}
IN_BUILD_ARGS: ${{ inputs.build-args }}
shell: bash
run: |
# shellcheck disable=SC2016,SC2129
set -euo pipefail

fail() { echo "::error::$*" >&2; exit 1; }
match() { [[ "$2" =~ $3 ]] || fail "Invalid $1: '$2' does not match $3"; }

# image: namespace/name
match image "$IN_IMAGE" '^[a-zA-Z0-9][a-zA-Z0-9._/-]*$'
# registry: hostname with optional :port
match registry "$IN_REGISTRY" '^[a-zA-Z0-9][a-zA-Z0-9.:-]*$'

# context: local path or known remote URL form; no ..
if ! [[ "$IN_CONTEXT" =~ ^(\.|[a-zA-Z0-9._][a-zA-Z0-9._/-]*|https?://[a-zA-Z0-9._:/?#=\&%-]+|git://[a-zA-Z0-9._:/?#=\&%-]+|github\.com/[a-zA-Z0-9._/-]+)$ ]]; then
fail "Invalid context: '$IN_CONTEXT'"
fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.
case "$IN_CONTEXT" in *..*) fail "context may not contain '..'" ;; esac

# dockerfile: relative path, no traversal
match dockerfile "$IN_DOCKERFILE" '^[a-zA-Z0-9._][a-zA-Z0-9._/-]*$'
case "$IN_DOCKERFILE" in *..*) fail "dockerfile may not contain '..'" ;; esac

# platforms: comma-separated lowercase os/arch tokens
match platforms "$IN_PLATFORMS" '^[a-z0-9]+/[a-z0-9_-]+(,[a-z0-9]+/[a-z0-9_-]+)*$'

# tags: newline-separated image refs
while IFS= read -r tag; do
[ -z "$tag" ] && continue
match tag "$tag" '^[a-zA-Z0-9][a-zA-Z0-9._:/@-]*$'
done <<< "$IN_TAGS"

# build-args: KEY=VALUE per line; no backticks / $(; reject smuggling
while IFS= read -r arg; do
[ -z "$arg" ] && continue
[[ "$arg" =~ ^[A-Za-z_][A-Za-z0-9_]*= ]] || fail "build-arg must be KEY=VALUE: '$arg'"
case "$arg" in
*'`'*) fail "build-arg may not contain backtick: '$arg'" ;;
*'$('*) fail "build-arg may not contain command substitution: '$arg'" ;;
esac
done <<< "$IN_BUILD_ARGS"

# Re-export sanitized values — CodeQL treats SAFE_* as workflow-controlled.
{
echo "SAFE_IMAGE=$IN_IMAGE"
echo "SAFE_REGISTRY=$IN_REGISTRY"
echo "SAFE_CONTEXT=$IN_CONTEXT"
echo "SAFE_DOCKERFILE=$IN_DOCKERFILE"
echo "SAFE_PLATFORMS=$IN_PLATFORMS"
} >> "$GITHUB_ENV"

{
echo "SAFE_TAGS<<__EOF__"
printf '%s\n' "$IN_TAGS"
echo "__EOF__"
} >> "$GITHUB_ENV"

{
echo "SAFE_BUILD_ARGS<<__EOF__"
printf '%s\n' "$IN_BUILD_ARGS"
echo "__EOF__"
} >> "$GITHUB_ENV"
Comment thread
WomB0ComB0 marked this conversation as resolved.
Dismissed

- uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3
- 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.SAFE_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.SAFE_CONTEXT }}
Comment thread
WomB0ComB0 marked this conversation as resolved.
Dismissed
file: ${{ env.SAFE_DOCKERFILE }}
platforms: ${{ env.SAFE_PLATFORMS }}
tags: ${{ env.SAFE_TAGS }}
build-args: ${{ env.SAFE_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.SAFE_REGISTRY }}/${{ env.SAFE_IMAGE }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
Loading
Loading