Real changes since 1.0.0, all live-verified before this release: - Firewall: rule corpus expanded 19 -> 43 rules, real OWASP-CRS-equivalent coverage (XXE, SSRF, session fixation, Log4Shell/JNDI, scanner-tool detection, deeper SQL injection/XSS/PHP-injection). - Fixed a real bug: a quarantined file's severity badge and its content analysis score could disagree with no explanation (e.g. a benign file showing CRITICAL next to Score 0); both are now derived consistently and shown together. - ARGUS now always keeps itself updated, and can optionally do the same for every other installed plugin and theme (Settings, on by default) -- uses WordPress's own native update system, nothing custom. - Global Threat Intelligence is now opt-in, not automatic -- a single click on its own page, with an honest, specific description of exactly what's shared (an IP address, a reason code, a confidence score, a country). Previously connected automatically on activation. - New first-run Welcome screen after activation: confirms what's already protecting the site, and surfaces the few real optional choices in one place. - Dashboard: running version now visible in the header; new "IPs Tracked" and "ANIS Protections" metrics. - Full WordPress.org Plugin Directory readiness audit performed against this codebase. Two real compliance issues found and fixed (see above: Global Threat Intelligence's default, and the self-update mechanism, which is excluded from this build entirely -- WordPress.org prohibits a plugin from using any update channel other than its own, even an inert one). This release is still self-distributed, not a WordPress.org submission -- that remains a future step. Verified before publishing: this exact ZIP was installed, activated (14 admin pages loaded clean, zero PHP errors/warnings), and uninstalled (zero leftover database tables or options) in a fresh, disposable WordPress + MySQL environment. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
115 lines
4.6 KiB
Bash
Executable File
115 lines
4.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Production release packaging -- docs/design/production-distribution-and-updates.md
|
|
# sections 1-3, 16, 18. Builds the public download ZIP from a clean git
|
|
# checkout (git archive -- never the working tree, so uncommitted local
|
|
# files can never leak into a release), strips comments/whitespace
|
|
# (not obfuscation -- see strip-comments.php's own header), and audits
|
|
# the result against an explicit allowlist before packaging, failing
|
|
# closed on anything unexpected rather than silently shipping it.
|
|
#
|
|
# Usage: bin/build-release.sh [git-ref] (defaults to HEAD)
|
|
#
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
REF="${1:-HEAD}"
|
|
BUILD_DIR="$(mktemp -d)"
|
|
STAGE_DIR="${BUILD_DIR}/argus-wordpress-defence"
|
|
OUT_DIR="${REPO_ROOT}/dist"
|
|
|
|
cleanup() { rm -rf "${BUILD_DIR}"; }
|
|
trap cleanup EXIT
|
|
|
|
echo "==> Packaging ${REF} from a clean checkout (git archive, not the working tree)"
|
|
mkdir -p "${STAGE_DIR}"
|
|
git -C "${REPO_ROOT}" archive "${REF}" | tar -x -C "${STAGE_DIR}"
|
|
|
|
echo "==> Removing dev-only paths not meant for the production artifact"
|
|
rm -rf \
|
|
"${STAGE_DIR}/docs" \
|
|
"${STAGE_DIR}/tests" \
|
|
"${STAGE_DIR}/README.md" \
|
|
"${STAGE_DIR}/ARGUS_WORDPRESS_SECURITY_ARCHITECTURE.md" \
|
|
"${STAGE_DIR}/WORDPRESS_ORG_READINESS_AUDIT.md" \
|
|
"${STAGE_DIR}/.gitignore" \
|
|
"${STAGE_DIR}/bin"
|
|
|
|
# A plugin hosted on WordPress.org must rely solely on WordPress.org's own
|
|
# update channel -- never a self-update-from-external-manifest mechanism,
|
|
# even one that ships inert-by-default. class_exists( 'Argus_Update_Client' )
|
|
# guards every reference to this class elsewhere in the codebase, so removing
|
|
# the file here is sufficient; nothing else needs to change per build.
|
|
echo "==> Removing the self-update client (WordPress.org must be the only update channel)"
|
|
rm -f "${STAGE_DIR}/includes/class-argus-update-client.php"
|
|
|
|
# WordPress.org explicitly prohibits trialware -- a plugin submitted to the
|
|
# directory must stay genuinely, permanently functional with no license
|
|
# requirement. class_exists( 'Argus_License' ) guards every reference to
|
|
# this class elsewhere, so removing the file here is sufficient; the free
|
|
# build never has a trial timer at all.
|
|
echo "==> Removing the license/trial system (WordPress.org prohibits trialware)"
|
|
rm -f "${STAGE_DIR}/includes/class-argus-license.php"
|
|
|
|
echo "==> Allowlist audit -- fail closed on anything unexpected"
|
|
UNEXPECTED=0
|
|
while IFS= read -r -d '' item; do
|
|
rel="${item#"${STAGE_DIR}"/}"
|
|
case "$rel" in
|
|
argus-wordpress-defence.php|uninstall.php|readme.txt) ;;
|
|
admin|admin/*|includes|includes/*|mu-loader|mu-loader/*|assets|assets/*|languages|languages/*) ;;
|
|
*)
|
|
if [ -f "$item" ]; then
|
|
echo " UNEXPECTED FILE: ${rel}"
|
|
UNEXPECTED=1
|
|
fi
|
|
;;
|
|
esac
|
|
done < <(find "${STAGE_DIR}" -print0)
|
|
|
|
# Reject known-forbidden patterns explicitly, even inside allowlisted dirs.
|
|
if find "${STAGE_DIR}" \( -name "*.key" -o -name "*.pem" -o -name ".env*" -o -name "*.map" \) | grep -q .; then
|
|
echo " FORBIDDEN FILE TYPE found (key/pem/.env/source map)"
|
|
UNEXPECTED=1
|
|
fi
|
|
|
|
if [ "${UNEXPECTED}" -ne 0 ]; then
|
|
echo "==> BUILD FAILED: allowlist audit found unexpected content. Nothing was packaged."
|
|
exit 1
|
|
fi
|
|
echo " clean -- only allowlisted paths present"
|
|
|
|
# Read the version BEFORE stripping -- it lives inside the plugin
|
|
# header's own DocBlock comment, which php_strip_whitespace() below
|
|
# would otherwise remove before this ever got a chance to read it.
|
|
VERSION="$(grep -oP '(?<=Version:)\s*\K\S+' "${STAGE_DIR}/argus-wordpress-defence.php" | head -1)"
|
|
if [ -z "${VERSION}" ]; then
|
|
echo "==> BUILD FAILED: could not read plugin version from argus-wordpress-defence.php"
|
|
exit 1
|
|
fi
|
|
echo "==> Packaging version ${VERSION}"
|
|
|
|
echo "==> Stripping comments/whitespace (not obfuscation -- see bin/strip-comments.php)"
|
|
php "${REPO_ROOT}/bin/strip-comments.php" "${STAGE_DIR}"
|
|
|
|
mkdir -p "${OUT_DIR}"
|
|
ZIP_PATH="${OUT_DIR}/argus-wordpress-defence-${VERSION}.zip"
|
|
rm -f "${ZIP_PATH}"
|
|
# Prefer the `zip` CLI when present (most CI runners have it); fall
|
|
# back to PHP's ZipArchive (bin/zip-directory.php) for a host that
|
|
# only has the PHP extension -- either is a real, complete archive, no
|
|
# feature difference between the two paths.
|
|
if command -v zip >/dev/null 2>&1; then
|
|
( cd "${BUILD_DIR}" && zip -rq "${ZIP_PATH}" "argus-wordpress-defence" )
|
|
else
|
|
php "${REPO_ROOT}/bin/zip-directory.php" "${STAGE_DIR}" "${ZIP_PATH}" "argus-wordpress-defence"
|
|
fi
|
|
|
|
SHA256="$(sha256sum "${ZIP_PATH}" | cut -d' ' -f1)"
|
|
echo "${SHA256} $(basename "${ZIP_PATH}")" > "${ZIP_PATH}.sha256"
|
|
|
|
echo "==> Done"
|
|
echo " ${ZIP_PATH}"
|
|
echo " SHA-256: ${SHA256}"
|
|
echo " Size: $(du -h "${ZIP_PATH}" | cut -f1)"
|