diff --git a/.github/workflows/bot-detection.lock.yml b/.github/workflows/bot-detection.lock.yml index d750ac7b9dd..1583e368877 100644 --- a/.github/workflows/bot-detection.lock.yml +++ b/.github/workflows/bot-detection.lock.yml @@ -21,7 +21,7 @@ # # Investigates suspicious repository activity and maintains a single triage issue # -# frontmatter-hash: 4c52fe089425a05e8f0c3eb417d76791e70457a80961e818e7ab24ce0b7eb5f1 +# frontmatter-hash: 2178e3732b12824d02944782c3e73dbab22b3c402a400b320ac94f7f77cdb68d name: "Bot Detection" "on": @@ -1018,10 +1018,6 @@ jobs: return new Date(d).toISOString(); } - function safeLower(s) { - return (s || "").toString().toLowerCase(); - } - function normalizeForDup(s) { return (s || "") .toString() @@ -1209,39 +1205,90 @@ jobs: let issueComments = []; try { - const { data } = await github.rest.issues.listComments({ - owner, - repo, - issue_number: it.number, - per_page: 100, - }); - issueComments = data || []; + let total = 0; + issueComments = await github.paginate( + github.rest.issues.listComments, + { + owner, + repo, + issue_number: it.number, + per_page: 100, + }, + (response, done) => { + const remaining = 500 - total; + if (remaining <= 0) { + done(); + return []; + } + if (total + response.data.length >= 500) { + total = 500; + done(); + return response.data.slice(0, remaining); + } + total += response.data.length; + return response.data; + } + ); } catch { // ignore } let reviewComments = []; try { - const { data } = await github.rest.pulls.listReviewComments({ - owner, - repo, - pull_number: it.number, - per_page: 100, - }); - reviewComments = data || []; + let total = 0; + reviewComments = await github.paginate( + github.rest.pulls.listReviewComments, + { + owner, + repo, + pull_number: it.number, + per_page: 100, + }, + (response, done) => { + const remaining = 500 - total; + if (remaining <= 0) { + done(); + return []; + } + if (total + response.data.length >= 500) { + total = 500; + done(); + return response.data.slice(0, remaining); + } + total += response.data.length; + return response.data; + } + ); } catch { // ignore } let reviews = []; try { - const { data } = await github.rest.pulls.listReviews({ - owner, - repo, - pull_number: it.number, - per_page: 100, - }); - reviews = data || []; + let total = 0; + reviews = await github.paginate( + github.rest.pulls.listReviews, + { + owner, + repo, + pull_number: it.number, + per_page: 100, + }, + (response, done) => { + const remaining = 500 - total; + if (remaining <= 0) { + done(); + return []; + } + if (total + response.data.length >= 500) { + total = 500; + done(); + return response.data.slice(0, remaining); + } + total += response.data.length; + return response.data; + } + ); } catch { // ignore } @@ -1285,19 +1332,37 @@ jobs: } // PR file touches (sensitive paths) - deterministic and bounded - for (const it of items.filter(i => i.is_pr)) { + for (const it of prItems) { const login = it.author; if (!login) continue; const s = ensureAuthor(login); try { - const filesResp = await github.rest.pulls.listFiles({ - owner, - repo, - pull_number: it.number, - per_page: 100, - }); - const filenames = (filesResp.data || []).map(f => f.filename); + let total = 0; + const files = await github.paginate( + github.rest.pulls.listFiles, + { + owner, + repo, + pull_number: it.number, + per_page: 100, + }, + (response, done) => { + const remaining = 500 - total; + if (remaining <= 0) { + done(); + return []; + } + if (total + response.data.length >= 500) { + total = 500; + done(); + return response.data.slice(0, remaining); + } + total += response.data.length; + return response.data; + } + ); + const filenames = files.map(f => f.filename); for (const fn of filenames) { if (fn.startsWith(".github/workflows/") || fn.startsWith(".github/actions/")) s.touchesWorkflows = true; if (fn === "Dockerfile" || fn === "Makefile" || fn.startsWith("scripts/") || fn.startsWith("actions/")) s.touchesCI = true; diff --git a/.github/workflows/bot-detection.md b/.github/workflows/bot-detection.md index a5957d68000..ba4b049463c 100644 --- a/.github/workflows/bot-detection.md +++ b/.github/workflows/bot-detection.md @@ -232,39 +232,90 @@ jobs: let issueComments = []; try { - const { data } = await github.rest.issues.listComments({ - owner, - repo, - issue_number: it.number, - per_page: 100, - }); - issueComments = data || []; + let total = 0; + issueComments = await github.paginate( + github.rest.issues.listComments, + { + owner, + repo, + issue_number: it.number, + per_page: 100, + }, + (response, done) => { + const remaining = 500 - total; + if (remaining <= 0) { + done(); + return []; + } + if (total + response.data.length >= 500) { + total = 500; + done(); + return response.data.slice(0, remaining); + } + total += response.data.length; + return response.data; + } + ); } catch { // ignore } let reviewComments = []; try { - const { data } = await github.rest.pulls.listReviewComments({ - owner, - repo, - pull_number: it.number, - per_page: 100, - }); - reviewComments = data || []; + let total = 0; + reviewComments = await github.paginate( + github.rest.pulls.listReviewComments, + { + owner, + repo, + pull_number: it.number, + per_page: 100, + }, + (response, done) => { + const remaining = 500 - total; + if (remaining <= 0) { + done(); + return []; + } + if (total + response.data.length >= 500) { + total = 500; + done(); + return response.data.slice(0, remaining); + } + total += response.data.length; + return response.data; + } + ); } catch { // ignore } let reviews = []; try { - const { data } = await github.rest.pulls.listReviews({ - owner, - repo, - pull_number: it.number, - per_page: 100, - }); - reviews = data || []; + let total = 0; + reviews = await github.paginate( + github.rest.pulls.listReviews, + { + owner, + repo, + pull_number: it.number, + per_page: 100, + }, + (response, done) => { + const remaining = 500 - total; + if (remaining <= 0) { + done(); + return []; + } + if (total + response.data.length >= 500) { + total = 500; + done(); + return response.data.slice(0, remaining); + } + total += response.data.length; + return response.data; + } + ); } catch { // ignore } @@ -314,13 +365,31 @@ jobs: const s = ensureAuthor(login); try { - const filesResp = await github.rest.pulls.listFiles({ - owner, - repo, - pull_number: it.number, - per_page: 100, - }); - const filenames = (filesResp.data || []).map(f => f.filename); + let total = 0; + const files = await github.paginate( + github.rest.pulls.listFiles, + { + owner, + repo, + pull_number: it.number, + per_page: 100, + }, + (response, done) => { + const remaining = 500 - total; + if (remaining <= 0) { + done(); + return []; + } + if (total + response.data.length >= 500) { + total = 500; + done(); + return response.data.slice(0, remaining); + } + total += response.data.length; + return response.data; + } + ); + const filenames = files.map(f => f.filename); for (const fn of filenames) { if (fn.startsWith(".github/workflows/") || fn.startsWith(".github/actions/")) s.touchesWorkflows = true; if (fn === "Dockerfile" || fn === "Makefile" || fn.startsWith("scripts/") || fn.startsWith("actions/")) s.touchesCI = true; diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index bb8eb9abb82..156d988faf1 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -37,7 +37,7 @@ name: "Smoke Copilot" types: - labeled schedule: - - cron: "37 */12 * * *" + - cron: "46 */12 * * *" workflow_dispatch: null permissions: {} diff --git a/actions/setup/js/merge_remote_agent_github_folder.cjs b/actions/setup/js/merge_remote_agent_github_folder.cjs index 6f843a36ac2..77a0a58356d 100644 --- a/actions/setup/js/merge_remote_agent_github_folder.cjs +++ b/actions/setup/js/merge_remote_agent_github_folder.cjs @@ -146,23 +146,23 @@ function validateGitParameter(value, name) { */ function validateSafePath(userPath, basePath, name) { // Reject paths with null bytes - if (userPath.includes('\0')) { + if (userPath.includes("\0")) { throw new Error(`Invalid ${name}: contains null bytes`); } - + // Reject paths that attempt to traverse up (..) - if (userPath.includes('..')) { + if (userPath.includes("..")) { throw new Error(`Invalid ${name}: path traversal detected`); } - + // Resolve the full path and ensure it's within the base path const resolvedPath = path.resolve(basePath, userPath); const resolvedBase = path.resolve(basePath); - + if (!resolvedPath.startsWith(resolvedBase + path.sep) && resolvedPath !== resolvedBase) { throw new Error(`Invalid ${name}: path escapes base directory`); } - + return resolvedPath; } @@ -239,7 +239,7 @@ function mergeGithubFolder(sourcePath, destPath) { for (const relativePath of sourceFiles) { // Validate relative path to prevent path traversal validateSafePath(relativePath, sourcePath, "relative file path"); - + // Check if the file is in one of the allowed subfolders const pathParts = relativePath.split(path.sep); const topLevelFolder = pathParts[0];