From df712f8613dcf434f3fe243fc8dca1a534c8a993 Mon Sep 17 00:00:00 2001 From: GuillaumeDua Date: Sun, 23 Feb 2025 13:33:15 +0100 Subject: [PATCH 01/26] [#2258] antispam workflow --- .github/workflows/antispam.yml | 21 +++ .github/workflows/scripts/antispam.js | 185 ++++++++++++++++++++++++++ 2 files changed, 206 insertions(+) create mode 100644 .github/workflows/antispam.yml create mode 100644 .github/workflows/scripts/antispam.js diff --git a/.github/workflows/antispam.yml b/.github/workflows/antispam.yml new file mode 100644 index 000000000..f3e5b2bf4 --- /dev/null +++ b/.github/workflows/antispam.yml @@ -0,0 +1,21 @@ +name: Anti-Spam Issue & PR Checker + +on: + workflow_dispatch: # manual testing only + issues: + types: [opened, reopened] + pull_request: + types: [opened, reopened] + +jobs: + check-spam: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/github-script@v7 + env: + SHA: '${{env.parentSHA}}' + with: + script: | + const script = require('.github/workflows/scripts/antispam.js') + await script({github, context, core}) diff --git a/.github/workflows/scripts/antispam.js b/.github/workflows/scripts/antispam.js new file mode 100644 index 000000000..d34d90e59 --- /dev/null +++ b/.github/workflows/scripts/antispam.js @@ -0,0 +1,185 @@ +const configuration = { + label_if_suspicious: false, + comment_if_suspicious: false, + close_if_suspicious: false, + suspicious_criteria_tolerated: 0 +}; + +async function make_information_report({ user }) { + // we might also create a (pre-)report for spam to GH using the following informations: + return `> [!WARNING] About the author: +> +> | information | value | +> | ----------- | ----- | +> | email | ${user.email} | +> | login | ${user.login} | +> | name | ${user.name} | +> | location | ${user.location} | +> | blog | ${user.blog} | +> | location | ${user.location} | +` +} + +async function when_suspicious({ github, context, failed_checks }){ + + // REFACTO: might wanna use a score of confidence (how suspicious it is), then react on that + + const reasons = failed_checks.map(check => `> - ${check.reason}`).join("\n"); + const commentBody = `> [!WARNING] This issue/PR has been automatically flagged as [suspicious] as it might not meet contribution requirements. +> Please read our contribution guide before submitting. +> +> Reason(s): +> +${reasons} +`; + + console.log("Body of the produced comment:\n", commentBody); + + if (context.eventName === 'workflow_dispatch') // so we can test manually + return; + + const { owner, repo } = context.repo; + const issueNumber = context.payload.number; // either issue or PR + + if (configuration.comment_if_suspicious) { + await github.rest.issues.createComment({ + owner, + repo, + issue_number: issueNumber, + body: `${commentBody}` + }); + } + if (! configuration.label_if_suspicious) { + await github.rest.issues.addLabels({ + owner, + repo, + issue_number: issueNumber, + labels: ["suspicious"] + }); + } + if (configuration.close_if_suspicious) { + await github.rest.issues.update({ + owner, + repo, + issue_number: issueNumber, + state: "closed" + }); + } +} + +class Check { + constructor({ predicate, reason }) { + this.predicate = predicate; + this.reason = reason; + } + + async pass() { + const result = await this.predicate(); + if (typeof result !== "boolean") + console.error("Check: invalid argument: not a predicate"); + + console.debug("- check: ", (result ? "PASSED" : "FAILED"), " => ", this.reason) + + return result; + } +} + +module.exports = async ({ github, context, core }) => { + + // const {SHA} = process.env; // for octokit.rest.repos.getCommit + const username = context.actor; + const { data: user } = await github.rest.users.getByUsername({ username: username }); + + const isAuthorOnlyContributionOnGH = await (async () => { + // WARNING: Depending on the time of day, event latency can be anywhere from 30s to 6h. (source: https://octokit.github.io/rest.js/v21/) + const { data: events } = await github.rest.activity.listEventsForAuthenticatedUser({ + username: username, + per_page: 1 + }); + return events.length === 0; + })(); + const WasAuthorRecentlyCreated = (() => { + + const time_point = (() => { + let value = new Date(); + value.setHours(value.getHours() - 2); + return value; + })(); + const create_at = new Date(user.created_at); + return create_at >= time_point; + })(); + + const isTitleOrBodyTooShort = (() => { + + if (context.eventName === 'workflow_dispatch') // issues or pull_request + return false; + + const payload = context.payload; + const title = payload.issue?.title || payload.pull_request?.title || ""; + const body = payload.issue?.body || payload.pull_request?.body || ""; + + const threshold = 20; + return title.length < threshold + || body.length < threshold; + })(); + + const checks = [ + new Check({ + predicate: () => false, // ! WasAuthorRecentlyCreated, + reason: "Author account was recently created" + }), + new Check({ + predicate: () => ! isAuthorOnlyContributionOnGH, + reason: "Author first contribution to any GitHub project" + }), + new Check({ + predicate: () => user.followers !== 0 && user.following !== 0, + reason: "Author has no relationships" + }), + new Check({ + predicate: () => user.public_repos !== 0 && user.public_gists !== 0, + reason: "Author has no public repo/gist" + }), + new Check({ + predicate: () => ! isTitleOrBodyTooShort, + reason: "Issue/PR title or body too short" + }), + ]; + + // IDEA: mandatory checks -> if any fails, then reject + // for other checks + // then use a weights/factors instead of booleans, + // compute a confidence score to check against a threshold => if below, then reject + + async function failedChecks(checks) { + const results = await Promise.all( + checks.map(async (check) => ({ + check, + passed: await check.pass(), + })) + ); + return results + .filter(({ passed }) => ! passed) + .map(({ check }) => check); + } + + failedChecks(checks).then(failed_checks => { + + console.log("Checks: ", { + passed: checks.length - failed_checks.length, + failed: failed_checks.length + }) + + if (failed_checks.length <= configuration.suspicious_criteria_tolerated) { + console.info("Not suspicious"); + return; + } + + when_suspicious({ github, context, failed_checks}); + + make_information_report({ user: user }).then(user_information_as_comment => { + // do stuffs with user_information_as_comment + console.log("user_information_as_comment", user_information_as_comment); + }); + }); +}; From ee8a2bc13ed3be1998df23f9e6d3251a7a66c43b Mon Sep 17 00:00:00 2001 From: GuillaumeDua Date: Sun, 2 Mar 2025 14:09:27 +0100 Subject: [PATCH 02/26] [antispam] experimenting (WIP) --- .github/workflows/scripts/antispam.js | 66 +++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/.github/workflows/scripts/antispam.js b/.github/workflows/scripts/antispam.js index d34d90e59..c66d1a24c 100644 --- a/.github/workflows/scripts/antispam.js +++ b/.github/workflows/scripts/antispam.js @@ -84,7 +84,59 @@ class Check { } } -module.exports = async ({ github, context, core }) => { +class Testing { + + static enabled = true; + + static parseGitHubUrl({ url }) { + const match = url.match(/github\.com\/([^\/]+)\/([^\/]+)\/(issues|pull)\/(\d+)/); + if (!match || match.length !== 4) + return null; + + return { + owner: match[1], + repo: match[2], + type: match[3], // "issues" or "pull" + number: parseInt(match[4], 10), + }; + } + + static async getContext({ url, github }) { + + const details = parseGitHubUrl(url); + if (!details) { + throw new Error(`Invalid GitHub issue/PR URL: [${url}]`); + } + + const { owner, repo, type, number } = details; + let response; + + try { + response = (type === "issues") + ? await github.rest.issues.get({ owner, repo, issue_number: number }) + : await github.rest.pulls.get({ owner, repo, pull_number: number }) + ; + } + catch (error) { + throw new Error(`Failed to fetch ${type.slice(0, -1)} #${number}: ${error.message}`); + } + + return response.data; + } + + static cases = [ + 'https://github.com/isocpp/CppCoreGuidelines/pull/2257', + 'https://github.com/isocpp/CppCoreGuidelines/pull/2241', + 'https://github.com/isocpp/CppCoreGuidelines/pull/2254', + 'https://github.com/isocpp/CppCoreGuidelines/pull/2252', + 'https://github.com/isocpp/CppCoreGuidelines/issues/2249', + 'https://github.com/isocpp/CppCoreGuidelines/issues/2238', + 'https://github.com/isocpp/CppCoreGuidelines/issues/2225', + 'https://github.com/isocpp/CppCoreGuidelines/issues/2255', + ] +} + +async function run({ github, context, core }) { // const {SHA} = process.env; // for octokit.rest.repos.getCommit const username = context.actor; @@ -108,7 +160,6 @@ module.exports = async ({ github, context, core }) => { const create_at = new Date(user.created_at); return create_at >= time_point; })(); - const isTitleOrBodyTooShort = (() => { if (context.eventName === 'workflow_dispatch') // issues or pull_request @@ -125,7 +176,7 @@ module.exports = async ({ github, context, core }) => { const checks = [ new Check({ - predicate: () => false, // ! WasAuthorRecentlyCreated, + predicate: () => ! WasAuthorRecentlyCreated, reason: "Author account was recently created" }), new Check({ @@ -183,3 +234,12 @@ module.exports = async ({ github, context, core }) => { }); }); }; + +module.exports = async ({ github, context, core }) => { + + if (! Testing.enabled) + return await run({ github, context, core }); + + const testing_contexts = await Array.from(cases, (url) => getContext({ url: url, github })) + testing_contexts.forEach((context) => console.log(`debug: ${context.actor}`)) +}; From a35f45af571fd6c226327410d58a80f4b1b2cc86 Mon Sep 17 00:00:00 2001 From: GuillaumeDua Date: Sun, 2 Mar 2025 14:11:37 +0100 Subject: [PATCH 03/26] [antispam] experimenting (WIP) --- .github/workflows/scripts/antispam.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/scripts/antispam.js b/.github/workflows/scripts/antispam.js index c66d1a24c..210c82b29 100644 --- a/.github/workflows/scripts/antispam.js +++ b/.github/workflows/scripts/antispam.js @@ -240,6 +240,9 @@ module.exports = async ({ github, context, core }) => { if (! Testing.enabled) return await run({ github, context, core }); - const testing_contexts = await Array.from(cases, (url) => getContext({ url: url, github })) + const testing_contexts = await Array.from( + Testing.cases, + (url) => getContext({ url: url, github }) + ); testing_contexts.forEach((context) => console.log(`debug: ${context.actor}`)) }; From 4785d9df73f9e690b96392086de7df0d4ed79e7e Mon Sep 17 00:00:00 2001 From: GuillaumeDua Date: Sun, 2 Mar 2025 14:12:57 +0100 Subject: [PATCH 04/26] [antispam] experimenting (WIP) --- .github/workflows/scripts/antispam.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scripts/antispam.js b/.github/workflows/scripts/antispam.js index 210c82b29..44f632221 100644 --- a/.github/workflows/scripts/antispam.js +++ b/.github/workflows/scripts/antispam.js @@ -242,7 +242,7 @@ module.exports = async ({ github, context, core }) => { const testing_contexts = await Array.from( Testing.cases, - (url) => getContext({ url: url, github }) + (url) => Testing.getContext({ url: url, github }) ); testing_contexts.forEach((context) => console.log(`debug: ${context.actor}`)) }; From f46e342c9b2a94c38641c5cc187f17289cf99599 Mon Sep 17 00:00:00 2001 From: GuillaumeDua Date: Sun, 2 Mar 2025 14:14:39 +0100 Subject: [PATCH 05/26] [antispam] experimenting (WIP) --- .github/workflows/scripts/antispam.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/scripts/antispam.js b/.github/workflows/scripts/antispam.js index 44f632221..15f873e51 100644 --- a/.github/workflows/scripts/antispam.js +++ b/.github/workflows/scripts/antispam.js @@ -88,7 +88,7 @@ class Testing { static enabled = true; - static parseGitHubUrl({ url }) { + static #parseGitHubUrl({ url }) { const match = url.match(/github\.com\/([^\/]+)\/([^\/]+)\/(issues|pull)\/(\d+)/); if (!match || match.length !== 4) return null; @@ -103,7 +103,7 @@ class Testing { static async getContext({ url, github }) { - const details = parseGitHubUrl(url); + const details = Testing.#parseGitHubUrl(url); if (!details) { throw new Error(`Invalid GitHub issue/PR URL: [${url}]`); } From db2636fba5383100279beb3d7ee96c192e7fe1ad Mon Sep 17 00:00:00 2001 From: GuillaumeDua Date: Sun, 2 Mar 2025 14:16:44 +0100 Subject: [PATCH 06/26] [antispam] experimenting (WIP) --- .github/workflows/scripts/antispam.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/scripts/antispam.js b/.github/workflows/scripts/antispam.js index 15f873e51..b69075ae9 100644 --- a/.github/workflows/scripts/antispam.js +++ b/.github/workflows/scripts/antispam.js @@ -103,7 +103,7 @@ class Testing { static async getContext({ url, github }) { - const details = Testing.#parseGitHubUrl(url); + const details = Testing.#parseGitHubUrl({ url: url }); if (!details) { throw new Error(`Invalid GitHub issue/PR URL: [${url}]`); } @@ -242,7 +242,7 @@ module.exports = async ({ github, context, core }) => { const testing_contexts = await Array.from( Testing.cases, - (url) => Testing.getContext({ url: url, github }) + async (url) => await Testing.getContext({ url: url, github }) ); testing_contexts.forEach((context) => console.log(`debug: ${context.actor}`)) }; From 1868ba390669a1082aa13de772b5bdb48282a910 Mon Sep 17 00:00:00 2001 From: GuillaumeDua Date: Sun, 2 Mar 2025 14:18:09 +0100 Subject: [PATCH 07/26] [antispam] experimenting (WIP) --- .github/workflows/scripts/antispam.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/scripts/antispam.js b/.github/workflows/scripts/antispam.js index b69075ae9..50a01e76d 100644 --- a/.github/workflows/scripts/antispam.js +++ b/.github/workflows/scripts/antispam.js @@ -90,7 +90,7 @@ class Testing { static #parseGitHubUrl({ url }) { const match = url.match(/github\.com\/([^\/]+)\/([^\/]+)\/(issues|pull)\/(\d+)/); - if (!match || match.length !== 4) + if (!match || match.length !== 5) return null; return { @@ -103,12 +103,12 @@ class Testing { static async getContext({ url, github }) { - const details = Testing.#parseGitHubUrl({ url: url }); - if (!details) { + const parsed_url = Testing.#parseGitHubUrl({ url: url }); + if (!parsed_url) { throw new Error(`Invalid GitHub issue/PR URL: [${url}]`); } - const { owner, repo, type, number } = details; + const { owner, repo, type, number } = parsed_url; let response; try { From 9cb1704ce4b278154f2442a3f8804ecc8abb7002 Mon Sep 17 00:00:00 2001 From: GuillaumeDua Date: Sun, 2 Mar 2025 14:20:38 +0100 Subject: [PATCH 08/26] [antispam] experimenting (WIP) --- .github/workflows/scripts/antispam.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/scripts/antispam.js b/.github/workflows/scripts/antispam.js index 50a01e76d..ba197ec1b 100644 --- a/.github/workflows/scripts/antispam.js +++ b/.github/workflows/scripts/antispam.js @@ -94,10 +94,10 @@ class Testing { return null; return { - owner: match[1], - repo: match[2], - type: match[3], // "issues" or "pull" - number: parseInt(match[4], 10), + owner: match[1], + repo: match[2], + type: match[3], // "issues" or "pull" + number: parseInt(match[4], 10), }; } @@ -105,20 +105,21 @@ class Testing { const parsed_url = Testing.#parseGitHubUrl({ url: url }); if (!parsed_url) { - throw new Error(`Invalid GitHub issue/PR URL: [${url}]`); + throw new Error(`Invalid GitHub issue/PR URL: [${url}]`); } const { owner, repo, type, number } = parsed_url; let response; try { - response = (type === "issues") - ? await github.rest.issues.get({ owner, repo, issue_number: number }) - : await github.rest.pulls.get({ owner, repo, pull_number: number }) + response = (type === "issues") + ? await github.rest.issues.get({ owner, repo, issue_number: number }) + : await github.rest.pulls.get({ owner, repo, pull_number: number }) ; + console.log(`>>> ${response}`) } catch (error) { - throw new Error(`Failed to fetch ${type.slice(0, -1)} #${number}: ${error.message}`); + throw new Error(`Failed to fetch ${type.slice(0, -1)} #${number}: ${error.message}`); } return response.data; From 3a229abed412c44350765764f5316fde03ed887d Mon Sep 17 00:00:00 2001 From: GuillaumeDua Date: Sun, 2 Mar 2025 14:21:35 +0100 Subject: [PATCH 09/26] [antispam] experimenting (WIP) --- .github/workflows/scripts/antispam.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scripts/antispam.js b/.github/workflows/scripts/antispam.js index ba197ec1b..97482b5ae 100644 --- a/.github/workflows/scripts/antispam.js +++ b/.github/workflows/scripts/antispam.js @@ -116,7 +116,7 @@ class Testing { ? await github.rest.issues.get({ owner, repo, issue_number: number }) : await github.rest.pulls.get({ owner, repo, pull_number: number }) ; - console.log(`>>> ${response}`) + console.log('>>>', response) } catch (error) { throw new Error(`Failed to fetch ${type.slice(0, -1)} #${number}: ${error.message}`); From 54ef95ecbc65b7fccdaa0e6d4fbdf3ec222350d0 Mon Sep 17 00:00:00 2001 From: GuillaumeDua Date: Sun, 2 Mar 2025 14:22:56 +0100 Subject: [PATCH 10/26] [antispam] experimenting (WIP) --- .github/workflows/scripts/antispam.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scripts/antispam.js b/.github/workflows/scripts/antispam.js index 97482b5ae..a8ec8ff9d 100644 --- a/.github/workflows/scripts/antispam.js +++ b/.github/workflows/scripts/antispam.js @@ -245,5 +245,5 @@ module.exports = async ({ github, context, core }) => { Testing.cases, async (url) => await Testing.getContext({ url: url, github }) ); - testing_contexts.forEach((context) => console.log(`debug: ${context.actor}`)) + await testing_contexts.forEach((context) => console.log(`debug: ${context.actor}`)) }; From 93dee9a43d3c3544aeaf46fd0554a969de936706 Mon Sep 17 00:00:00 2001 From: GuillaumeDua Date: Sun, 2 Mar 2025 14:27:31 +0100 Subject: [PATCH 11/26] [antispam] experimenting (WIP) --- .github/workflows/scripts/antispam.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/scripts/antispam.js b/.github/workflows/scripts/antispam.js index a8ec8ff9d..020225f5f 100644 --- a/.github/workflows/scripts/antispam.js +++ b/.github/workflows/scripts/antispam.js @@ -116,7 +116,6 @@ class Testing { ? await github.rest.issues.get({ owner, repo, issue_number: number }) : await github.rest.pulls.get({ owner, repo, pull_number: number }) ; - console.log('>>>', response) } catch (error) { throw new Error(`Failed to fetch ${type.slice(0, -1)} #${number}: ${error.message}`); @@ -245,5 +244,7 @@ module.exports = async ({ github, context, core }) => { Testing.cases, async (url) => await Testing.getContext({ url: url, github }) ); - await testing_contexts.forEach((context) => console.log(`debug: ${context.actor}`)) + + console.log("Testing contextes:") + await testing_contexts.forEach((context) => console.log(`>>> debug: ${context.actor}`)) }; From 72fd0ab803cdaf2ecd07ae412ab7a0ff6b4db36c Mon Sep 17 00:00:00 2001 From: GuillaumeDua Date: Sun, 2 Mar 2025 14:39:44 +0100 Subject: [PATCH 12/26] [antispam] experimenting (WIP) --- .github/workflows/scripts/antispam.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/scripts/antispam.js b/.github/workflows/scripts/antispam.js index 020225f5f..d33b45ddd 100644 --- a/.github/workflows/scripts/antispam.js +++ b/.github/workflows/scripts/antispam.js @@ -240,11 +240,10 @@ module.exports = async ({ github, context, core }) => { if (! Testing.enabled) return await run({ github, context, core }); - const testing_contexts = await Array.from( - Testing.cases, - async (url) => await Testing.getContext({ url: url, github }) + const testing_contexts = await Promise.all( + Testing.cases.map(async (url) => await Testing.getContext({ url, github })) ); - + console.log("Testing contextes:") await testing_contexts.forEach((context) => console.log(`>>> debug: ${context.actor}`)) }; From 7dce9d2380fd5c97e3d4c6db5c3b28d70c9d5bae Mon Sep 17 00:00:00 2001 From: GuillaumeDua Date: Sun, 2 Mar 2025 14:44:57 +0100 Subject: [PATCH 13/26] [antispam] experimenting (WIP) --- .github/workflows/scripts/antispam.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/scripts/antispam.js b/.github/workflows/scripts/antispam.js index d33b45ddd..8a5111f0b 100644 --- a/.github/workflows/scripts/antispam.js +++ b/.github/workflows/scripts/antispam.js @@ -240,10 +240,11 @@ module.exports = async ({ github, context, core }) => { if (! Testing.enabled) return await run({ github, context, core }); - const testing_contexts = await Promise.all( + // IDEA: run N-by-N to limit memory bloat + await Promise.all( Testing.cases.map(async (url) => await Testing.getContext({ url, github })) - ); - - console.log("Testing contextes:") - await testing_contexts.forEach((context) => console.log(`>>> debug: ${context.actor}`)) + ).then((testing_contexts) => { + console.log("Actors:", testing_contexts.map((context) => context.actor)) + }) + ; }; From 5c805c26a51f1420eaa3fc282d7da9252b74721b Mon Sep 17 00:00:00 2001 From: GuillaumeDua Date: Sun, 2 Mar 2025 14:46:53 +0100 Subject: [PATCH 14/26] [antispam] experimenting (WIP) --- .github/workflows/scripts/antispam.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scripts/antispam.js b/.github/workflows/scripts/antispam.js index 8a5111f0b..2fa92968f 100644 --- a/.github/workflows/scripts/antispam.js +++ b/.github/workflows/scripts/antispam.js @@ -242,7 +242,7 @@ module.exports = async ({ github, context, core }) => { // IDEA: run N-by-N to limit memory bloat await Promise.all( - Testing.cases.map(async (url) => await Testing.getContext({ url, github })) + Testing.cases.map((url) => Testing.getContext({ url, github })) ).then((testing_contexts) => { console.log("Actors:", testing_contexts.map((context) => context.actor)) }) From b12a19f25da42cc9d60702c0f0a06bd9092ea074 Mon Sep 17 00:00:00 2001 From: GuillaumeDua Date: Sun, 2 Mar 2025 14:48:32 +0100 Subject: [PATCH 15/26] [antispam] experimenting (WIP) --- .github/workflows/scripts/antispam.js | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/scripts/antispam.js b/.github/workflows/scripts/antispam.js index 2fa92968f..2cc02bed1 100644 --- a/.github/workflows/scripts/antispam.js +++ b/.github/workflows/scripts/antispam.js @@ -121,6 +121,7 @@ class Testing { throw new Error(`Failed to fetch ${type.slice(0, -1)} #${number}: ${error.message}`); } + console.log(">>> WTF", response.data.actor) return response.data; } From 492b15e4f556ef9eacaae6c2a63ea50193d49519 Mon Sep 17 00:00:00 2001 From: GuillaumeDua Date: Sun, 2 Mar 2025 14:49:33 +0100 Subject: [PATCH 16/26] [antispam] experimenting (WIP) --- .github/workflows/scripts/antispam.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scripts/antispam.js b/.github/workflows/scripts/antispam.js index 2cc02bed1..b57ae8b59 100644 --- a/.github/workflows/scripts/antispam.js +++ b/.github/workflows/scripts/antispam.js @@ -121,7 +121,7 @@ class Testing { throw new Error(`Failed to fetch ${type.slice(0, -1)} #${number}: ${error.message}`); } - console.log(">>> WTF", response.data.actor) + console.log('response.data', response.data) return response.data; } From c3b11f67507673a712103f24a1b38f3fa4e62876 Mon Sep 17 00:00:00 2001 From: GuillaumeDua Date: Sun, 2 Mar 2025 14:54:24 +0100 Subject: [PATCH 17/26] [antispam] experimenting (WIP) --- .github/workflows/scripts/antispam.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/scripts/antispam.js b/.github/workflows/scripts/antispam.js index b57ae8b59..47885ca64 100644 --- a/.github/workflows/scripts/antispam.js +++ b/.github/workflows/scripts/antispam.js @@ -245,7 +245,15 @@ module.exports = async ({ github, context, core }) => { await Promise.all( Testing.cases.map((url) => Testing.getContext({ url, github })) ).then((testing_contexts) => { - console.log("Actors:", testing_contexts.map((context) => context.actor)) + console.log( + "Actors:", + testing_contexts.map((context) => { + return { + login: context.user.login, + id: context.user.id + } + }) + ) }) ; }; From 294eea7b82a3ce39d3c3239dcbddbd5812552486 Mon Sep 17 00:00:00 2001 From: GuillaumeDua Date: Sun, 2 Mar 2025 20:29:39 +0100 Subject: [PATCH 18/26] [antispam] experimenting (WIP) --- .github/workflows/scripts/antispam.js | 136 ++++++++++++++------------ 1 file changed, 75 insertions(+), 61 deletions(-) diff --git a/.github/workflows/scripts/antispam.js b/.github/workflows/scripts/antispam.js index 47885ca64..8d9311e25 100644 --- a/.github/workflows/scripts/antispam.js +++ b/.github/workflows/scripts/antispam.js @@ -84,59 +84,6 @@ class Check { } } -class Testing { - - static enabled = true; - - static #parseGitHubUrl({ url }) { - const match = url.match(/github\.com\/([^\/]+)\/([^\/]+)\/(issues|pull)\/(\d+)/); - if (!match || match.length !== 5) - return null; - - return { - owner: match[1], - repo: match[2], - type: match[3], // "issues" or "pull" - number: parseInt(match[4], 10), - }; - } - - static async getContext({ url, github }) { - - const parsed_url = Testing.#parseGitHubUrl({ url: url }); - if (!parsed_url) { - throw new Error(`Invalid GitHub issue/PR URL: [${url}]`); - } - - const { owner, repo, type, number } = parsed_url; - let response; - - try { - response = (type === "issues") - ? await github.rest.issues.get({ owner, repo, issue_number: number }) - : await github.rest.pulls.get({ owner, repo, pull_number: number }) - ; - } - catch (error) { - throw new Error(`Failed to fetch ${type.slice(0, -1)} #${number}: ${error.message}`); - } - - console.log('response.data', response.data) - return response.data; - } - - static cases = [ - 'https://github.com/isocpp/CppCoreGuidelines/pull/2257', - 'https://github.com/isocpp/CppCoreGuidelines/pull/2241', - 'https://github.com/isocpp/CppCoreGuidelines/pull/2254', - 'https://github.com/isocpp/CppCoreGuidelines/pull/2252', - 'https://github.com/isocpp/CppCoreGuidelines/issues/2249', - 'https://github.com/isocpp/CppCoreGuidelines/issues/2238', - 'https://github.com/isocpp/CppCoreGuidelines/issues/2225', - 'https://github.com/isocpp/CppCoreGuidelines/issues/2255', - ] -} - async function run({ github, context, core }) { // const {SHA} = process.env; // for octokit.rest.repos.getCommit @@ -200,7 +147,7 @@ async function run({ github, context, core }) { // IDEA: mandatory checks -> if any fails, then reject // for other checks - // then use a weights/factors instead of booleans, + // use a weights/factors instead of booleans // compute a confidence score to check against a threshold => if below, then reject async function failedChecks(checks) { @@ -236,24 +183,91 @@ async function run({ github, context, core }) { }); }; +class Testing { + + static enabled = true; + + static #parseGitHubUrl({ url }) { + const match = url.match(/github\.com\/([^\/]+)\/([^\/]+)\/(issues|pull)\/(\d+)/); + if (!match || match.length !== 5) + return null; + + return { + owner: match[1], + repo: match[2], + type: match[3], // "issues" or "pull" + number: parseInt(match[4], 10), + }; + } + + static async getContext({ url, github }) { + + const parsed_url = Testing.#parseGitHubUrl({ url: url }); + if (!parsed_url) { + throw new Error(`Invalid GitHub issue/PR URL: [${url}]`); + } + + const { owner, repo, type, number } = parsed_url; + let response; + + try { + response = (type === "issues") + ? await github.rest.issues.get({ owner, repo, issue_number: number }) + : await github.rest.pulls.get({ owner, repo, pull_number: number }) + ; + } + catch (error) { + throw new Error(`Failed to fetch ${type.slice(0, -1)} #${number}: ${error.message}`); + } + + // console.log('response.data', response.data) + return response.data; + } + + static cases = [ + 'https://github.com/isocpp/CppCoreGuidelines/pull/2257' + // 'https://github.com/isocpp/CppCoreGuidelines/pull/2241', + // 'https://github.com/isocpp/CppCoreGuidelines/pull/2254', + // 'https://github.com/isocpp/CppCoreGuidelines/pull/2252', + // 'https://github.com/isocpp/CppCoreGuidelines/issues/2249', + // 'https://github.com/isocpp/CppCoreGuidelines/issues/2238', + // 'https://github.com/isocpp/CppCoreGuidelines/issues/2225', + // 'https://github.com/isocpp/CppCoreGuidelines/issues/2255', + ] + // WIP: split suspicious, legit cases to test against false-positives +} + + module.exports = async ({ github, context, core }) => { if (! Testing.enabled) return await run({ github, context, core }); + console.log('>>> DEBUG: context', context) + // IDEA: run N-by-N to limit memory bloat await Promise.all( Testing.cases.map((url) => Testing.getContext({ url, github })) - ).then((testing_contexts) => { + ).then(async (testing_contexts) => { console.log( "Actors:", - testing_contexts.map((context) => { - return { - login: context.user.login, - id: context.user.id - } - }) + testing_contexts[0] + // testing_contexts.map((context) => { + // return { + // login: context.user.login, + // id: context.user.id + // } + // }) ) + + // WIP: homogeneous context ? or run call ? + // await run({ github, context, core }) + // WIP: is_suspicious }) ; }; + +/* +A bit more experiments: made some changes so such a CI can run on an arbitrary issue/PR URL, +so it's easier to test against a list of. See https://github.com/GuillaumeDua/CppCoreGuidelines/actions/runs/13616115173/job/38059374996#step:3:1547 +*/ \ No newline at end of file From 548bec67e7bcf9f4ffb24b4c01b5226d523f5038 Mon Sep 17 00:00:00 2001 From: GuillaumeDua Date: Sun, 2 Mar 2025 21:08:56 +0100 Subject: [PATCH 19/26] [antispam] experimenting (WIP) --- .github/workflows/scripts/antispam.js | 109 +++++++++++++++----------- 1 file changed, 62 insertions(+), 47 deletions(-) diff --git a/.github/workflows/scripts/antispam.js b/.github/workflows/scripts/antispam.js index 8d9311e25..ca5844103 100644 --- a/.github/workflows/scripts/antispam.js +++ b/.github/workflows/scripts/antispam.js @@ -1,7 +1,7 @@ -const configuration = { - label_if_suspicious: false, - comment_if_suspicious: false, - close_if_suspicious: false, +let configuration = { + label_if_suspicious: true, + comment_if_suspicious: true, + close_if_suspicious: true, suspicious_criteria_tolerated: 0 }; @@ -84,9 +84,8 @@ class Check { } } -async function run({ github, context, core }) { +async function run({ github, context }) { - // const {SHA} = process.env; // for octokit.rest.repos.getCommit const username = context.actor; const { data: user } = await github.rest.users.getByUsername({ username: username }); @@ -174,7 +173,7 @@ async function run({ github, context, core }) { return; } - when_suspicious({ github, context, failed_checks}); + when_suspicious({ github, context, failed_checks }); make_information_report({ user: user }).then(user_information_as_comment => { // do stuffs with user_information_as_comment @@ -221,53 +220,69 @@ class Testing { } // console.log('response.data', response.data) - return response.data; + // return response.data; + + // make context adapter => could perform it upstream with graphql + const payload_content = { + title: response.data.title, + body: response.data.body, + }; + return { + actor: response.data.user.login, + sender: response.data.user, + eventName: type, + payload: type === 'issues' ? { issue: payload_content } : { pull_request: payload_content } + } } - static cases = [ - 'https://github.com/isocpp/CppCoreGuidelines/pull/2257' - // 'https://github.com/isocpp/CppCoreGuidelines/pull/2241', - // 'https://github.com/isocpp/CppCoreGuidelines/pull/2254', - // 'https://github.com/isocpp/CppCoreGuidelines/pull/2252', - // 'https://github.com/isocpp/CppCoreGuidelines/issues/2249', - // 'https://github.com/isocpp/CppCoreGuidelines/issues/2238', - // 'https://github.com/isocpp/CppCoreGuidelines/issues/2225', - // 'https://github.com/isocpp/CppCoreGuidelines/issues/2255', - ] - // WIP: split suspicious, legit cases to test against false-positives + static cases = { + + legits: [ + 'https://github.com/isocpp/CppCoreGuidelines/pull/2258', + 'https://github.com/isocpp/CppCoreGuidelines/pull/2259' + ], + spams: [ + 'https://github.com/isocpp/CppCoreGuidelines/pull/2257', + 'https://github.com/isocpp/CppCoreGuidelines/pull/2241', + 'https://github.com/isocpp/CppCoreGuidelines/pull/2254', + 'https://github.com/isocpp/CppCoreGuidelines/pull/2252', + 'https://github.com/isocpp/CppCoreGuidelines/issues/2249', + 'https://github.com/isocpp/CppCoreGuidelines/issues/2238', + 'https://github.com/isocpp/CppCoreGuidelines/issues/2225', + 'https://github.com/isocpp/CppCoreGuidelines/issues/2255', + ] + } } +module.exports = async ({ github, context }) => { + + if (context.eventName === 'workflow_dispatch'){ + + console.log('Testing enabled') + + Testing.enabled = true; + configuration = { + label_if_suspicious: false, + comment_if_suspicious: false, + close_if_suspicious: false, + suspicious_criteria_tolerated: 0 + } + + // IDEA: run N-by-N to limit memory bloat + await Promise.all( + Testing.cases.spams.map((url) => Testing.getContext({ url, github })) + ).then(async (testing_contexts) => { + await run({ github, context, core }) + }); + + return; + } -module.exports = async ({ github, context, core }) => { - - if (! Testing.enabled) - return await run({ github, context, core }); - - console.log('>>> DEBUG: context', context) - - // IDEA: run N-by-N to limit memory bloat - await Promise.all( - Testing.cases.map((url) => Testing.getContext({ url, github })) - ).then(async (testing_contexts) => { - console.log( - "Actors:", - testing_contexts[0] - // testing_contexts.map((context) => { - // return { - // login: context.user.login, - // id: context.user.id - // } - // }) - ) - - // WIP: homogeneous context ? or run call ? - // await run({ github, context, core }) - // WIP: is_suspicious - }) - ; + return await run({ github, context, core }); }; /* +WIP: A bit more experiments: made some changes so such a CI can run on an arbitrary issue/PR URL, so it's easier to test against a list of. See https://github.com/GuillaumeDua/CppCoreGuidelines/actions/runs/13616115173/job/38059374996#step:3:1547 -*/ \ No newline at end of file +*/ From 265fcc1720b5106fab10ab1b0012e5bc2ac85895 Mon Sep 17 00:00:00 2001 From: GuillaumeDua Date: Sun, 2 Mar 2025 21:10:14 +0100 Subject: [PATCH 20/26] [antispam] experimenting (WIP) --- .github/workflows/antispam.yml | 2 +- .github/workflows/scripts/antispam.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/antispam.yml b/.github/workflows/antispam.yml index f3e5b2bf4..733ab864b 100644 --- a/.github/workflows/antispam.yml +++ b/.github/workflows/antispam.yml @@ -18,4 +18,4 @@ jobs: with: script: | const script = require('.github/workflows/scripts/antispam.js') - await script({github, context, core}) + await script({ github, context }) diff --git a/.github/workflows/scripts/antispam.js b/.github/workflows/scripts/antispam.js index ca5844103..c1120582a 100644 --- a/.github/workflows/scripts/antispam.js +++ b/.github/workflows/scripts/antispam.js @@ -272,13 +272,13 @@ module.exports = async ({ github, context }) => { await Promise.all( Testing.cases.spams.map((url) => Testing.getContext({ url, github })) ).then(async (testing_contexts) => { - await run({ github, context, core }) + await run({ github, context }) }); return; } - return await run({ github, context, core }); + return await run({ github, context }); }; /* From 558296f74fea28f4b9486f95c54d9b2d5f2a8b0e Mon Sep 17 00:00:00 2001 From: GuillaumeDua Date: Sun, 2 Mar 2025 21:19:26 +0100 Subject: [PATCH 21/26] [antispam] experimenting (WIP) --- .github/workflows/scripts/antispam.js | 37 +++++++++++++++------------ 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/.github/workflows/scripts/antispam.js b/.github/workflows/scripts/antispam.js index c1120582a..c39088949 100644 --- a/.github/workflows/scripts/antispam.js +++ b/.github/workflows/scripts/antispam.js @@ -88,6 +88,11 @@ async function run({ github, context }) { const username = context.actor; const { data: user } = await github.rest.users.getByUsername({ username: username }); + const payload = context.payload; + const title = payload.issue?.title || payload.pull_request?.title || ""; + const body = payload.issue?.body || payload.pull_request?.body || ""; + + console.log('Checking', { user: username, title: title }) const isAuthorOnlyContributionOnGH = await (async () => { // WARNING: Depending on the time of day, event latency can be anywhere from 30s to 6h. (source: https://octokit.github.io/rest.js/v21/) @@ -112,10 +117,6 @@ async function run({ github, context }) { if (context.eventName === 'workflow_dispatch') // issues or pull_request return false; - const payload = context.payload; - const title = payload.issue?.title || payload.pull_request?.title || ""; - const body = payload.issue?.body || payload.pull_request?.body || ""; - const threshold = 20; return title.length < threshold || body.length < threshold; @@ -199,7 +200,7 @@ class Testing { }; } - static async getContext({ url, github }) { + static async #getContext({ url, github }) { const parsed_url = Testing.#parseGitHubUrl({ url: url }); if (!parsed_url) { @@ -235,7 +236,7 @@ class Testing { } } - static cases = { + static #cases = { legits: [ 'https://github.com/isocpp/CppCoreGuidelines/pull/2258', @@ -252,12 +253,8 @@ class Testing { 'https://github.com/isocpp/CppCoreGuidelines/issues/2255', ] } -} - -module.exports = async ({ github, context }) => { - - if (context.eventName === 'workflow_dispatch'){ + static async run({ github, context }){ console.log('Testing enabled') Testing.enabled = true; @@ -267,18 +264,24 @@ module.exports = async ({ github, context }) => { close_if_suspicious: false, suspicious_criteria_tolerated: 0 } - + // IDEA: run N-by-N to limit memory bloat await Promise.all( - Testing.cases.spams.map((url) => Testing.getContext({ url, github })) + Testing.#cases.spams.map((url) => Testing.#getContext({ url, github })) ).then(async (testing_contexts) => { - await run({ github, context }) + testing_contexts.forEach( + async (value) => await run({ github, value }) + ) }); - - return; } +} + +module.exports = async ({ github, context }) => { + + if (context.eventName !== 'workflow_dispatch') + return await run({ github, context }); - return await run({ github, context }); + return await Testing.run({ github, context }); }; /* From 813e81d3020ddc70f4058d1689dda3dcac494d40 Mon Sep 17 00:00:00 2001 From: GuillaumeDua Date: Sun, 2 Mar 2025 21:22:01 +0100 Subject: [PATCH 22/26] [antispam] experimenting (WIP) --- .github/workflows/scripts/antispam.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/scripts/antispam.js b/.github/workflows/scripts/antispam.js index c39088949..f54d68296 100644 --- a/.github/workflows/scripts/antispam.js +++ b/.github/workflows/scripts/antispam.js @@ -228,6 +228,7 @@ class Testing { title: response.data.title, body: response.data.body, }; + console.log(">>> DEBUG", response.data.user.login) return { actor: response.data.user.login, sender: response.data.user, @@ -270,7 +271,7 @@ class Testing { Testing.#cases.spams.map((url) => Testing.#getContext({ url, github })) ).then(async (testing_contexts) => { testing_contexts.forEach( - async (value) => await run({ github, value }) + async (value) => console.log(value)// await run({ github, value }) ) }); } From 722a89d714a43e7f03e4b8c7800e25910bc6c219 Mon Sep 17 00:00:00 2001 From: GuillaumeDua Date: Sun, 2 Mar 2025 21:24:23 +0100 Subject: [PATCH 23/26] [antispam] experimenting (WIP) --- .github/workflows/scripts/antispam.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/scripts/antispam.js b/.github/workflows/scripts/antispam.js index f54d68296..d7d307492 100644 --- a/.github/workflows/scripts/antispam.js +++ b/.github/workflows/scripts/antispam.js @@ -220,7 +220,6 @@ class Testing { throw new Error(`Failed to fetch ${type.slice(0, -1)} #${number}: ${error.message}`); } - // console.log('response.data', response.data) // return response.data; // make context adapter => could perform it upstream with graphql @@ -228,7 +227,6 @@ class Testing { title: response.data.title, body: response.data.body, }; - console.log(">>> DEBUG", response.data.user.login) return { actor: response.data.user.login, sender: response.data.user, @@ -255,7 +253,7 @@ class Testing { ] } - static async run({ github, context }){ + static async run({ github }){ console.log('Testing enabled') Testing.enabled = true; @@ -271,7 +269,7 @@ class Testing { Testing.#cases.spams.map((url) => Testing.#getContext({ url, github })) ).then(async (testing_contexts) => { testing_contexts.forEach( - async (value) => console.log(value)// await run({ github, value }) + async (value) => await run({ github, context: value }) ) }); } @@ -282,7 +280,7 @@ module.exports = async ({ github, context }) => { if (context.eventName !== 'workflow_dispatch') return await run({ github, context }); - return await Testing.run({ github, context }); + return await Testing.run({ github }); }; /* From d61ce3ba2cb832717c9ba30b24f6903a702538ce Mon Sep 17 00:00:00 2001 From: GuillaumeDua Date: Sun, 2 Mar 2025 21:26:55 +0100 Subject: [PATCH 24/26] [antispam] experimenting (WIP) --- .github/workflows/scripts/antispam.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/scripts/antispam.js b/.github/workflows/scripts/antispam.js index d7d307492..42440da71 100644 --- a/.github/workflows/scripts/antispam.js +++ b/.github/workflows/scripts/antispam.js @@ -231,7 +231,8 @@ class Testing { actor: response.data.user.login, sender: response.data.user, eventName: type, - payload: type === 'issues' ? { issue: payload_content } : { pull_request: payload_content } + payload: type === 'issues' ? { issue: payload_content } : { pull_request: payload_content }, + repo: response.data.repo } } From a733faca89bf24b7bf3b7cc648928551270ee620 Mon Sep 17 00:00:00 2001 From: GuillaumeDua Date: Sun, 2 Mar 2025 21:28:40 +0100 Subject: [PATCH 25/26] [antispam] experimenting (WIP) --- .github/workflows/scripts/antispam.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/scripts/antispam.js b/.github/workflows/scripts/antispam.js index 42440da71..ad0e64f53 100644 --- a/.github/workflows/scripts/antispam.js +++ b/.github/workflows/scripts/antispam.js @@ -11,12 +11,12 @@ async function make_information_report({ user }) { > > | information | value | > | ----------- | ----- | -> | email | ${user.email} | -> | login | ${user.login} | -> | name | ${user.name} | -> | location | ${user.location} | -> | blog | ${user.blog} | -> | location | ${user.location} | +> | email | ${user.email || '' } | +> | login | ${user.login || '' } | +> | name | ${user.name || '' } | +> | location | ${user.location || '' } | +> | blog | ${user.blog || '' } | +> | location | ${user.location || '' } | ` } From 6343bb32c2395482fbd9f79e1a8a01aaa7d0aa57 Mon Sep 17 00:00:00 2001 From: GuillaumeDua Date: Sun, 2 Mar 2025 21:43:25 +0100 Subject: [PATCH 26/26] [antispam] experimenting (WIP) --- .github/workflows/scripts/antispam.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/.github/workflows/scripts/antispam.js b/.github/workflows/scripts/antispam.js index ad0e64f53..c7b02985c 100644 --- a/.github/workflows/scripts/antispam.js +++ b/.github/workflows/scripts/antispam.js @@ -88,11 +88,17 @@ async function run({ github, context }) { const username = context.actor; const { data: user } = await github.rest.users.getByUsername({ username: username }); + const payload = context.payload; - const title = payload.issue?.title || payload.pull_request?.title || ""; - const body = payload.issue?.body || payload.pull_request?.body || ""; - console.log('Checking', { user: username, title: title }) + const issue_or_pr = (() => { + if (payload.issue) return payload.issue; + if (payload.pull_request) return payload.pull_request; + throw new Error("Only supports issues and PRs") + })(); + + + console.log('Checking', { user: username, title: issue_or_pr.title }) const isAuthorOnlyContributionOnGH = await (async () => { // WARNING: Depending on the time of day, event latency can be anywhere from 30s to 6h. (source: https://octokit.github.io/rest.js/v21/) @@ -105,7 +111,7 @@ async function run({ github, context }) { const WasAuthorRecentlyCreated = (() => { const time_point = (() => { - let value = new Date(); + let value = Date.parse(issue_or_pr.created_at); //new Date(); value.setHours(value.getHours() - 2); return value; })(); @@ -118,8 +124,8 @@ async function run({ github, context }) { return false; const threshold = 20; - return title.length < threshold - || body.length < threshold; + return issue_or_pr.length < threshold + || issue_or_pr.length < threshold; })(); const checks = [ @@ -226,6 +232,7 @@ class Testing { const payload_content = { title: response.data.title, body: response.data.body, + created_at: new Date() // now }; return { actor: response.data.user.login,