From d7fde9db1f68a8ea81009655af73c5761c81cc90 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Thu, 11 Jun 2026 14:37:49 +1000 Subject: [PATCH 1/9] Add soft-gate PR risk scanning automation Introduce a PR risk scanner script plus two workflows: one to scan changed files and upload findings, and one to upsert a sticky PR comment with a summary table and findings. This adds non-blocking supply-chain risk visibility for agentic contributions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/pr-risk-scan-comment.yml | 86 ++++++ .github/workflows/pr-risk-scan.yml | 51 ++++ eng/pr-risk-scan.mjs | 311 +++++++++++++++++++++ 3 files changed, 448 insertions(+) create mode 100644 .github/workflows/pr-risk-scan-comment.yml create mode 100644 .github/workflows/pr-risk-scan.yml create mode 100644 eng/pr-risk-scan.mjs diff --git a/.github/workflows/pr-risk-scan-comment.yml b/.github/workflows/pr-risk-scan-comment.yml new file mode 100644 index 000000000..dd815871d --- /dev/null +++ b/.github/workflows/pr-risk-scan-comment.yml @@ -0,0 +1,86 @@ +name: PR Risk Scan — Comment + +on: + workflow_run: + workflows: ["PR Risk Scan — Gate"] + types: [completed] + +permissions: + issues: write + pull-requests: write + actions: read + +jobs: + comment: + runs-on: ubuntu-latest + if: github.event.workflow_run.event == 'pull_request' + steps: + - name: Download scan artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: pr-risk-scan-results + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + + - name: Upsert PR comment + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + with: + script: | + const fs = require('fs'); + const marker = ''; + const reportPath = 'report.md'; + const prNumberPath = 'pr-number.txt'; + + if (!fs.existsSync(reportPath)) { + core.setFailed('Risk scan report.md artifact was not found.'); + return; + } + + const body = fs.readFileSync(reportPath, 'utf8'); + let prNumber = null; + + if (fs.existsSync(prNumberPath)) { + const parsed = parseInt(fs.readFileSync(prNumberPath, 'utf8').trim(), 10); + if (!Number.isNaN(parsed)) { + prNumber = parsed; + } + } + + if (!prNumber) { + const fallback = context.payload.workflow_run.pull_requests?.[0]?.number; + if (fallback) { + prNumber = fallback; + } + } + + if (!prNumber) { + core.setFailed('Could not determine PR number for comment upsert.'); + return; + } + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + per_page: 100, + }); + + const existing = comments.find((comment) => comment.body.includes(marker)); + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + console.log(`Updated existing risk scan comment ${existing.id}`); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body, + }); + console.log('Created new risk scan comment'); + } diff --git a/.github/workflows/pr-risk-scan.yml b/.github/workflows/pr-risk-scan.yml new file mode 100644 index 000000000..2dc22b412 --- /dev/null +++ b/.github/workflows/pr-risk-scan.yml @@ -0,0 +1,51 @@ +name: PR Risk Scan — Gate + +on: + pull_request: + branches: [staged] + types: [opened, synchronize, reopened] + paths: + - "skills/**" + - "agents/**" + - "workflows/**" + - "plugins/**" + - "hooks/**" + - "instructions/**" + +permissions: + contents: read + actions: read + +jobs: + scan: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + fetch-depth: 0 + + - name: Collect changed files + run: | + git diff --name-only --diff-filter=ACMR "origin/${{ github.base_ref }}...HEAD" > changed-files.txt + echo "Changed files:" + cat changed-files.txt || true + + - name: Run PR risk scanner + run: | + mkdir -p pr-risk-results + node ./eng/pr-risk-scan.mjs \ + --files changed-files.txt \ + --output-json pr-risk-results/results.json \ + --output-md pr-risk-results/report.md + + - name: Save metadata + run: | + echo "${{ github.event.pull_request.number }}" > pr-risk-results/pr-number.txt + + - name: Upload scan artifact + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: pr-risk-scan-results + path: pr-risk-results/ + retention-days: 1 diff --git a/eng/pr-risk-scan.mjs b/eng/pr-risk-scan.mjs new file mode 100644 index 000000000..573dcbd46 --- /dev/null +++ b/eng/pr-risk-scan.mjs @@ -0,0 +1,311 @@ +#!/usr/bin/env node + +import fs from "fs"; +import path from "path"; + +const SCRIPT_EXTENSIONS = new Set([ + ".sh", + ".bash", + ".ps1", + ".py", + ".js", + ".mjs", + ".ts", +]); + +const severityLevels = { + high: "high", + medium: "medium", + info: "info", +}; + +const LINE_RULES = [ + { + rule_id: "guardrail-bypass-language", + severity: severityLevels.high, + regex: + /\b(ignore (all|any|previous) (guardrails?|rules?|instructions?)|bypass (the )?(guardrails?|safety|policy)|disable (safety|guardrails?)|do not ask (for )?(confirmation|consent)|without prompting (the )?user)\b/i, + reason: "Language suggests bypassing policy or confirmation controls.", + suggested_fix: + "Require explicit policy adherence and user-confirmation steps for risky actions.", + }, + { + rule_id: "remote-shell-execution", + severity: severityLevels.high, + regex: /\b(curl|wget)\b[^\n|]*\|\s*(sh|bash|zsh|pwsh|powershell)\b/i, + reason: "Piping remote content directly to a shell is high-risk.", + suggested_fix: + "Download, verify integrity/signature, and run from a reviewed local file.", + }, + { + rule_id: "autoyes-package-exec", + severity: severityLevels.high, + regex: + /\b(npx|npm\s+exec|pnpm\s+dlx|uvx|pipx\s+run)\b[^\n]*\s(-y|--yes)\b/i, + reason: + "Auto-yes execution can bypass human review of package/runtime prompts.", + suggested_fix: + "Remove automatic consent flags and require explicit reviewer-approved invocation.", + }, + { + rule_id: "package-exec-command", + severity: severityLevels.medium, + regex: /\b(npx|npm\s+exec|pnpm\s+dlx|uvx|pipx\s+run|uv\s+tool\s+run)\b/i, + reason: "Dynamic package/runtime execution introduces supply-chain risk.", + suggested_fix: + "Pin exact versions and document manual confirmation controls.", + }, + { + rule_id: "unpinned-version-indicator", + severity: severityLevels.medium, + regex: /\B@latest\b|\blatest\b|\*|(\^|~)\d+/i, + reason: "Unpinned dependencies can change behavior between runs.", + suggested_fix: "Use exact immutable versions or commit hashes.", + shouldApply: (line) => + /\b(npm|pnpm|yarn|npx|uvx|pip|pipx|cargo|go)\b/i.test(line), + }, +]; + +function parseArgs(argv) { + const args = {}; + for (let i = 0; i < argv.length; i += 1) { + const key = argv[i]; + if (!key.startsWith("--")) { + continue; + } + + args[key.slice(2)] = argv[i + 1]; + i += 1; + } + return args; +} + +function ensureParentDir(filePath) { + const directory = path.dirname(filePath); + fs.mkdirSync(directory, { recursive: true }); +} + +function normalizeRelativePath(value) { + const cleaned = String(value || "") + .trim() + .replace(/\\/g, "/") + .replace(/^\.\/+/, ""); + if (!cleaned) { + return ""; + } + + if (cleaned.includes("..")) { + throw new Error(`Unsafe relative path in changed files list: ${value}`); + } + + return cleaned; +} + +function isPotentialText(contentBuffer) { + const nullByte = contentBuffer.includes(0x00); + return !nullByte; +} + +function addFinding(findings, finding) { + findings.push({ + rule_id: finding.rule_id, + severity: finding.severity, + file: finding.file, + line: finding.line, + match: finding.match.slice(0, 180), + reason: finding.reason, + suggested_fix: finding.suggested_fix, + }); +} + +function scanLineRules(filePath, content, findings) { + const lines = content.split(/\r?\n/); + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index]; + for (const rule of LINE_RULES) { + if (typeof rule.shouldApply === "function" && !rule.shouldApply(line)) { + continue; + } + + const match = line.match(rule.regex); + if (!match) { + continue; + } + + addFinding(findings, { + rule_id: rule.rule_id, + severity: rule.severity, + file: filePath, + line: index + 1, + match: line.trim(), + reason: rule.reason, + suggested_fix: rule.suggested_fix, + }); + } + } +} + +function scanSkillScriptPath(filePath, findings) { + const normalized = filePath.replace(/\\/g, "/"); + if (!normalized.startsWith("skills/")) { + return; + } + + const extension = path.extname(normalized).toLowerCase(); + if (!SCRIPT_EXTENSIONS.has(extension)) { + return; + } + + addFinding(findings, { + rule_id: "skill-script-added", + severity: severityLevels.info, + file: normalized, + line: 1, + match: normalized, + reason: + "Script asset under a skill may require external runtime/dependencies.", + suggested_fix: + "Document dependencies, pin versions, and avoid implicit network installs.", + }); +} + +function severityCounts(findings) { + return findings.reduce( + (acc, finding) => { + acc[finding.severity] = (acc[finding.severity] || 0) + 1; + return acc; + }, + { high: 0, medium: 0, info: 0 } + ); +} + +function toMarkdownReport(findings, scannedFiles, skippedFiles) { + const marker = ""; + const counts = severityCounts(findings); + const summary = [ + marker, + "## 🔒 PR Risk Scan Results", + "", + `Scanned **${scannedFiles.length}** changed file(s).`, + "", + "| Severity | Count |", + "|---|---:|", + `| 🔴 High | ${counts.high} |`, + `| 🟠 Medium | ${counts.medium} |`, + `| â„šī¸ Info | ${counts.info} |`, + "", + ]; + + if (findings.length === 0) { + summary.push( + "✅ No matching risk patterns were detected in changed files." + ); + } else { + summary.push("| Severity | Rule | File | Line | Match |"); + summary.push("|---|---|---|---:|---|"); + for (const finding of findings.slice(0, 100)) { + const severity = + finding.severity === severityLevels.high + ? "🔴" + : finding.severity === severityLevels.medium + ? "🟠" + : "â„šī¸"; + const match = finding.match.replace(/\|/g, "\\|"); + summary.push( + `| ${severity} | \`${finding.rule_id}\` | \`${finding.file}\` | ${finding.line} | ${match} |` + ); + } + + if (findings.length > 100) { + summary.push( + "", + `_${findings.length - 100} additional finding(s) omitted from table._` + ); + } + } + + if (skippedFiles.length > 0) { + summary.push( + "", + "
", + "Skipped non-text or missing files", + "" + ); + summary.push(skippedFiles.map((filePath) => `- ${filePath}`).join("\n")); + summary.push("", "
"); + } + + summary.push( + "", + "> This is an automated soft-gate report. Findings indicate review targets and do not block merge by themselves." + ); + + return `${summary.join("\n")}\n`; +} + +function main() { + const args = parseArgs(process.argv.slice(2)); + if (!args.files || !args["output-json"] || !args["output-md"]) { + throw new Error( + "Usage: node ./eng/pr-risk-scan.mjs --files --output-json --output-md " + ); + } + + const changedFilesPath = path.resolve(args.files); + const outputJsonPath = path.resolve(args["output-json"]); + const outputMarkdownPath = path.resolve(args["output-md"]); + + const changedFiles = fs + .readFileSync(changedFilesPath, "utf8") + .split(/\r?\n/) + .map(normalizeRelativePath) + .filter(Boolean); + + const findings = []; + const scannedFiles = []; + const skippedFiles = []; + + for (const relativePath of changedFiles) { + const absolutePath = path.resolve(relativePath); + scanSkillScriptPath(relativePath, findings); + + if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile()) { + skippedFiles.push(relativePath); + continue; + } + + const contentBuffer = fs.readFileSync(absolutePath); + if (!isPotentialText(contentBuffer)) { + skippedFiles.push(relativePath); + continue; + } + + const content = contentBuffer.toString("utf8"); + scanLineRules(relativePath, content, findings); + scannedFiles.push(relativePath); + } + + const results = { + generated_at: new Date().toISOString(), + scanned_files: scannedFiles, + skipped_files: skippedFiles, + finding_count: findings.length, + severity_counts: severityCounts(findings), + findings, + }; + + ensureParentDir(outputJsonPath); + ensureParentDir(outputMarkdownPath); + fs.writeFileSync(outputJsonPath, `${JSON.stringify(results, null, 2)}\n`); + fs.writeFileSync( + outputMarkdownPath, + toMarkdownReport(findings, scannedFiles, skippedFiles) + ); +} + +try { + main(); +} catch (error) { + console.error(error.message); + process.exit(1); +} From d97d8de7ac9a0dd08abe37cabbf6ded1a40ba705 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Thu, 11 Jun 2026 15:51:50 +1000 Subject: [PATCH 2/9] Harden path checks and reduce scanner false positives Reject absolute paths, enforce repo-root containment after resolution, and tighten unpinned-version detection to dependency/version contexts to avoid markdown noise. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pr-risk-scan.mjs | 77 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 71 insertions(+), 6 deletions(-) diff --git a/eng/pr-risk-scan.mjs b/eng/pr-risk-scan.mjs index 573dcbd46..3acce983b 100644 --- a/eng/pr-risk-scan.mjs +++ b/eng/pr-risk-scan.mjs @@ -13,6 +13,62 @@ const SCRIPT_EXTENSIONS = new Set([ ".ts", ]); +function isLikelyAbsolutePath(value) { + if (!value) { + return false; + } + + // POSIX absolute (/foo), UNC (//server/share), Windows drive paths (C:/foo). + return ( + value.startsWith("/") || + value.startsWith("//") || + /^[A-Za-z]:\//.test(value) + ); +} + +function isPathWithinRoot(rootPath, targetPath) { + const relative = path.relative(rootPath, targetPath); + return ( + relative === "" || + (!relative.startsWith("..") && !path.isAbsolute(relative)) + ); +} + +function hasUnpinnedVersionIndicator(line) { + const trimmed = line.trim(); + + if (!trimmed) { + return false; + } + + // Command contexts where floating versions are risky. + if ( + /\b(npm|pnpm|yarn|bun|npx|uvx|pip|pipx)\b[^\n]*(?:@latest\b|\blatest\b)/i.test( + trimmed + ) + ) { + return true; + } + + // package.json/yaml style dependency entries with floating ranges. + if ( + /["'][^"']+["']\s*:\s*["'](\^|~|\*|latest\b)[^"']*["']/i.test(trimmed) + ) { + return true; + } + + // pyproject/requirements style entries with broad lower-bound only specs. + if ( + /\b[A-Za-z0-9_.-]+\s*(>=|>|~=)\s*\d+(?:\.\d+){0,2}\b(?!\s*,\s*<)/.test( + trimmed + ) + ) { + return true; + } + + return false; +} + const severityLevels = { high: "high", medium: "medium", @@ -58,11 +114,9 @@ const LINE_RULES = [ { rule_id: "unpinned-version-indicator", severity: severityLevels.medium, - regex: /\B@latest\b|\blatest\b|\*|(\^|~)\d+/i, reason: "Unpinned dependencies can change behavior between runs.", suggested_fix: "Use exact immutable versions or commit hashes.", - shouldApply: (line) => - /\b(npm|pnpm|yarn|npx|uvx|pip|pipx|cargo|go)\b/i.test(line), + matcher: (line) => hasUnpinnedVersionIndicator(line), }, ]; @@ -98,6 +152,10 @@ function normalizeRelativePath(value) { throw new Error(`Unsafe relative path in changed files list: ${value}`); } + if (isLikelyAbsolutePath(cleaned)) { + throw new Error(`Absolute paths are not allowed in changed files list: ${value}`); + } + return cleaned; } @@ -127,8 +185,10 @@ function scanLineRules(filePath, content, findings) { continue; } - const match = line.match(rule.regex); - if (!match) { + const matchedByRegex = rule.regex ? rule.regex.test(line) : false; + const matchedByFunction = + typeof rule.matcher === "function" ? rule.matcher(line) : false; + if (!matchedByRegex && !matchedByFunction) { continue; } @@ -254,6 +314,7 @@ function main() { const changedFilesPath = path.resolve(args.files); const outputJsonPath = path.resolve(args["output-json"]); const outputMarkdownPath = path.resolve(args["output-md"]); + const repoRootPath = process.cwd(); const changedFiles = fs .readFileSync(changedFilesPath, "utf8") @@ -266,7 +327,11 @@ function main() { const skippedFiles = []; for (const relativePath of changedFiles) { - const absolutePath = path.resolve(relativePath); + const absolutePath = path.resolve(repoRootPath, relativePath); + if (!isPathWithinRoot(repoRootPath, absolutePath)) { + throw new Error(`Path escapes repository root: ${relativePath}`); + } + scanSkillScriptPath(relativePath, findings); if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile()) { From b9ea0b7d29f9ca7190e96703f8c80222cf9eeb9a Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Thu, 11 Jun 2026 16:16:04 +1000 Subject: [PATCH 3/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- eng/pr-risk-scan.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/pr-risk-scan.mjs b/eng/pr-risk-scan.mjs index 3acce983b..5fe719242 100644 --- a/eng/pr-risk-scan.mjs +++ b/eng/pr-risk-scan.mjs @@ -148,7 +148,7 @@ function normalizeRelativePath(value) { return ""; } - if (cleaned.includes("..")) { + if (/(^|\/)\.\.(\/|$)/.test(cleaned)) { throw new Error(`Unsafe relative path in changed files list: ${value}`); } From 372b845dce71261dcb392941338a868f33316799 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Thu, 11 Jun 2026 16:17:04 +1000 Subject: [PATCH 4/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- eng/pr-risk-scan.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/pr-risk-scan.mjs b/eng/pr-risk-scan.mjs index 5fe719242..f4820e324 100644 --- a/eng/pr-risk-scan.mjs +++ b/eng/pr-risk-scan.mjs @@ -217,7 +217,7 @@ function scanSkillScriptPath(filePath, findings) { } addFinding(findings, { - rule_id: "skill-script-added", + rule_id: "skill-script-touched", severity: severityLevels.info, file: normalized, line: 1, From 1c72b5f2f2e0114653d361ffa0ffda26f5b85cfd Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Fri, 12 Jun 2026 12:02:19 +1000 Subject: [PATCH 5/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- eng/pr-risk-scan.mjs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/eng/pr-risk-scan.mjs b/eng/pr-risk-scan.mjs index f4820e324..f96f66db6 100644 --- a/eng/pr-risk-scan.mjs +++ b/eng/pr-risk-scan.mjs @@ -270,9 +270,15 @@ function toMarkdownReport(findings, scannedFiles, skippedFiles) { : finding.severity === severityLevels.medium ? "🟠" : "â„šī¸"; - const match = finding.match.replace(/\|/g, "\\|"); + const match = finding.match + .replace(/\\/g, "\\\\") + .replace(/`/g, "\\`") + .replace(//g, ">") + .replace(/\|/g, "\\|") + .replace(/@/g, "@\u200b"); summary.push( - `| ${severity} | \`${finding.rule_id}\` | \`${finding.file}\` | ${finding.line} | ${match} |` + `| ${severity} | \`${finding.rule_id}\` | \`${finding.file}\` | ${finding.line} | \`${match}\` |` ); } From 78762a53a88253950b2a0b5ba66e82095cc1780f Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Fri, 12 Jun 2026 12:02:41 +1000 Subject: [PATCH 6/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- eng/pr-risk-scan.mjs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/eng/pr-risk-scan.mjs b/eng/pr-risk-scan.mjs index f96f66db6..a1fc4ee02 100644 --- a/eng/pr-risk-scan.mjs +++ b/eng/pr-risk-scan.mjs @@ -340,11 +340,22 @@ function main() { scanSkillScriptPath(relativePath, findings); - if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile()) { + if (!fs.existsSync(absolutePath)) { skippedFiles.push(relativePath); continue; } + const stat = fs.statSync(absolutePath); + if (!stat.isFile()) { + skippedFiles.push(relativePath); + continue; + } + + if (stat.size > 1024 * 1024) { + skippedFiles.push(`${relativePath} (skipped: file too large)`); + continue; + } + const contentBuffer = fs.readFileSync(absolutePath); if (!isPotentialText(contentBuffer)) { skippedFiles.push(relativePath); From 71f771df51e0ceaf6f0f6dee88bf1a1931727a09 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Fri, 12 Jun 2026 12:20:13 +1000 Subject: [PATCH 7/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- eng/pr-risk-scan.mjs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/eng/pr-risk-scan.mjs b/eng/pr-risk-scan.mjs index a1fc4ee02..04464472c 100644 --- a/eng/pr-risk-scan.mjs +++ b/eng/pr-risk-scan.mjs @@ -270,15 +270,20 @@ function toMarkdownReport(findings, scannedFiles, skippedFiles) { : finding.severity === severityLevels.medium ? "🟠" : "â„šī¸"; - const match = finding.match + const matchText = finding.match .replace(/\\/g, "\\\\") - .replace(/`/g, "\\`") .replace(//g, ">") .replace(/\|/g, "\\|") .replace(/@/g, "@\u200b"); + const backtickRuns = matchText.match(/`+/g); + const fenceLength = backtickRuns + ? Math.max(...backtickRuns.map((run) => run.length)) + 1 + : 1; + const fence = "`".repeat(fenceLength); + const match = `${fence}${matchText}${fence}`; summary.push( - `| ${severity} | \`${finding.rule_id}\` | \`${finding.file}\` | ${finding.line} | \`${match}\` |` + `| ${severity} | \`${finding.rule_id}\` | \`${finding.file}\` | ${finding.line} | ${match} |` ); } From 4abf38ac1788b307dcda2e9a2eadcfbebe9b2943 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Fri, 12 Jun 2026 12:20:40 +1000 Subject: [PATCH 8/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- eng/pr-risk-scan.mjs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/eng/pr-risk-scan.mjs b/eng/pr-risk-scan.mjs index 04464472c..d3ca7ec03 100644 --- a/eng/pr-risk-scan.mjs +++ b/eng/pr-risk-scan.mjs @@ -350,7 +350,11 @@ function main() { continue; } - const stat = fs.statSync(absolutePath); + const stat = fs.lstatSync(absolutePath); + if (stat.isSymbolicLink()) { + skippedFiles.push(`${relativePath} (skipped: symbolic link)`); + continue; + } if (!stat.isFile()) { skippedFiles.push(relativePath); continue; From e410554bbd6042bac76aa914b265b87138824201 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Fri, 12 Jun 2026 12:21:06 +1000 Subject: [PATCH 9/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/workflows/pr-risk-scan-comment.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-risk-scan-comment.yml b/.github/workflows/pr-risk-scan-comment.yml index dd815871d..5870594e2 100644 --- a/.github/workflows/pr-risk-scan-comment.yml +++ b/.github/workflows/pr-risk-scan-comment.yml @@ -36,9 +36,19 @@ jobs: return; } - const body = fs.readFileSync(reportPath, 'utf8'); - let prNumber = null; + let body = fs.readFileSync(reportPath, 'utf8'); + // Treat artifact content as untrusted (the gate workflow runs on PR code). + // Prevent spam/notification abuse and avoid API failures on oversized bodies. + body = body.replace(/@/g, '@\u200b'); + const maxLength = 65000; + if (body.length > maxLength) { + body = `${body.slice(0, maxLength)}\n\n_...(truncated)..._`; + } + if (!body.includes(marker)) { + body = `${marker}\n${body}`; + } + let prNumber = null; if (fs.existsSync(prNumberPath)) { const parsed = parseInt(fs.readFileSync(prNumberPath, 'utf8').trim(), 10); if (!Number.isNaN(parsed)) {