mirror of
https://github.com/github/awesome-copilot.git
synced 2026-07-14 18:11:01 +00:00
79cda6bb19
* 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>
138 lines
5.4 KiB
YAML
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." |