mirror of
https://github.com/acmesh-official/acme.sh.git
synced 2026-10-21 13:31:18 +00:00
Merge pull request #7091 from acmesh-official/dev
wiki-guard: trust repo/org members with write access in all rule checks
This commit is contained in:
2 files changed
+301
-38
No files matched your search
@@ -2,6 +2,12 @@ name: "Update issues"
|
|||||||
on:
|
on:
|
||||||
issues:
|
issues:
|
||||||
types: [opened]
|
types: [opened]
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
comment:
|
comment:
|
||||||
@@ -10,6 +16,46 @@ jobs:
|
|||||||
- uses: actions/github-script@v9
|
- uses: actions/github-script@v9
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
|
const item = context.payload.issue || context.payload.pull_request;
|
||||||
|
|
||||||
|
// Close on sight anything opened by a user on the wiki Blacklist
|
||||||
|
// page (maintained by the Wiki Guard workflow).
|
||||||
|
let blacklist = [];
|
||||||
|
try {
|
||||||
|
const res = await fetch(`https://raw.githubusercontent.com/wiki/${context.repo.owner}/${context.repo.repo}/Blacklist.md`);
|
||||||
|
if (res.ok) {
|
||||||
|
blacklist = (await res.text()).split("\n")
|
||||||
|
.filter(l => l.startsWith("- "))
|
||||||
|
.map(l => l.slice(2).trim().toLowerCase())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
core.warning(`Failed to fetch the blacklist: ${e}`);
|
||||||
|
}
|
||||||
|
if (blacklist.includes(item.user.login.toLowerCase())) {
|
||||||
|
if (context.payload.pull_request) {
|
||||||
|
await github.rest.pulls.update({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
pull_number: item.number,
|
||||||
|
state: "closed"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await github.rest.issues.update({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: item.number,
|
||||||
|
state: "closed",
|
||||||
|
state_reason: "not_planned"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.payload.pull_request) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const issue = context.payload.issue;
|
const issue = context.payload.issue;
|
||||||
if (issue.title.startsWith("Report bugs to")) {
|
if (issue.title.startsWith("Report bugs to")) {
|
||||||
// Tracking issue for a third-party dns/deploy/notify api:
|
// Tracking issue for a third-party dns/deploy/notify api:
|
||||||
|
|||||||
@@ -1,7 +1,19 @@
|
|||||||
name: Restore Wiki Pages Deleted by Others
|
name: Wiki Guard
|
||||||
|
|
||||||
|
# Rules enforced here:
|
||||||
|
# - Only the maintainer and write-access members may delete or rename wiki
|
||||||
|
# pages. Anyone else doing so gets blacklisted and the page restored to
|
||||||
|
# its last good revision.
|
||||||
|
# - Only the maintainer and write-access members may edit the Blacklist
|
||||||
|
# wiki page. Anyone else touching it gets blacklisted and the page
|
||||||
|
# reverted.
|
||||||
|
# - Any wiki change made by a blacklisted identity is reverted.
|
||||||
|
# A "good" revision is one authored by the maintainer, by this bot, or by
|
||||||
|
# a non-blacklisted user -- restoring from the deleted commit's parent is
|
||||||
|
# NOT safe, because vandals replace a page before destroying it and the
|
||||||
|
# parent would launder their version into a bot commit.
|
||||||
# The gollum event only fires on page create/update, never on deletion,
|
# The gollum event only fires on page create/update, never on deletion,
|
||||||
# so deletions can only be caught by polling the wiki git history.
|
# so violations are caught by polling the wiki git history.
|
||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
@@ -18,7 +30,7 @@ concurrency:
|
|||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
restore:
|
guard:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout wiki repository
|
- name: Checkout wiki repository
|
||||||
@@ -28,68 +40,273 @@ jobs:
|
|||||||
path: wiki
|
path: wiki
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Restore pages deleted by non-maintainer
|
- name: Enforce wiki rules
|
||||||
id: restore
|
id: guard
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
|
# Logins with write (push) access to the repository, including
|
||||||
|
# organization members -- they may delete/rename pages and edit
|
||||||
|
# the blacklist just like the maintainer. If the API call fails,
|
||||||
|
# the list stays empty and enforcement falls back to
|
||||||
|
# maintainer-only, which is the safe direction.
|
||||||
|
gh api "repos/${GITHUB_REPOSITORY}/collaborators?per_page=100" --paginate \
|
||||||
|
-q '.[] | select(.permissions.push) | .login' 2>/dev/null \
|
||||||
|
| tr 'A-Z' 'a-z' | sort -u > writers.txt || true
|
||||||
cd wiki
|
cd wiki
|
||||||
git config core.quotePath false
|
git config core.quotePath false
|
||||||
|
|
||||||
# Any author email under this domain is the maintainer and may delete pages.
|
# Any author email under this domain is the maintainer.
|
||||||
OWNER_DOMAIN="neilpang.com"
|
OWNER_DOMAIN="neilpang.com"
|
||||||
|
# Our own enforcement commits.
|
||||||
|
BOT_EMAIL="41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
BL_PAGE="Blacklist.md"
|
||||||
|
# Rolling window; the cron runs every 10 minutes, so 7 days gives
|
||||||
|
# ample overlap without re-judging old changes the maintainer
|
||||||
|
# already accepted.
|
||||||
|
WINDOW="7 days ago"
|
||||||
|
|
||||||
: > ../restored.txt
|
: > ../actions.txt
|
||||||
|
: > ../bl_new.txt
|
||||||
|
|
||||||
# Paths deleted within the recent window (rolling; the cron runs
|
is_owner() {
|
||||||
# every 10 minutes, so 7 days gives ample overlap without
|
case "$1" in
|
||||||
# resurrecting old deletions the maintainer already accepted).
|
*@"$OWNER_DOMAIN") return 0 ;;
|
||||||
# --no-renames makes a rename count as a deletion of the old path,
|
esac
|
||||||
# so a page renamed by a non-maintainer is restored under its
|
return 1
|
||||||
# original name (the new name stays -- creating pages is allowed).
|
}
|
||||||
git log --since="7 days ago" --diff-filter=D --no-renames --name-only --format= \
|
|
||||||
|
is_bot() {
|
||||||
|
[ "$1" = "$BOT_EMAIL" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
author_email() {
|
||||||
|
git show -s --format=%ae "$1" | tr 'A-Z' 'a-z'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Identity of a commit author: the GitHub login when the email is a
|
||||||
|
# users.noreply.github.com address, otherwise the email itself.
|
||||||
|
identity_of() {
|
||||||
|
case "$1" in
|
||||||
|
*+*@users.noreply.github.com)
|
||||||
|
printf '%s\n' "$1" | sed 's/^[^+]*+//; s/@users\.noreply\.github\.com$//'
|
||||||
|
;;
|
||||||
|
*@users.noreply.github.com)
|
||||||
|
printf '%s\n' "$1" | sed 's/@users\.noreply\.github\.com$//'
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
printf '%s\n' "$1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
is_blacklisted() {
|
||||||
|
grep -Fxq "$1" ../bl_all.txt
|
||||||
|
}
|
||||||
|
|
||||||
|
# Trusted committers: the maintainer (by email domain), this bot,
|
||||||
|
# and anyone whose GitHub login has write access to the repo.
|
||||||
|
is_trusted() {
|
||||||
|
if is_owner "$1" || is_bot "$1"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
grep -Fxq "$(identity_of "$1")" ../writers.txt
|
||||||
|
}
|
||||||
|
|
||||||
|
# Newest commit on file $1 authored by a non-blacklisted user.
|
||||||
|
last_good_for() {
|
||||||
|
for g in $(git log --format=%H --no-renames -- "$1"); do
|
||||||
|
gae="$(author_email "$g")"
|
||||||
|
if is_trusted "$gae"; then
|
||||||
|
printf '%s\n' "$g"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
gid="$(identity_of "$gae")"
|
||||||
|
if ! is_blacklisted "$gid" && ! is_blacklisted "$gae"; then
|
||||||
|
printf '%s\n' "$g"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -e "$BL_PAGE" ]; then
|
||||||
|
page_existed=1
|
||||||
|
else
|
||||||
|
page_existed=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- 1. Last good version of the blacklist page: the newest
|
||||||
|
# revision authored by the maintainer or by this bot. Everything
|
||||||
|
# else on that page is tampering and is discarded.
|
||||||
|
bl_good_commit=""
|
||||||
|
for c in $(git log --format=%H --no-renames -- "$BL_PAGE"); do
|
||||||
|
ae="$(author_email "$c")"
|
||||||
|
if is_trusted "$ae"; then
|
||||||
|
bl_good_commit="$c"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ -n "$bl_good_commit" ] && git cat-file -e "$bl_good_commit:$BL_PAGE" 2>/dev/null; then
|
||||||
|
git show "$bl_good_commit:$BL_PAGE" > ../bl_page.txt
|
||||||
|
else
|
||||||
|
{
|
||||||
|
echo "# Blacklist"
|
||||||
|
echo ""
|
||||||
|
echo "Users listed below violated the wiki rules (deleted or renamed"
|
||||||
|
echo "pages, or tampered with this page). Their new issues and pull"
|
||||||
|
echo "requests are closed on sight and their wiki edits are reverted"
|
||||||
|
echo "automatically. Only the maintainer and write-access members"
|
||||||
|
echo "may edit this page."
|
||||||
|
echo ""
|
||||||
|
echo "To pardon a user while their violation is still inside the"
|
||||||
|
echo "scan window, replace their entry with: pardon: username"
|
||||||
|
echo ""
|
||||||
|
} > ../bl_page.txt
|
||||||
|
fi
|
||||||
|
sed -n 's/^- *//p' ../bl_page.txt | tr -d '\r' | tr 'A-Z' 'a-z' | sort -u > ../bl_good.txt
|
||||||
|
sed -n 's/^[Pp]ardon: *//p' ../bl_page.txt | tr -d '\r' | tr 'A-Z' 'a-z' | sort -u > ../bl_pardon.txt
|
||||||
|
|
||||||
|
bl_add() {
|
||||||
|
if grep -Fxq "$1" ../bl_pardon.txt; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if ! grep -Fxq "$1" ../bl_good.txt && ! grep -Fxq "$1" ../bl_new.txt; then
|
||||||
|
printf '%s\n' "$1" >> ../bl_new.txt
|
||||||
|
printf '%s\n' "- blacklisted \`$1\`: $2" >> ../actions.txt
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- 2. Blacklist everyone who deleted or renamed a page.
|
||||||
|
# --no-renames makes a rename count as a deletion of the old path.
|
||||||
|
for c in $(git log --since="$WINDOW" --diff-filter=D --no-renames --format=%H); do
|
||||||
|
ae="$(author_email "$c")"
|
||||||
|
if is_trusted "$ae"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
an="$(git show -s --format=%an "$c")"
|
||||||
|
bl_add "$(identity_of "$ae")" "deleted or renamed pages in $c ($an <$ae>)"
|
||||||
|
done
|
||||||
|
|
||||||
|
# ---- 3. Blacklist everyone else who touched the blacklist page.
|
||||||
|
# The revert of their tampering falls out of steps 5 and 6.
|
||||||
|
for c in $(git log --since="$WINDOW" --format=%H --no-renames -- "$BL_PAGE"); do
|
||||||
|
ae="$(author_email "$c")"
|
||||||
|
if is_trusted "$ae"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
an="$(git show -s --format=%an "$c")"
|
||||||
|
bl_add "$(identity_of "$ae")" "tampered with \`$BL_PAGE\` in $c ($an <$ae>)"
|
||||||
|
done
|
||||||
|
|
||||||
|
sort -u ../bl_new.txt > ../bl_new_u.txt
|
||||||
|
cat ../bl_good.txt ../bl_new_u.txt | sort -u > ../bl_all.txt
|
||||||
|
|
||||||
|
# ---- 4. Restore pages that are currently missing because a
|
||||||
|
# non-maintainer deleted them, using the last good revision.
|
||||||
|
git log --since="$WINDOW" --diff-filter=D --no-renames --name-only --format= \
|
||||||
| sort -u \
|
| sort -u \
|
||||||
| while IFS= read -r f; do
|
| while IFS= read -r f; do
|
||||||
if [ -z "$f" ] || [ -e "$f" ]; then
|
if [ -z "$f" ] || [ "$f" = "$BL_PAGE" ] || [ -e "$f" ]; then
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
# The most recent commit (in full history) that deleted this path.
|
|
||||||
del="$(git log -1 --diff-filter=D --no-renames --format=%H -- "$f")"
|
del="$(git log -1 --diff-filter=D --no-renames --format=%H -- "$f")"
|
||||||
if [ -z "$del" ]; then
|
if [ -z "$del" ]; then
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
ae="$(git show -s --format=%ae "$del" | tr 'A-Z' 'a-z')"
|
ae="$(author_email "$del")"
|
||||||
case "$ae" in
|
if is_trusted "$ae"; then
|
||||||
*@"$OWNER_DOMAIN")
|
continue
|
||||||
continue
|
fi
|
||||||
;;
|
good="$(last_good_for "$f")"
|
||||||
esac
|
if [ -n "$good" ] && git cat-file -e "$good:$f" 2>/dev/null; then
|
||||||
an="$(git show -s --format=%an "$del")"
|
git checkout "$good" -- "$f"
|
||||||
echo "Restoring: $f (deleted in $del by $an <$ae>)"
|
printf '%s\n' "- restored \`$f\` (deleted in $del) from its last good revision $good" >> ../actions.txt
|
||||||
git checkout "$del^" -- "$f"
|
fi
|
||||||
printf '%s\n' "- \`$f\` deleted in $del by $an <$ae>" >> ../restored.txt
|
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ -s ../restored.txt ]; then
|
# ---- 5. Revert every recent change made by a blacklisted
|
||||||
|
# identity: each touched file goes back to its newest revision
|
||||||
|
# authored by a non-blacklisted user; a file that has no such
|
||||||
|
# revision (they created it) is removed.
|
||||||
|
if [ -s ../bl_all.txt ]; then
|
||||||
|
for c in $(git log --since="$WINDOW" --format=%H --no-renames); do
|
||||||
|
ae="$(author_email "$c")"
|
||||||
|
if is_trusted "$ae"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
id="$(identity_of "$ae")"
|
||||||
|
if ! is_blacklisted "$id" && ! is_blacklisted "$ae"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
git show --name-only --no-renames --format= "$c" \
|
||||||
|
| while IFS= read -r f; do
|
||||||
|
if [ -z "$f" ] || [ "$f" = "$BL_PAGE" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
good="$(last_good_for "$f")"
|
||||||
|
if [ -n "$good" ] && git cat-file -e "$good:$f" 2>/dev/null; then
|
||||||
|
want="$(git rev-parse "$good:$f")"
|
||||||
|
have="$(git hash-object -- "$f" 2>/dev/null || echo missing)"
|
||||||
|
if [ "$want" != "$have" ]; then
|
||||||
|
git checkout "$good" -- "$f"
|
||||||
|
printf '%s\n' "- reverted \`$f\` to its last good revision $good (undoing change by \`$id\` in $c)" >> ../actions.txt
|
||||||
|
fi
|
||||||
|
elif [ -e "$f" ]; then
|
||||||
|
git rm -q -- "$f"
|
||||||
|
printf '%s\n' "- removed \`$f\` created by blacklisted \`$id\` in $c" >> ../actions.txt
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- 6. Regenerate the blacklist page: the last good text plus
|
||||||
|
# any newly blacklisted identities. This both reverts tampering
|
||||||
|
# and records new violators; manual edits by the maintainer are
|
||||||
|
# preserved as the new good text.
|
||||||
|
cp ../bl_page.txt ../bl_page_new.txt
|
||||||
|
if [ -s ../bl_page_new.txt ] && [ -n "$(tail -c1 ../bl_page_new.txt)" ]; then
|
||||||
|
echo >> ../bl_page_new.txt
|
||||||
|
fi
|
||||||
|
while IFS= read -r id; do
|
||||||
|
if [ -n "$id" ] && ! grep -Fxiq -- "- $id" ../bl_page_new.txt; then
|
||||||
|
printf -- '- %s\n' "$id" >> ../bl_page_new.txt
|
||||||
|
fi
|
||||||
|
done < ../bl_new_u.txt
|
||||||
|
if ! cmp -s ../bl_page_new.txt "$BL_PAGE" 2>/dev/null; then
|
||||||
|
cp ../bl_page_new.txt "$BL_PAGE"
|
||||||
|
git add -- "$BL_PAGE"
|
||||||
|
if [ -n "$page_existed" ] || [ -s ../bl_new_u.txt ]; then
|
||||||
|
printf '%s\n' "- updated \`$BL_PAGE\`" >> ../actions.txt
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- 7. Commit, push, notify.
|
||||||
|
if [ -n "$(git status --porcelain)" ]; then
|
||||||
git config user.name "github-actions[bot]"
|
git config user.name "github-actions[bot]"
|
||||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
git config user.email "$BOT_EMAIL"
|
||||||
git commit -m "Restore pages deleted by non-maintainer"
|
git commit -m "wiki-guard: restore pages and enforce blacklist"
|
||||||
git push origin HEAD || { git pull --rebase && git push origin HEAD; }
|
git push origin HEAD || { git pull --rebase && git push origin HEAD; }
|
||||||
|
fi
|
||||||
|
if [ -s ../actions.txt ]; then
|
||||||
{
|
{
|
||||||
echo "The following wiki pages were deleted or renamed by someone other than the maintainer and have been restored automatically:"
|
echo "The wiki guard handled the following rule violations:"
|
||||||
echo ""
|
echo ""
|
||||||
cat ../restored.txt
|
cat ../actions.txt
|
||||||
echo ""
|
echo ""
|
||||||
|
echo "Blacklist: https://github.com/${GITHUB_REPOSITORY}/wiki/Blacklist"
|
||||||
echo "Wiki: https://github.com/${GITHUB_REPOSITORY}/wiki"
|
echo "Wiki: https://github.com/${GITHUB_REPOSITORY}/wiki"
|
||||||
} > ../restore-msg.txt
|
} > ../guard-msg.txt
|
||||||
echo "restored=true" >> "$GITHUB_OUTPUT"
|
echo "acted=true" >> "$GITHUB_OUTPUT"
|
||||||
else
|
else
|
||||||
echo "No unauthorized deletions found."
|
echo "No rule violations found."
|
||||||
echo "restored=false" >> "$GITHUB_OUTPUT"
|
echo "acted=false" >> "$GITHUB_OUTPUT"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Create issue to notify Neilpang
|
- name: Create issue to notify Neilpang
|
||||||
if: steps.restore.outputs.restored == 'true'
|
if: steps.guard.outputs.acted == 'true'
|
||||||
uses: peter-evans/create-issue-from-file@v6
|
uses: peter-evans/create-issue-from-file@v6
|
||||||
with:
|
with:
|
||||||
title: "Wiki pages restored after unauthorized deletion"
|
title: "Wiki guard: rule violations handled"
|
||||||
content-filepath: ./restore-msg.txt
|
content-filepath: ./guard-msg.txt
|
||||||
assignees: Neilpang
|
assignees: Neilpang
|
||||||