feat: vendor and censor the phishing blocklist at build time (closes #219)
This commit was merged in pull request #301.
This commit is contained in:
@@ -8,6 +8,7 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
main() {
|
||||
"$SCRIPT_DIR/test"
|
||||
"$SCRIPT_DIR/test-verify-build"
|
||||
"$SCRIPT_DIR/check-censored"
|
||||
"$SCRIPT_DIR/lint"
|
||||
"$SCRIPT_DIR/fmt-check"
|
||||
}
|
||||
|
||||
301
script/check-censored
Executable file
301
script/check-censored
Executable file
@@ -0,0 +1,301 @@
|
||||
#!/bin/sh
|
||||
# script/check-censored: assert that the competitor name RULES.md bars appears
|
||||
# nowhere in this repo, and nowhere in the built extension, except where it is
|
||||
# deliberate. Our own extension to scripts-to-rule-them-all, run from
|
||||
# script/check and from make build.
|
||||
#
|
||||
# Where the name is allowed, and why each one is not negotiable away:
|
||||
#
|
||||
# - script/vendor-blocklist. Build-time tooling, never shipped. A pinned
|
||||
# source reference that does not say what the source is cannot be verified
|
||||
# by anyone, so it names it. Whole-file exemption.
|
||||
# - the two provider-shim identifiers in src/content/inpage.js. Protocol
|
||||
# identifiers dApps feature-detect on; renaming them does not rename them in
|
||||
# their code, it only stops this wallet working on their sites.
|
||||
# - the on-chain name of the MUSD ERC-20 in src/shared/tokenList.js. It is not
|
||||
# what backs symbol-spoof detection — that reads symbol and address — but
|
||||
# the wallet already surfaces the on-chain name of any token the user holds
|
||||
# (src/shared/balances.js), and this contract's on-chain name is that
|
||||
# string, so censoring the repo cannot stop the wallet displaying it.
|
||||
# Dropping the entry instead would cost the user MUSD spoof detection.
|
||||
#
|
||||
# Everything else fails, in the working tree and under dist/. The last two are
|
||||
# literals rather than whole files, so they are enforced by counting, and each
|
||||
# literal is scoped to the path allowed to carry it: a file may contain the name
|
||||
# only as many times as it contains the literals permitted *there*, and zero
|
||||
# times anywhere else. The emitted bundles carry them too, so a plain "the name
|
||||
# must not appear in dist/" could never have passed.
|
||||
#
|
||||
# The name itself is not written in this file. script/vendor-blocklist is the
|
||||
# one place in this repo that defines it, and this reads it back out of there —
|
||||
# so the repo-wide grep this check exists to enforce keeps returning exactly the
|
||||
# files named above, and this file is not one of them.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
# Absolute path to this script, resolved before anything cd's anywhere: the
|
||||
# scan half runs in a re-invocation through xargs, so that the paths it works on
|
||||
# arrive as arguments and cannot be reshaped by field splitting on the way in.
|
||||
SELF="$(cd "$(dirname "$0")" && pwd -P)/$(basename "$0")"
|
||||
|
||||
# Internal re-entry flag. Not part of the command-line interface.
|
||||
SCAN_FLAG="--scan-paths"
|
||||
|
||||
VENDOR_SCRIPT="$ROOT/script/vendor-blocklist"
|
||||
|
||||
# Set by extract_name / make_literals_file.
|
||||
NAME=""
|
||||
ALLOWED_LITERALS_FILE=""
|
||||
|
||||
FAILED=0
|
||||
|
||||
cleanup() {
|
||||
[ -z "$ALLOWED_LITERALS_FILE" ] || rm -f "$ALLOWED_LITERALS_FILE"
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
fail() {
|
||||
echo "check-censored: FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# The name, taken from the single place that defines it. A check scanning for a
|
||||
# pattern it failed to read would pass against anything, so this refuses to
|
||||
# continue unless it got something that looks like the definition.
|
||||
extract_name() {
|
||||
[ -f "$VENDOR_SCRIPT" ] ||
|
||||
fail "$VENDOR_SCRIPT is missing, and it is where the name being
|
||||
checked for is defined. Nothing was scanned."
|
||||
|
||||
NAME="$(grep -m1 '^UPSTREAM_ORG=' "$VENDOR_SCRIPT" | cut -d'"' -f2)" ||
|
||||
fail "could not read UPSTREAM_ORG from $VENDOR_SCRIPT. Nothing was
|
||||
scanned."
|
||||
|
||||
case "$NAME" in
|
||||
"" | *[!A-Za-z0-9]*)
|
||||
fail "UPSTREAM_ORG in $VENDOR_SCRIPT did not yield a plain name
|
||||
(got: '$NAME'). Scanning for that would prove nothing. Nothing was
|
||||
scanned."
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
make_literals_file() {
|
||||
ALLOWED_LITERALS_FILE="$(mktemp \
|
||||
"${TMPDIR:-/tmp}/autistmask-censored.XXXXXX")" ||
|
||||
fail "could not create a temporary file, so nothing was scanned."
|
||||
}
|
||||
|
||||
# The literals $1 may carry, and nothing else may. Each contains the name
|
||||
# exactly once, which is what makes counting them sound; each is scoped to its
|
||||
# path, so a file with no business carrying the name fails even when it spells
|
||||
# it the way shipped code has to. Scoping is the point: permitting these
|
||||
# literals in any file is what once let this check pass its own prose.
|
||||
#
|
||||
# The emitted paths are listed next to the sources they come from. If the
|
||||
# bundler moves one, this goes red and the new path gets added deliberately,
|
||||
# rather than a wildcard over dist/ covering whatever lands there.
|
||||
allowed_literals_for() {
|
||||
: >"$ALLOWED_LITERALS_FILE"
|
||||
case "$1" in
|
||||
src/content/inpage.js | dist/*/src/content/inpage.js)
|
||||
printf 'is%s\n_%s\n' "$NAME" "$NAME" >"$ALLOWED_LITERALS_FILE"
|
||||
;;
|
||||
src/shared/tokenList.js | dist/*/src/background/index.js | \
|
||||
dist/*/src/popup/index.js)
|
||||
printf '%s USD\n' "$NAME" >"$ALLOWED_LITERALS_FILE"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# How many times does $1 contain the name (TOTAL), and how many of those are one
|
||||
# of the allowed literals (ALLOWED)? Same discipline the rest of this repo's
|
||||
# shell checks apply to grep: exit 0 and 1 are answers about the file, anything
|
||||
# else means the file was not searched and is not an answer at all.
|
||||
count_matches() {
|
||||
_cm_status=0
|
||||
_cm_out="$(grep -a -o -i -F -e "$NAME" -- "$1")" || _cm_status=$?
|
||||
case "$_cm_status" in
|
||||
0) TOTAL="$(printf '%s\n' "$_cm_out" | grep -c .)" ;;
|
||||
1) TOTAL=0 ;;
|
||||
*)
|
||||
fail "grep exited $_cm_status reading $1, so the file was never
|
||||
searched and nothing was established about it. That is a permissions or I/O
|
||||
fault, not a clean file. Refusing to report success."
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$TOTAL" -eq 0 ]; then
|
||||
ALLOWED=0
|
||||
return 0
|
||||
fi
|
||||
|
||||
# No literal is permitted at this path, so every occurrence is a violation.
|
||||
# Handled here rather than by grep, which is not required to say anything
|
||||
# useful about an empty pattern file.
|
||||
if [ ! -s "$ALLOWED_LITERALS_FILE" ]; then
|
||||
ALLOWED=0
|
||||
return 0
|
||||
fi
|
||||
|
||||
_cm_status=0
|
||||
_cm_out="$(grep -a -o -i -F -f "$ALLOWED_LITERALS_FILE" -- "$1")" ||
|
||||
_cm_status=$?
|
||||
case "$_cm_status" in
|
||||
0) ALLOWED="$(printf '%s\n' "$_cm_out" | grep -c .)" ;;
|
||||
1) ALLOWED=0 ;;
|
||||
*)
|
||||
fail "grep exited $_cm_status matching the allowed literals in $1.
|
||||
Refusing to report success."
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# The per-path half, run in a re-invocation of this script so it uses the same
|
||||
# counting as everything else rather than a second copy of it.
|
||||
scan_paths() {
|
||||
for _file in "$@"; do
|
||||
# dist/ arrives absolute (find) and the worktree relative (git
|
||||
# ls-files). The allowlist is keyed on repo-relative paths, so both
|
||||
# forms are reduced to one before anything is decided about them.
|
||||
_rel="$_file"
|
||||
case "$_rel" in
|
||||
"$ROOT"/*) _rel="${_rel#"$ROOT"/}" ;;
|
||||
esac
|
||||
|
||||
case "$_rel" in
|
||||
script/vendor-blocklist) continue ;;
|
||||
esac
|
||||
[ -f "$_file" ] || continue
|
||||
|
||||
allowed_literals_for "$_rel"
|
||||
count_matches "$_file"
|
||||
[ "$TOTAL" -gt "$ALLOWED" ] || continue
|
||||
|
||||
FAILED=$((FAILED + 1))
|
||||
echo "check-censored: $_rel: $TOTAL occurrence(s) of the name," \
|
||||
"$ALLOWED of them allowed at this path" >&2
|
||||
grep -a -n -i -F -e "$NAME" -- "$_file" | cut -c1-140 | head -5 >&2
|
||||
done
|
||||
[ "$FAILED" -eq 0 ]
|
||||
}
|
||||
|
||||
# Hand a NUL-delimited listing to the scan half. Returns non-zero if any path
|
||||
# failed, or if the scan could not be run at all.
|
||||
scan_listing() {
|
||||
xargs -0 "$SELF" "$SCAN_FLAG" <"$1"
|
||||
}
|
||||
|
||||
# Every file git tracks, plus everything untracked and not ignored: the working
|
||||
# tree as a reviewer would see it, and never node_modules or dist/ (both are
|
||||
# ignored; dist/ is walked separately below).
|
||||
check_worktree() {
|
||||
_list="$(mktemp "${TMPDIR:-/tmp}/autistmask-censored-tree.XXXXXX")" ||
|
||||
fail "could not create a temporary file, so nothing was scanned."
|
||||
_status=0
|
||||
git ls-files -z --cached --others --exclude-standard >"$_list" ||
|
||||
_status=$?
|
||||
[ "$_status" -eq 0 ] || {
|
||||
rm -f "$_list"
|
||||
fail "git ls-files exited $_status, so the working tree was never
|
||||
enumerated and nothing was established about it."
|
||||
}
|
||||
|
||||
# Repo-relative paths. The scan half cd's to the repo root before it opens
|
||||
# anything, so they reach it intact and unjoined.
|
||||
WORKTREE_COUNT="$(tr -dc '\0' <"$_list" | wc -c | tr -d ' ')"
|
||||
|
||||
_status=0
|
||||
scan_listing "$_list" || _status=$?
|
||||
rm -f "$_list"
|
||||
return "$_status"
|
||||
}
|
||||
|
||||
check_dist() {
|
||||
_list="$(mktemp "${TMPDIR:-/tmp}/autistmask-censored-dist.XXXXXX")" ||
|
||||
fail "could not create a temporary file, so dist/ was not scanned."
|
||||
_status=0
|
||||
find "$ROOT/dist" -type f -print0 >"$_list" || _status=$?
|
||||
[ "$_status" -eq 0 ] || {
|
||||
rm -f "$_list"
|
||||
fail "find exited $_status enumerating dist/, so part of the emitted
|
||||
tree was never walked and an unchecked file there went unchecked. Refusing
|
||||
to report success."
|
||||
}
|
||||
|
||||
DIST_COUNT="$(tr -dc '\0' <"$_list" | wc -c | tr -d ' ')"
|
||||
|
||||
_status=0
|
||||
scan_listing "$_list" || _status=$?
|
||||
rm -f "$_list"
|
||||
return "$_status"
|
||||
}
|
||||
|
||||
usage() {
|
||||
echo "usage: script/check-censored [--require-dist]" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
|
||||
# Internal re-entry from scan_listing's xargs.
|
||||
if [ "${1-}" = "$SCAN_FLAG" ]; then
|
||||
shift
|
||||
extract_name
|
||||
make_literals_file
|
||||
scan_paths "$@"
|
||||
return $?
|
||||
fi
|
||||
|
||||
require_dist=no
|
||||
case "${1-}" in
|
||||
"") ;;
|
||||
--require-dist) require_dist=yes ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
|
||||
extract_name
|
||||
make_literals_file
|
||||
|
||||
echo "Checking for censored names..."
|
||||
|
||||
tree_status=0
|
||||
check_worktree || tree_status=$?
|
||||
|
||||
dist_status=0
|
||||
dist_inspected=no
|
||||
DIST_COUNT=0
|
||||
if [ -d "$ROOT/dist" ]; then
|
||||
dist_inspected=yes
|
||||
check_dist || dist_status=$?
|
||||
fi
|
||||
|
||||
if [ "$tree_status" -ne 0 ] || [ "$dist_status" -ne 0 ]; then
|
||||
fail "the name appears outside the deliberate exceptions (reported
|
||||
above). See the header of script/check-censored for what is allowed and
|
||||
why."
|
||||
fi
|
||||
|
||||
if [ "$dist_inspected" = no ]; then
|
||||
if [ "$require_dist" = yes ]; then
|
||||
fail "there is no dist/ to inspect and this run was asked to
|
||||
require one. Run make build."
|
||||
fi
|
||||
cat <<EOF
|
||||
################################################################################
|
||||
## WARNING: dist/ WAS NOT INSPECTED BY THIS RUN AND IS NOT PROVEN CLEAN BY IT.
|
||||
## There is no dist/ in this tree. The working tree is clean, but a build can
|
||||
## carry text no source file does — a dependency's, or a bundler's. Every
|
||||
## make build runs this check again with dist/ required, so a release artifact
|
||||
## is always covered; this run simply had none to look at.
|
||||
################################################################################
|
||||
EOF
|
||||
fi
|
||||
|
||||
echo "check-censored: $WORKTREE_COUNT tracked file(s) inspected," \
|
||||
"$DIST_COUNT file(s) under dist/"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
147
script/lib/build-blocklist.js
Normal file
147
script/lib/build-blocklist.js
Normal file
@@ -0,0 +1,147 @@
|
||||
// The transform half of script/vendor-blocklist: upstream's config.json in,
|
||||
// src/shared/phishingBlocklist.json out. Build-time repo tooling; nothing here
|
||||
// is shipped to users.
|
||||
//
|
||||
// Usage: node script/lib/build-blocklist.js <source.json> <output.json>
|
||||
//
|
||||
// What it does, and why each step is here:
|
||||
//
|
||||
// - only the blacklist is carried over. The extension matches a hostname and
|
||||
// its parent domains against that one list; upstream's whitelist, fuzzylist
|
||||
// and version metadata are read by nothing here, so shipping them would add
|
||||
// megabytes of dead weight to every install.
|
||||
// - entries are lowercased and de-duplicated, because that is the form
|
||||
// isPhishingDomain() compares against.
|
||||
// - entries that cannot be a hostname are dropped and counted. Upstream
|
||||
// carries the odd URL-shaped entry (a path, a scheme); hostname matching can
|
||||
// never match one, and once the artifact is hashes nobody can see that it is
|
||||
// in there, so it is reported at vendoring time instead.
|
||||
// - entries are hashed (see src/shared/domainHash.js) and sorted, and the
|
||||
// digests are concatenated into one fixed-width string. Sorted is what makes
|
||||
// the runtime lookup a binary search over that string, with no set to build
|
||||
// on every service-worker wake; one string rather than an array of 100k+ is
|
||||
// what keeps the file, the bundle and the JSON parse small.
|
||||
//
|
||||
// Deterministic by construction: same input bytes, same output bytes.
|
||||
|
||||
"use strict";
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
const {
|
||||
HASH_ALGORITHM,
|
||||
HASH_HEX_CHARS,
|
||||
hashDomain,
|
||||
} = require("../../src/shared/domainHash");
|
||||
|
||||
// A blocklist that has collapsed to a handful of entries is a broken fetch or a
|
||||
// changed upstream shape, not a quiet day in phishing. Vendoring it would
|
||||
// disarm the feature, so it fails instead and a human decides.
|
||||
const MIN_ENTRIES = 10000;
|
||||
|
||||
function fail(message) {
|
||||
process.stderr.write("build-blocklist: " + message + "\n");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// A hostname, as the matcher understands one: dot-separated labels of letters,
|
||||
// digits, hyphens and underscores. Anything else — a path, a scheme, a space,
|
||||
// an empty string, a non-ASCII label a browser would have punycoded before it
|
||||
// ever reached isPhishingDomain() — cannot be produced by the hostname variants
|
||||
// the extension looks up, so it could only ever sit in the artifact unused.
|
||||
//
|
||||
// Underscores are deliberate. They are not legal in a hostname per RFC 1123,
|
||||
// but DNS carries them and browsers resolve them, and upstream lists 141 entries
|
||||
// that use one — real phishing sites on shared subdomain hosts. A stricter
|
||||
// pattern silently drops every one of them.
|
||||
const HOSTNAME_RE =
|
||||
/^[a-z0-9_]([a-z0-9_-]*[a-z0-9_])?(\.[a-z0-9_]([a-z0-9_-]*[a-z0-9_])?)+$/;
|
||||
|
||||
function main(argv) {
|
||||
const [source, output] = argv;
|
||||
if (!source || !output) {
|
||||
fail("usage: build-blocklist.js <source.json> <output.json>");
|
||||
}
|
||||
|
||||
let config;
|
||||
try {
|
||||
config = JSON.parse(fs.readFileSync(source, "utf8"));
|
||||
} catch (e) {
|
||||
fail("could not read " + source + " as JSON: " + e.message);
|
||||
}
|
||||
|
||||
if (!Array.isArray(config.blacklist)) {
|
||||
fail(
|
||||
"the source has no blacklist array, so its shape is not the one " +
|
||||
"this transform understands. Refusing to write an artifact.",
|
||||
);
|
||||
}
|
||||
|
||||
const seen = new Set();
|
||||
let dropped = 0;
|
||||
for (const raw of config.blacklist) {
|
||||
if (typeof raw !== "string") {
|
||||
dropped++;
|
||||
continue;
|
||||
}
|
||||
const domain = raw.trim().toLowerCase();
|
||||
if (!HOSTNAME_RE.test(domain)) {
|
||||
dropped++;
|
||||
continue;
|
||||
}
|
||||
seen.add(domain);
|
||||
}
|
||||
|
||||
if (seen.size < MIN_ENTRIES) {
|
||||
fail(
|
||||
"the source yielded " +
|
||||
seen.size +
|
||||
" usable entries, below the " +
|
||||
MIN_ENTRIES +
|
||||
" floor. That is a broken source or a changed upstream " +
|
||||
"shape, and vendoring it would disarm phishing detection. " +
|
||||
"Refusing to write an artifact.",
|
||||
);
|
||||
}
|
||||
|
||||
const hashes = [];
|
||||
for (const domain of seen) hashes.push(hashDomain(domain));
|
||||
hashes.sort();
|
||||
|
||||
// Truncation makes collisions possible; they are harmless (both entries are
|
||||
// blocked either way) but they must not inflate the count the artifact
|
||||
// claims, which the runtime cross-checks against the string length.
|
||||
const unique = [];
|
||||
for (const hash of hashes) {
|
||||
if (unique.length === 0 || unique[unique.length - 1] !== hash) {
|
||||
unique.push(hash);
|
||||
}
|
||||
}
|
||||
|
||||
const artifact = {
|
||||
algorithm: HASH_ALGORITHM,
|
||||
hashHexChars: HASH_HEX_CHARS,
|
||||
count: unique.length,
|
||||
hashes: unique.join(""),
|
||||
};
|
||||
|
||||
// Four-space JSON with a trailing newline: what prettier emits for this
|
||||
// shape, so a vendored artifact passes make fmt-check untouched.
|
||||
fs.writeFileSync(output, JSON.stringify(artifact, null, 4) + "\n");
|
||||
|
||||
process.stdout.write(
|
||||
"build-blocklist: " +
|
||||
config.blacklist.length +
|
||||
" source entries -> " +
|
||||
seen.size +
|
||||
" usable domains -> " +
|
||||
unique.length +
|
||||
" digests (" +
|
||||
dropped +
|
||||
" not hostnames, " +
|
||||
(seen.size - unique.length) +
|
||||
" digest collisions)\n",
|
||||
);
|
||||
}
|
||||
|
||||
main(process.argv.slice(2));
|
||||
@@ -59,12 +59,12 @@ main() {
|
||||
# browser profile.
|
||||
# PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS=1: without it,
|
||||
# ctx.route() intercepts page requests only, and every fetch made by
|
||||
# the MV3 background service worker — including the phishing
|
||||
# blocklist fetch that src/background/index.js issues at worker
|
||||
# startup — goes to the real internet. The flag is experimental and
|
||||
# Playwright may drop or rename it. It cannot break silently: the
|
||||
# harness probes service-worker interception at launch and aborts
|
||||
# the whole suite if it is not in effect (see the interception
|
||||
# the MV3 background service worker — the JSON-RPC calls behind
|
||||
# every approval the suite drives among them — goes to the real
|
||||
# internet. The flag is experimental and Playwright may drop or
|
||||
# rename it. It cannot break silently: the harness asks the worker
|
||||
# for one request of its own at launch and aborts the whole suite
|
||||
# if it does not reach the route handler (see the interception
|
||||
# canary in tests/e2e/harness.js). If a future Playwright removes
|
||||
# the flag, that probe is what will fail, and the fix is either a
|
||||
# replacement mechanism or an honest downgrade of the isolation
|
||||
|
||||
105
script/vendor-blocklist
Executable file
105
script/vendor-blocklist
Executable file
@@ -0,0 +1,105 @@
|
||||
#!/bin/sh
|
||||
# script/vendor-blocklist: refresh the vendored phishing blocklist at
|
||||
# src/shared/phishingBlocklist.json from its upstream source. Our own extension
|
||||
# to scripts-to-rule-them-all.
|
||||
#
|
||||
# This is build-time repo tooling and is not shipped. It is the one place in
|
||||
# this repo that names the upstream project, because a source reference that
|
||||
# does not say what the source is cannot be verified by anyone; the artifact it
|
||||
# writes carries no names at all (see src/shared/domainHash.js).
|
||||
# script/check-censored reads the name back out of this file rather than
|
||||
# repeating it, so it stays defined exactly once.
|
||||
#
|
||||
# Run it deliberately, not on every build: the output is committed, and the
|
||||
# extension does no runtime fetching, so the shipped list is exactly as fresh as
|
||||
# the last time someone ran this and landed the result. Re-run it, land the
|
||||
# diff, cut a release; that is the whole refresh path.
|
||||
#
|
||||
# Pinned by content hash, twice over, as REPO_POLICIES.md requires. The commit
|
||||
# below is an immutable ref — the upstream default branch moves several times a
|
||||
# day and cannot be pinned — and UPSTREAM_SHA256 is the sha256 of the bytes that
|
||||
# commit serves. A mismatch is a hard failure: a vendoring step that accepts
|
||||
# whatever it is handed is a supply-chain hole, and this one feeds a security
|
||||
# warning shown to users.
|
||||
#
|
||||
# To move the pin: pick the new commit, run this with the new UPSTREAM_COMMIT
|
||||
# and an UPSTREAM_SHA256 you have not yet updated, and it will print the hash it
|
||||
# actually got. Verify that hash against the source independently before
|
||||
# recording it. Never copy the "actual" line in on trust.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
# Upstream, pinned 2026-08-17.
|
||||
UPSTREAM_ORG="MetaMask"
|
||||
UPSTREAM_REPO="eth-phishing-detect"
|
||||
UPSTREAM_COMMIT="6dddf74a87da3e1a0841f7ae0d1cb31aaf2c05db"
|
||||
UPSTREAM_FILE="src/config.json"
|
||||
UPSTREAM_SHA256="166d5b3504e8f4ed52eae37d3dd20c1a56efa0502bfb3dc957044ff8b5f1283f"
|
||||
|
||||
OUTPUT="src/shared/phishingBlocklist.json"
|
||||
|
||||
WORK=""
|
||||
|
||||
cleanup() {
|
||||
[ -z "$WORK" ] || rm -rf "$WORK"
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
fail() {
|
||||
echo "vendor-blocklist: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
sha256_of() {
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
sha256sum "$1" | cut -d' ' -f1
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
shasum -a 256 "$1" | cut -d' ' -f1
|
||||
else
|
||||
fail "neither sha256sum nor shasum is available, so the fetched
|
||||
source cannot be verified. Refusing to vendor unverified content."
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
|
||||
command -v curl >/dev/null 2>&1 ||
|
||||
fail "curl is required to fetch the upstream list"
|
||||
command -v node >/dev/null 2>&1 ||
|
||||
fail "node is required to build the artifact; run script/bootstrap"
|
||||
|
||||
WORK="$(mktemp -d "${TMPDIR:-/tmp}/autistmask-vendor-blocklist.XXXXXX")" ||
|
||||
fail "could not create a working directory"
|
||||
|
||||
url="https://raw.githubusercontent.com/$UPSTREAM_ORG/$UPSTREAM_REPO/$UPSTREAM_COMMIT/$UPSTREAM_FILE"
|
||||
echo "Fetching $url"
|
||||
curl -fsSL --proto '=https' --tlsv1.2 -o "$WORK/source.json" "$url" ||
|
||||
fail "the fetch failed, so nothing was vendored"
|
||||
|
||||
actual="$(sha256_of "$WORK/source.json")"
|
||||
if [ "$actual" != "$UPSTREAM_SHA256" ]; then
|
||||
fail "sha256 mismatch on the fetched source.
|
||||
expected: $UPSTREAM_SHA256
|
||||
actual: $actual
|
||||
The pinned commit is immutable, so the same commit serving different bytes
|
||||
means the content was substituted somewhere between upstream and here.
|
||||
Nothing was written. Do not update the expectation to match unless you have
|
||||
verified the new bytes independently."
|
||||
fi
|
||||
echo "Verified sha256 $actual"
|
||||
|
||||
node script/lib/build-blocklist.js "$WORK/source.json" "$WORK/out.json" ||
|
||||
fail "the transform failed, so nothing was written"
|
||||
|
||||
if [ -f "$OUTPUT" ] && cmp -s "$WORK/out.json" "$OUTPUT"; then
|
||||
echo "vendor-blocklist: $OUTPUT is already up to date"
|
||||
return 0
|
||||
fi
|
||||
|
||||
cp "$WORK/out.json" "$OUTPUT"
|
||||
echo "vendor-blocklist: wrote $OUTPUT (sha256 $(sha256_of "$OUTPUT"))"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user