name: Security Testing and Signing
on:
workflow_run:
workflows: ["Release"]
types:
- completed
schedule:
- cron: '0 2 * * *'
workflow_dispatch:
inputs:
tag:
description: 'Release tag to sign (e.g., v0.4.0)'
required: false
type: string
permissions:
contents: read
jobs:
sign-release:
name: Sign Release Artifacts
runs-on: ubuntu-latest
if: |
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.tag != '')
permissions:
contents: write id-token: write timeout-minutes: 15
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
- name: Install cosign
uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22
- name: Determine release tag
id: tag
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "tag=${{ github.event.inputs.tag }}" >> "$GITHUB_OUTPUT"
else
# Derive tag from the triggering workflow_run event.
# head_branch is a version tag on tag pushes, but a branch
# name on PR runs — skip if it doesn't look like a version.
TAG="${{ github.event.workflow_run.head_branch }}"
if [ -z "$TAG" ]; then
echo "Error: Unable to determine release tag from workflow_run event." >&2
exit 1
fi
if ! echo "$TAG" | grep -qE '^v?[0-9]+\.[0-9]+\.[0-9]+'; then
echo "Skipping: head_branch '$TAG' is not a version tag"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
fi
env:
GH_TOKEN: ${{ github.token }}
- name: Download release artifacts
if: steps.tag.outputs.skip != 'true'
run: |
TAG="${{ steps.tag.outputs.tag }}"
echo "Downloading artifacts for release $TAG"
gh release download "$TAG" --pattern 'thoughtjack-*' --dir ./artifacts
env:
GH_TOKEN: ${{ github.token }}
- name: Sign artifacts with Sigstore
if: steps.tag.outputs.skip != 'true'
run: |
cd ./artifacts
for artifact in thoughtjack-*; do
# Skip if already a signature file
if [[ "$artifact" == *.sig ]] || [[ "$artifact" == *.pem ]]; then
continue
fi
echo "Signing $artifact"
# Single signing operation producing bundle, signature, and certificate
cosign sign-blob \
--yes \
--bundle "${artifact}.bundle" \
--output-signature "${artifact}.sig" \
--output-certificate "${artifact}.pem" \
"$artifact"
done
- name: Upload signatures to release
if: steps.tag.outputs.skip != 'true'
run: |
TAG="${{ steps.tag.outputs.tag }}"
cd ./artifacts
echo "Uploading signatures for release $TAG"
for sig_file in *.sig *.pem *.bundle; do
if [ -f "$sig_file" ]; then
echo "Uploading $sig_file"
gh release upload "$TAG" "$sig_file" --clobber
fi
done
env:
GH_TOKEN: ${{ github.token }}
- name: Verify signatures
if: steps.tag.outputs.skip != 'true'
run: |
cd ./artifacts
for artifact in thoughtjack-*; do
# Skip signature files themselves
if [[ "$artifact" == *.sig ]] || [[ "$artifact" == *.pem ]] || [[ "$artifact" == *.bundle ]]; then
continue
fi
echo "Verifying signature for $artifact"
cosign verify-blob \
--certificate "${artifact}.pem" \
--signature "${artifact}.sig" \
--certificate-identity-regexp="^https://github.com/${{ github.repository }}" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
"$artifact"
done
fuzz:
name: Fuzz Testing
runs-on: ubuntu-latest
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
timeout-minutes: 300 permissions:
contents: read
issues: write
strategy:
fail-fast: false
matrix:
target:
- fuzz_jsonrpc_parser
- fuzz_oatf_loader
- fuzz_mcp_handler
- fuzz_synthesize_validation
- fuzz_uri_template
- fuzz_mcp_response_dispatch
- fuzz_a2a_sse_parser
- fuzz_agui_sse_parser
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
- name: Install Rust nightly
uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 with:
toolchain: nightly
- name: Install cargo-fuzz
run: cargo install cargo-fuzz
- name: Cache fuzz corpus
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with:
path: fuzz/corpus/${{ matrix.target }}
key: fuzz-corpus-${{ matrix.target }}
restore-keys: |
fuzz-corpus-${{ matrix.target }}
- name: Run fuzzer (1 hour per target)
run: |
cd fuzz
cargo +nightly fuzz run ${{ matrix.target }} -- \
-max_total_time=3600 \
-print_final_stats=1 \
-timeout=30
continue-on-error: true
- name: Check for crashes
id: crashes
run: |
if [ -d "fuzz/artifacts/${{ matrix.target }}" ] && [ -n "$(ls -A fuzz/artifacts/${{ matrix.target }})" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
echo "::warning::Fuzzer found crashes for ${{ matrix.target }}"
else
echo "found=false" >> "$GITHUB_OUTPUT"
fi
- name: Upload crash artifacts
if: steps.crashes.outputs.found == 'true'
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f with:
name: fuzz-crashes-${{ matrix.target }}
path: fuzz/artifacts/${{ matrix.target }}/
retention-days: 30
- name: Create issue on crash
if: steps.crashes.outputs.found == 'true'
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea with:
script: |
const fs = require('fs');
const target = '${{ matrix.target }}';
const artifactsDir = `fuzz/artifacts/${target}`;
// List crash files
const crashes = fs.readdirSync(artifactsDir);
const crashList = crashes.map(f => `- \`${f}\``).join('\n');
// Create issue
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `🐛 Fuzzing crash found in ${target}`,
labels: ['bug', 'security', 'fuzzing'],
body: `## Fuzzing Crash Report
**Target**: \`${target}\`
**Workflow Run**: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
### Crashes Found
${crashList}
### Reproduction
Download the crash artifacts and reproduce locally:
\`\`\`bash
# Download artifacts from workflow run
gh run download ${{ github.run_id }} -n fuzz-crashes-${target}
# Reproduce the crash
cd fuzz
cargo +nightly fuzz run ${target} <path-to-crash-file>
\`\`\`
### Next Steps
1. Analyze the crash with \`cargo fuzz fmt ${target} <crash-file>\`
2. Add regression test
3. Fix the underlying issue
4. Re-run fuzzer to verify fix
`
});