mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-07 08:31:00 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bba26f860 | ||
|
|
281f55a99a |
No files matched your search
@@ -4,7 +4,7 @@ on:
|
||||
issues:
|
||||
types: [opened, reopened, edited]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
types: [created, edited]
|
||||
|
||||
permissions: {}
|
||||
|
||||
@@ -24,14 +24,48 @@ jobs:
|
||||
echo "Grok Bot webhook secrets not set; skipping."
|
||||
exit 0
|
||||
fi
|
||||
jq --arg event "$GITHUB_EVENT_NAME" '{
|
||||
event: $event,
|
||||
action: .action,
|
||||
repository: .repository.full_name,
|
||||
sender: .sender.login,
|
||||
issue: {number: .issue.number, title: .issue.title, author: .issue.user.login, url: .issue.html_url},
|
||||
comment: (if .comment then {id: .comment.id, author: .comment.user.login, url: .comment.html_url} else null end)
|
||||
}' "$GITHUB_EVENT_PATH" \
|
||||
jq --arg event "$GITHUB_EVENT_NAME" '
|
||||
def clip:
|
||||
if type == "string" then .[0:4000] else . end;
|
||||
{
|
||||
event: $event,
|
||||
action: .action,
|
||||
repository: .repository.full_name,
|
||||
sender: .sender.login,
|
||||
issue: (
|
||||
{
|
||||
number: .issue.number,
|
||||
title: .issue.title,
|
||||
author: .issue.user.login,
|
||||
url: .issue.html_url
|
||||
} + (
|
||||
if $event == "issues" then {
|
||||
author_association: .issue.author_association,
|
||||
body: (.issue.body | clip)
|
||||
} else {} end
|
||||
)
|
||||
),
|
||||
comment: (
|
||||
if .comment then
|
||||
{
|
||||
id: .comment.id,
|
||||
author: .comment.user.login,
|
||||
url: .comment.html_url,
|
||||
node_id: .comment.node_id,
|
||||
author_association: .comment.author_association,
|
||||
user_type: .comment.user.type,
|
||||
created_at: .comment.created_at,
|
||||
updated_at: .comment.updated_at,
|
||||
body: (.comment.body | clip)
|
||||
} + (
|
||||
if .action == "edited" then {
|
||||
previous_body: (.changes.body.from | clip)
|
||||
} else {} end
|
||||
)
|
||||
else null end
|
||||
)
|
||||
}
|
||||
' "$GITHUB_EVENT_PATH" \
|
||||
| curl -fsS --max-time 20 -X POST \
|
||||
-H "Authorization: Bearer $WEBHOOK_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
|
||||
@@ -2,7 +2,7 @@ name: spam-comments
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
types: [created, edited]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -10,13 +10,13 @@ permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
delete-spam:
|
||||
hide-spam:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout filter rules
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Delete suspected malware spam
|
||||
- name: Hide suspected malware spam
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
@@ -25,26 +25,59 @@ jobs:
|
||||
path.join(process.env.GITHUB_WORKSPACE, 'scripts/spam-comment-filter.cjs')
|
||||
);
|
||||
|
||||
const comment = context.payload.comment;
|
||||
const comment = context.payload.comment || {};
|
||||
const issue = context.payload.issue || {};
|
||||
const login = (comment.user && comment.user.login) || 'unknown';
|
||||
const result = detectSpamComment({
|
||||
body: comment.body || '',
|
||||
authorAssociation: comment.author_association,
|
||||
userType: comment.user && comment.user.type,
|
||||
commentAuthor: comment.user && comment.user.login,
|
||||
issueAuthor: issue.user && issue.user.login,
|
||||
});
|
||||
|
||||
core.info(JSON.stringify(result, null, 2));
|
||||
|
||||
if (!result.spam) {
|
||||
core.info('Comment did not match the zip/dangerous-file spam filter.');
|
||||
core.info('Comment did not match the risky-download spam filter.');
|
||||
return;
|
||||
}
|
||||
|
||||
await github.rest.issues.deleteComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: comment.id,
|
||||
});
|
||||
const matchedUrl = result.matchedUrls.join(', ');
|
||||
if (!comment.node_id) {
|
||||
core.setFailed(
|
||||
`Comment ${comment.id} by @${login} on #${issue.number} matched ${matchedUrl} but has no node_id.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await github.graphql(
|
||||
`mutation($input: MinimizeCommentInput!) {
|
||||
minimizeComment(input: $input) {
|
||||
minimizedComment {
|
||||
isMinimized
|
||||
}
|
||||
}
|
||||
}`,
|
||||
{
|
||||
input: {
|
||||
subjectId: comment.node_id,
|
||||
classifier: 'SPAM',
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
const message = String((error && error.message) || error);
|
||||
if (/already minimized/i.test(message)) {
|
||||
core.info(
|
||||
`Comment ${comment.id} by @${login} on #${issue.number} is already minimized. Matched URL: ${matchedUrl}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
core.notice(
|
||||
`Deleted comment with zip/dangerous attachment by @${comment.user.login} on #${context.payload.issue.number}: ${result.reasons.join('; ')}`
|
||||
`Hid comment ${comment.id} by @${login} on #${issue.number} as spam. Matched URL: ${matchedUrl}`
|
||||
);
|
||||
+103
-50
@@ -6,21 +6,32 @@ const TRUSTED_AUTHOR_ASSOCIATIONS = new Set([
|
||||
"COLLABORATOR",
|
||||
]);
|
||||
|
||||
const DANGEROUS_FILE_EXTENSION =
|
||||
"(?:zip|7z|rar|tar\\.gz|tgz|exe|msi|dmg|pkg|deb|rpm|appimage|bat|cmd|ps1|scr|vbs)";
|
||||
const RISKY_EXTENSIONS = [
|
||||
".tar.gz",
|
||||
".tar.xz",
|
||||
".appimage",
|
||||
".msix",
|
||||
".zip",
|
||||
".7z",
|
||||
".rar",
|
||||
".tgz",
|
||||
".exe",
|
||||
".msi",
|
||||
".dmg",
|
||||
".pkg",
|
||||
".deb",
|
||||
".rpm",
|
||||
".bat",
|
||||
".cmd",
|
||||
".ps1",
|
||||
".scr",
|
||||
".vbs",
|
||||
".jar",
|
||||
".iso",
|
||||
".lnk",
|
||||
];
|
||||
|
||||
const dangerousFilePattern = new RegExp(
|
||||
`(?:^|[\\s([<"'=])([^\\s()[\\]<>\"']+\\.${DANGEROUS_FILE_EXTENSION})(?=$|[\\s)\\]>\"']|[.,!?;:](?:$|\\s))`,
|
||||
"gi"
|
||||
);
|
||||
|
||||
const zipFilePattern = new RegExp(
|
||||
`(?:^|[\\s([<"'=])([^\\s()[\\]<>\"']+\\.zip)(?=$|[\\s)\\]>\"']|[.,!?;:](?:$|\\s))`,
|
||||
"gi"
|
||||
);
|
||||
|
||||
const githubUserAttachmentPattern =
|
||||
/^https:\/\/github\.com\/user-attachments\/files\/\d+\//i;
|
||||
const URL_PATTERN = /https?:\/\/[^\s<>"'`]+/gi;
|
||||
|
||||
function normalizeAssociation(authorAssociation) {
|
||||
return String(authorAssociation || "").trim().toUpperCase();
|
||||
@@ -30,65 +41,107 @@ function isTrustedAuthor(authorAssociation) {
|
||||
return TRUSTED_AUTHOR_ASSOCIATIONS.has(normalizeAssociation(authorAssociation));
|
||||
}
|
||||
|
||||
function extractMatches(body, pattern) {
|
||||
const files = [];
|
||||
for (const match of body.matchAll(pattern)) {
|
||||
files.push(match[1]);
|
||||
function isSameLogin(left, right) {
|
||||
const a = String(left || "").trim().toLowerCase();
|
||||
const b = String(right || "").trim().toLowerCase();
|
||||
return a.length > 0 && a === b;
|
||||
}
|
||||
|
||||
function trimUrlCandidate(raw) {
|
||||
let value = String(raw || "");
|
||||
while (value.length > 0) {
|
||||
const last = value[value.length - 1];
|
||||
if (".,;:!?\"']".includes(last)) {
|
||||
value = value.slice(0, -1);
|
||||
continue;
|
||||
}
|
||||
if (last === ")") {
|
||||
const opens = (value.match(/\(/g) || []).length;
|
||||
const closes = (value.match(/\)/g) || []).length;
|
||||
if (closes > opens) {
|
||||
value = value.slice(0, -1);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
return [...new Set(files)];
|
||||
return value;
|
||||
}
|
||||
|
||||
function extractDangerousFiles(body) {
|
||||
return extractMatches(body, dangerousFilePattern);
|
||||
function hasRiskyExtension(segment) {
|
||||
const name = String(segment || "").toLowerCase();
|
||||
if (!name || name === "." || name === "..") return false;
|
||||
return RISKY_EXTENSIONS.some((extension) => name.endsWith(extension));
|
||||
}
|
||||
|
||||
function extractZipFiles(body) {
|
||||
return extractMatches(body, zipFilePattern);
|
||||
function urlHasRiskyPath(raw) {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(raw);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
|
||||
|
||||
let pathname = parsed.pathname || "";
|
||||
try {
|
||||
pathname = decodeURIComponent(pathname);
|
||||
} catch {
|
||||
// Keep the raw pathname when it is not valid percent-encoding.
|
||||
}
|
||||
|
||||
return pathname.split("/").some((segment) => hasRiskyExtension(segment));
|
||||
}
|
||||
|
||||
function isGitHubUserAttachment(file) {
|
||||
return githubUserAttachmentPattern.test(file);
|
||||
function extractRiskyUrls(body) {
|
||||
const text = String(body || "");
|
||||
const found = [];
|
||||
const seen = new Set();
|
||||
for (const match of text.matchAll(URL_PATTERN)) {
|
||||
const raw = trimUrlCandidate(match[0]);
|
||||
if (!raw || seen.has(raw) || !urlHasRiskyPath(raw)) continue;
|
||||
seen.add(raw);
|
||||
found.push(raw);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
function detectSpamComment({ body, authorAssociation, userType } = {}) {
|
||||
const normalizedBody = String(body || "").replace(/\s+/g, " ").trim();
|
||||
const dangerousFiles = extractDangerousFiles(normalizedBody);
|
||||
const zipFiles = extractZipFiles(normalizedBody);
|
||||
function detectSpamComment({
|
||||
body,
|
||||
authorAssociation,
|
||||
userType,
|
||||
commentAuthor,
|
||||
issueAuthor,
|
||||
} = {}) {
|
||||
const trustedAuthor = isTrustedAuthor(authorAssociation);
|
||||
const botAuthor = String(userType || "").toLowerCase() === "bot";
|
||||
const botAuthor = String(userType || "").trim().toLowerCase() === "bot";
|
||||
const authorIsIssueAuthor = isSameLogin(commentAuthor, issueAuthor);
|
||||
const matchedUrls = extractRiskyUrls(body);
|
||||
|
||||
const reasons = [];
|
||||
let score = 0;
|
||||
|
||||
if (zipFiles.length > 0) {
|
||||
score += 10;
|
||||
reasons.push(`zip attachment or link: ${zipFiles.join(", ")}`);
|
||||
} else if (dangerousFiles.length > 0) {
|
||||
score += 10;
|
||||
reasons.push(`dangerous downloadable file: ${dangerousFiles.join(", ")}`);
|
||||
}
|
||||
|
||||
// Hard rule: any zip (or other dangerous downloadable) from an untrusted
|
||||
// human is deleted. Maintainers and bots are exempt.
|
||||
// Hard rule: hide when an untrusted human who is not the issue author posts
|
||||
// an http(s) URL whose path contains a risky archive or executable.
|
||||
// Maintainers, bots, and the issue author are exempt. Bare filenames and
|
||||
// local paths are not links, so they do not match.
|
||||
const spam =
|
||||
!trustedAuthor &&
|
||||
!botAuthor &&
|
||||
(zipFiles.length > 0 || dangerousFiles.length > 0);
|
||||
!authorIsIssueAuthor &&
|
||||
matchedUrls.length > 0;
|
||||
|
||||
return {
|
||||
spam,
|
||||
score: spam ? score : 0,
|
||||
reasons: spam ? reasons : [],
|
||||
dangerousFiles: zipFiles.length > 0 ? zipFiles : dangerousFiles,
|
||||
score: spam ? 10 : 0,
|
||||
reasons: spam ? [`risky download URL: ${matchedUrls.join(", ")}`] : [],
|
||||
matchedUrls,
|
||||
trustedAuthor,
|
||||
botAuthor,
|
||||
authorIsIssueAuthor,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
detectSpamComment,
|
||||
extractDangerousFiles,
|
||||
extractZipFiles,
|
||||
isGitHubUserAttachment,
|
||||
extractRiskyUrls,
|
||||
isTrustedAuthor,
|
||||
RISKY_EXTENSIONS,
|
||||
};
|
||||
@@ -2,113 +2,164 @@
|
||||
|
||||
const assert = require("node:assert/strict");
|
||||
const test = require("node:test");
|
||||
const {
|
||||
detectSpamComment,
|
||||
extractDangerousFiles,
|
||||
extractZipFiles,
|
||||
isGitHubUserAttachment,
|
||||
} = require("./spam-comment-filter.cjs");
|
||||
const { detectSpamComment, extractRiskyUrls } = require("./spam-comment-filter.cjs");
|
||||
|
||||
test("flags the fake Netcatty patch spam pattern", () => {
|
||||
test("issue author pasting a local SecureCRT.exe path is not hidden", () => {
|
||||
const result = detectSpamComment({
|
||||
authorAssociation: "NONE",
|
||||
userType: "User",
|
||||
body: "[netcatty_patch.zip](https://example.com/netcatty_patch.zip)\n\nMan, that terminal rendering bug is such a pain. It looks like the sftp module is tripping over the encoding when it tries to sync the current path. I found a quick patch that fixes the character mapping in the backend so those black boxes finally disappear.",
|
||||
});
|
||||
|
||||
assert.equal(result.spam, true);
|
||||
});
|
||||
|
||||
test("flags fake patch spam when the filename ends a sentence", () => {
|
||||
const result = detectSpamComment({
|
||||
authorAssociation: "NONE",
|
||||
userType: "User",
|
||||
body: "Download this patch.zip. It fixes the backend encoding so the terminal rendering black boxes disappear.",
|
||||
});
|
||||
|
||||
assert.equal(result.spam, true);
|
||||
assert.deepEqual(result.dangerousFiles, ["patch.zip"]);
|
||||
});
|
||||
|
||||
test("flags any zip from an outside user, including hash-named soft bait", () => {
|
||||
const result = detectSpamComment({
|
||||
authorAssociation: "NONE",
|
||||
userType: "User",
|
||||
body: "[63bf1862b52422e38b8cb170.zip](https://github.com/user-attachments/files/29784176/63bf1862b52422e38b8cb170.zip)\nI'd start with the docs inside",
|
||||
});
|
||||
|
||||
assert.equal(result.spam, true);
|
||||
assert.ok(
|
||||
result.dangerousFiles.some((file) => file.includes("63bf1862b52422e38b8cb170.zip"))
|
||||
);
|
||||
});
|
||||
|
||||
test("flags ordinary zip log attachments from outside users", () => {
|
||||
const result = detectSpamComment({
|
||||
authorAssociation: "FIRST_TIME_CONTRIBUTOR",
|
||||
userType: "User",
|
||||
body: "[debug-logs.zip](https://github.com/user-attachments/files/29784176/debug-logs.zip)\nI attached logs from a failed connection. The app hangs after I click connect, and the logs show the SSH handshake timing out.",
|
||||
});
|
||||
|
||||
assert.equal(result.spam, true);
|
||||
});
|
||||
|
||||
test("flags other dangerous archives from outside users", () => {
|
||||
const result = detectSpamComment({
|
||||
authorAssociation: "NONE",
|
||||
userType: "User",
|
||||
body: "See hotfix.dmg for the workaround.",
|
||||
});
|
||||
|
||||
assert.equal(result.spam, true);
|
||||
assert.deepEqual(result.dangerousFiles, ["hotfix.dmg"]);
|
||||
});
|
||||
|
||||
test("does not flag trusted maintainers even when sharing zip archives", () => {
|
||||
const result = detectSpamComment({
|
||||
authorAssociation: "OWNER",
|
||||
userType: "User",
|
||||
body: "Try this temporary netcatty_patch.zip while I prepare the signed release. It fixes the rendering issue.",
|
||||
commentAuthor: "reporter",
|
||||
issueAuthor: "reporter",
|
||||
body: "It crashes here: C:\\Program Files\\SecureCRT\\SecureCRT.exe",
|
||||
});
|
||||
|
||||
assert.equal(result.spam, false);
|
||||
assert.deepEqual(result.matchedUrls, []);
|
||||
});
|
||||
|
||||
test("does not flag bot comments that mention zip files", () => {
|
||||
const result = detectSpamComment({
|
||||
authorAssociation: "NONE",
|
||||
userType: "Bot",
|
||||
body: "Uploaded build-artifacts.zip for CI.",
|
||||
});
|
||||
|
||||
assert.equal(result.spam, false);
|
||||
});
|
||||
|
||||
test("does not flag comments without downloadable archives", () => {
|
||||
test("bare Windows path without a URL is not hidden even from a third party", () => {
|
||||
const result = detectSpamComment({
|
||||
authorAssociation: "NONE",
|
||||
userType: "User",
|
||||
body: "I attached screenshots of the hang after connect. The SSH handshake times out.",
|
||||
commentAuthor: "mallory",
|
||||
issueAuthor: "reporter",
|
||||
body: "Try Files\\SecureCRT\\SecureCRT.exe and also hotfix.dmg.",
|
||||
});
|
||||
|
||||
assert.equal(result.spam, false);
|
||||
assert.deepEqual(result.matchedUrls, []);
|
||||
});
|
||||
|
||||
test("extracts risky file names from markdown links and plain text", () => {
|
||||
assert.deepEqual(
|
||||
extractDangerousFiles("[fix.zip](https://example.com/fix.zip) also hotfix.dmg."),
|
||||
["fix.zip", "https://example.com/fix.zip", "hotfix.dmg"]
|
||||
);
|
||||
assert.deepEqual(extractZipFiles("[fix.zip](https://example.com/fix.zip) also hotfix.dmg."), [
|
||||
"fix.zip",
|
||||
"https://example.com/fix.zip",
|
||||
test("third party github user-attachments zip URL is hidden", () => {
|
||||
const result = detectSpamComment({
|
||||
authorAssociation: "NONE",
|
||||
userType: "User",
|
||||
commentAuthor: "mallory",
|
||||
issueAuthor: "reporter",
|
||||
body: "Grab https://github.com/user-attachments/files/123/fix.zip and run it.",
|
||||
});
|
||||
|
||||
assert.equal(result.spam, true);
|
||||
assert.deepEqual(result.matchedUrls, [
|
||||
"https://github.com/user-attachments/files/123/fix.zip",
|
||||
]);
|
||||
});
|
||||
|
||||
test("identifies GitHub user attachment URLs", () => {
|
||||
assert.equal(
|
||||
isGitHubUserAttachment("https://github.com/user-attachments/files/29784176/netcatty_fix.zip"),
|
||||
true
|
||||
test("third party mediafire zip URL is hidden when the extension is a path segment", () => {
|
||||
const result = detectSpamComment({
|
||||
authorAssociation: "NONE",
|
||||
userType: "User",
|
||||
commentAuthor: "mallory",
|
||||
issueAuthor: "reporter",
|
||||
body: "Download https://www.mediafire.com/file/abc/netcatty_fix.zip/file",
|
||||
});
|
||||
|
||||
assert.equal(result.spam, true);
|
||||
assert.deepEqual(result.matchedUrls, [
|
||||
"https://www.mediafire.com/file/abc/netcatty_fix.zip/file",
|
||||
]);
|
||||
});
|
||||
|
||||
test("maintainer posting a release dmg link is not hidden", () => {
|
||||
const result = detectSpamComment({
|
||||
authorAssociation: "OWNER",
|
||||
userType: "User",
|
||||
commentAuthor: "maintainer",
|
||||
issueAuthor: "reporter",
|
||||
body: "Signed build: https://github.com/binaricat/Netcatty/releases/download/v1.2.3/Netcatty.dmg",
|
||||
});
|
||||
|
||||
assert.equal(result.spam, false);
|
||||
assert.equal(result.trustedAuthor, true);
|
||||
assert.deepEqual(result.matchedUrls, [
|
||||
"https://github.com/binaricat/Netcatty/releases/download/v1.2.3/Netcatty.dmg",
|
||||
]);
|
||||
});
|
||||
|
||||
test("issue author uploading their own repro zip is not hidden", () => {
|
||||
const result = detectSpamComment({
|
||||
authorAssociation: "NONE",
|
||||
userType: "User",
|
||||
commentAuthor: "Reporter",
|
||||
issueAuthor: "reporter",
|
||||
body: "[repro.zip](https://github.com/user-attachments/files/99/repro.zip)",
|
||||
});
|
||||
|
||||
assert.equal(result.spam, false);
|
||||
assert.equal(result.authorIsIssueAuthor, true);
|
||||
assert.deepEqual(result.matchedUrls, [
|
||||
"https://github.com/user-attachments/files/99/repro.zip",
|
||||
]);
|
||||
});
|
||||
|
||||
test("query strings do not block a risky path, and a query-only filename does not match", () => {
|
||||
assert.deepEqual(
|
||||
extractRiskyUrls("https://example.com/dropper.exe?download=1&arch=win"),
|
||||
["https://example.com/dropper.exe?download=1&arch=win"]
|
||||
);
|
||||
assert.equal(isGitHubUserAttachment("https://example.com/netcatty_fix.zip"), false);
|
||||
assert.deepEqual(
|
||||
extractRiskyUrls("https://example.com/download?file=fix.zip"),
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
test("matches newly added extensions and ignores non-risky links", () => {
|
||||
const body = [
|
||||
"https://example.com/a.jar",
|
||||
"https://example.com/b.iso",
|
||||
"https://example.com/c.msix",
|
||||
"https://example.com/d.lnk",
|
||||
"https://example.com/e.tar.xz",
|
||||
"https://example.com/f.tar.gz",
|
||||
"https://github.com/user-attachments/assets/e1302660-dece-46be-9857-1678a9b13d00",
|
||||
"https://example.com/package.json",
|
||||
"https://example.com/notes.zip.txt",
|
||||
].join("\n");
|
||||
|
||||
assert.deepEqual(extractRiskyUrls(body), [
|
||||
"https://example.com/a.jar",
|
||||
"https://example.com/b.iso",
|
||||
"https://example.com/c.msix",
|
||||
"https://example.com/d.lnk",
|
||||
"https://example.com/e.tar.xz",
|
||||
"https://example.com/f.tar.gz",
|
||||
]);
|
||||
});
|
||||
|
||||
test("member and collaborator exemptions still apply to http links", () => {
|
||||
for (const authorAssociation of ["MEMBER", "COLLABORATOR"]) {
|
||||
const result = detectSpamComment({
|
||||
authorAssociation,
|
||||
userType: "User",
|
||||
commentAuthor: "maintainer",
|
||||
issueAuthor: "reporter",
|
||||
body: "Mirror: http://example.com/setup.msi",
|
||||
});
|
||||
assert.equal(result.spam, false);
|
||||
assert.deepEqual(result.matchedUrls, ["http://example.com/setup.msi"]);
|
||||
}
|
||||
});
|
||||
|
||||
test("does not flag bot comments that include a risky URL", () => {
|
||||
const result = detectSpamComment({
|
||||
authorAssociation: "NONE",
|
||||
userType: "Bot",
|
||||
commentAuthor: "netcatty[bot]",
|
||||
issueAuthor: "reporter",
|
||||
body: "Uploaded https://example.com/build-artifacts.zip for CI.",
|
||||
});
|
||||
|
||||
assert.equal(result.spam, false);
|
||||
assert.equal(result.botAuthor, true);
|
||||
});
|
||||
|
||||
test("strips markdown and sentence punctuation around the URL", () => {
|
||||
assert.deepEqual(
|
||||
extractRiskyUrls("See [patch](https://example.com/patch.zip). Thanks!"),
|
||||
["https://example.com/patch.zip"]
|
||||
);
|
||||
assert.deepEqual(
|
||||
extractRiskyUrls("<https://example.com/tool.EXE>"),
|
||||
["https://example.com/tool.EXE"]
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user