Files
awesome-copilot/.github/workflows/validate-canvas-extensions.yml
T
Aaron Powell 79cda6bb19 Add canvas schema validation to extension submission workflow (#2161)
* Add canvas schema and extension submission checks

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix: use namespace import for js-yaml

Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com>

* Fix contributors page build markup

Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com>

* Address PR feedback on canvas schema validation

- Add ajv-cli@5 as a pinned devDependency; install via npm ci in CI instead of npx --yes
- Fix screenshot path regex to prevent .. traversal segments
- Validate canvas.schema.json is parseable JSON even on schema-only PRs

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Harden canvas extension workflow against injection attacks

Switch from newline to null-terminated git diff output (git diff -z) so filenames
containing newlines are read atomically, matching the existing skill-check.yml pattern.

Add an allowlist regex guard on the extracted extension directory name immediately after
it is parsed from git diff output. Any name not matching ^[a-z0-9][a-z0-9-]*$ (e.g.
names containing dollar signs, parentheses, spaces, or other shell metacharacters) is
silently skipped before being used anywhere in the script.

Add a matching allowlist guard on each screenshot path extracted from canvas.json before
the file-existence check, so a crafted manifest cannot supply a path with shell
metacharacters or traversal segments even after the schema check passes.

Follows the same defence-in-depth pattern introduced after the injection PoCs in #1236
and #1240.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Replace ajv-cli with in-repo schema validator

- Remove ajv-cli to avoid vulnerable/deprecated transitive dependencies
- Add eng/validate-json-schema.mjs using ajv + ajv-formats
- Update validate-canvas-extensions workflow to use local script
- Use npm ci --ignore-scripts in PR validation job

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-01 10:40:40 +10:00

138 lines
5.4 KiB
YAML

name: Validate Canvas Extensions
on:
pull_request:
branches: [main]
types: [opened, synchronize, reopened]
paths:
- "extensions/**"
- ".schemas/canvas.schema.json"
permissions:
contents: read
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "22"
cache: "npm"
- name: Install dependencies
run: npm ci --ignore-scripts
- name: Validate changed canvas extensions
run: |
set -euo pipefail
# Validate schema structure once, even for schema-only PRs.
if ! node ./eng/validate-json-schema.mjs --schema .schemas/canvas.schema.json; then
echo "❌ .schemas/canvas.schema.json failed schema compilation"
exit 1
fi
# Collect changed extension directories.
# Use null-terminated (-z) output from git diff so filenames containing newlines
# or other special characters are read atomically (matches the pattern in skill-check.yml).
# Each extracted name is then validated against a strict allowlist regex before use,
# rejecting anything containing shell metacharacters ($, (, ), spaces, etc.).
declare -A seen_dirs=()
changed_extensions=()
errors=()
while IFS= read -r -d '' file; do
case "$file" in
extensions/*)
ext_name="${file#extensions/}"
ext_name="${ext_name%%/*}"
if [ "$ext_name" = "external-assets" ]; then
continue
fi
# Allowlist: extension directory names must be lowercase alphanumeric + hyphens only.
# Fail for disallowed names so a PR cannot bypass validation by using a nonconforming folder.
if [[ ! "$ext_name" =~ ^[a-z0-9][a-z0-9-]*$ ]]; then
errors+=("\`extensions/$ext_name\`: invalid extension directory name (must match ^[a-z0-9][a-z0-9-]*$).")
continue
fi
if [ -z "${seen_dirs[$ext_name]+x}" ]; then
seen_dirs["$ext_name"]=1
changed_extensions+=("extensions/$ext_name")
fi
;;
esac
done < <(git diff --name-only -z "origin/${{ github.base_ref }}...HEAD")
if [ "${#changed_extensions[@]}" -eq 0 ]; then
if [ "${#errors[@]}" -ne 0 ]; then
echo "❌ Canvas extension validation failed:"
for error in "${errors[@]}"; do
echo "- $error"
done
exit 1
fi
echo "No canvas extension directories changed — skipping validation."
exit 0
fi
echo "Validating ${#changed_extensions[@]} extension(s): ${changed_extensions[*]}"
for ext_dir in "${changed_extensions[@]}"; do
if [ ! -d "$ext_dir" ]; then
echo "$ext_dir no longer exists (deleted?), skipping."
continue
fi
if [ ! -f "$ext_dir/extension.mjs" ]; then
errors+=("\`$ext_dir\`: missing required \`extension.mjs\`.")
fi
if [ ! -f "$ext_dir/canvas.json" ]; then
errors+=("\`$ext_dir\`: missing required \`canvas.json\`.")
continue
fi
if [ ! -f "$ext_dir/assets/preview.png" ]; then
errors+=("\`$ext_dir\`: missing required \`assets/preview.png\`.")
fi
if ! schema_output="$(node ./eng/validate-json-schema.mjs --schema .schemas/canvas.schema.json --data "$ext_dir/canvas.json" 2>&1)"; then
condensed_output="$(echo "$schema_output" | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g')"
errors+=("\`$ext_dir/canvas.json\`: schema validation failed against \`.schemas/canvas.schema.json\` ($condensed_output).")
continue
fi
mapfile -t screenshot_paths < <(
node -e 'const fs = require("fs"); const manifest = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const paths = [manifest?.screenshots?.icon?.path, manifest?.screenshots?.gallery?.path].filter((value) => typeof value === "string" && value.trim().length > 0); for (const value of [...new Set(paths)]) { console.log(value); }' "$ext_dir/canvas.json"
)
for screenshot_path in "${screenshot_paths[@]}"; do
if [[ ! "$screenshot_path" =~ ^assets/([A-Za-z0-9_-]+/)*[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*\.(png|jpe?g|gif|webp|svg)$ ]]; then
errors+=("\`$ext_dir/canvas.json\`: screenshot path \`$screenshot_path\` is not a valid assets path.")
continue
fi
if [ ! -f "$ext_dir/$screenshot_path" ]; then
errors+=("\`$ext_dir/canvas.json\`: screenshot path \`$screenshot_path\` does not exist in the extension directory.")
fi
done
done
if [ "${#errors[@]}" -ne 0 ]; then
echo "❌ Canvas extension validation failed:"
for error in "${errors[@]}"; do
echo "- $error"
done
exit 1
fi
echo "✅ All changed canvas extensions passed validation."