mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-08-28 13:54:39 +00:00
Refine filter policy playground
This commit is contained in:
+159
-174
@@ -22,7 +22,7 @@
|
||||
});
|
||||
const MODES = Object.freeze(["active", "shadow", "disabled"]);
|
||||
const STOPS = Object.freeze(["none", "phase", "policy"]);
|
||||
const ROUTES = Object.freeze(["flood", "unscoped_flood", "scoped_flood", "direct", "any"]);
|
||||
const ROUTES = Object.freeze(["flood", "unscoped_flood", "scoped_flood"]);
|
||||
const TYPE_NAMES = Object.freeze([
|
||||
"req", "response", "txt_msg", "ack", "advert", "grp_txt", "grp_data", "anon_req",
|
||||
"path", "trace", "multipart", "control", "ota", "13", "14", "raw_custom",
|
||||
@@ -82,16 +82,6 @@
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function nullableNumber(value, minimum, maximum, label) {
|
||||
const text = clean(value);
|
||||
if (!text) return null;
|
||||
const parsed = Number(text);
|
||||
if (!Number.isFinite(parsed) || parsed < minimum || parsed > maximum) {
|
||||
throw new FilterToolError(`${label} must be ${minimum}-${maximum}.`);
|
||||
}
|
||||
return Math.round(parsed * 100) / 100;
|
||||
}
|
||||
|
||||
function enumValue(value, allowed, label, fallback) {
|
||||
const normalized = clean(value).toLowerCase() || fallback;
|
||||
if (!allowed.includes(normalized)) throw new FilterToolError(`${label} is invalid.`);
|
||||
@@ -215,8 +205,6 @@
|
||||
pathPrefix: "",
|
||||
sender: "",
|
||||
tempRadio: "any",
|
||||
snrMin: null,
|
||||
snrMax: null,
|
||||
verdict: "continue",
|
||||
scopeGate: "unchanged",
|
||||
targetKind: "scope",
|
||||
@@ -261,8 +249,6 @@
|
||||
pathPrefix: "",
|
||||
sender: clean(input.sender),
|
||||
tempRadio: enumValue(input.tempRadio, ["any", "active", "inactive"], "Temporary-radio matcher", "any"),
|
||||
snrMin: nullableNumber(input.snrMin, -30, 30, "Minimum SNR"),
|
||||
snrMax: nullableNumber(input.snrMax, -30, 30, "Maximum SNR"),
|
||||
verdict: enumValue(input.verdict, ["continue", "drop"], "Forwarding verdict", "continue"),
|
||||
scopeGate: enumValue(input.scopeGate, ["unchanged", "require_allowed", "bypass_global"], "Scope-gate action", "unchanged"),
|
||||
targetKind: enumValue(input.targetKind, ["none", "scope", "region"], "Scope target", "none"),
|
||||
@@ -289,10 +275,6 @@
|
||||
if (rule.channel && !["any", "class:group", "grp_txt", "grp_data"].includes(rule.type)) {
|
||||
throw new FilterToolError("Authenticated channel matching requires a group-capable payload type or class.");
|
||||
}
|
||||
if (rule.snrMin != null && rule.snrMax != null && rule.snrMin > rule.snrMax) {
|
||||
throw new FilterToolError("Minimum SNR cannot exceed maximum SNR.");
|
||||
}
|
||||
|
||||
if (rule.targetKind === "scope") rule.target = normalizeScopeName(input.target);
|
||||
if (rule.targetKind === "region") rule.target = normalizeRegionName(input.target);
|
||||
if (rule.rate != null) {
|
||||
@@ -366,9 +348,6 @@
|
||||
else if (rule.pathKind !== "none") matches.push(`path=${rule.pathKind}`);
|
||||
if (rule.sender) matches.push(`sender=${quoteDsl(rule.sender)}`);
|
||||
if (rule.tempRadio !== "any") matches.push(`tempradio=${rule.tempRadio}`);
|
||||
if (rule.snrMin != null || rule.snrMax != null) {
|
||||
matches.push(`snr=${rule.snrMin == null ? "" : rule.snrMin}..${rule.snrMax == null ? "" : rule.snrMax}`);
|
||||
}
|
||||
const actions = [];
|
||||
if (rule.verdict === "drop") actions.push("drop");
|
||||
if (rule.scopeGate === "require_allowed") actions.push("scope-gate=require");
|
||||
@@ -442,8 +421,6 @@
|
||||
rule.pathPrefix = "";
|
||||
rule.sender = "";
|
||||
rule.tempRadio = "any";
|
||||
rule.snrMin = null;
|
||||
rule.snrMax = null;
|
||||
rule.verdict = "continue";
|
||||
rule.scopeGate = "unchanged";
|
||||
rule.targetKind = "none";
|
||||
@@ -479,12 +456,7 @@
|
||||
} else rule.pathKind = value;
|
||||
} else if (name === "sender") rule.sender = value;
|
||||
else if (name === "tempradio") rule.tempRadio = value;
|
||||
else if (name === "snr") {
|
||||
const match = value.match(/^(-?\d+(?:\.\d+)?)?\.\.(-?\d+(?:\.\d+)?)?$/);
|
||||
if (!match || (!match[1] && !match[2])) throw new FilterToolError("SNR matcher must use min..max, min.., or ..max.");
|
||||
rule.snrMin = match[1] || null;
|
||||
rule.snrMax = match[2] || null;
|
||||
} else throw new FilterToolError(`Unsupported receive-time matcher: ${token}`);
|
||||
else throw new FilterToolError(`Unsupported receive-time matcher: ${token}`);
|
||||
});
|
||||
tokens.slice(doIndex + 1).forEach((token) => {
|
||||
const [name, value] = splitOption(token);
|
||||
@@ -534,8 +506,6 @@
|
||||
flood: "either flood route",
|
||||
unscoped_flood: "an unscoped flood",
|
||||
scoped_flood: "a transport-scoped flood",
|
||||
direct: "a direct route",
|
||||
any: "any route",
|
||||
}[route];
|
||||
}
|
||||
|
||||
@@ -585,11 +555,6 @@
|
||||
else if (rule.pathKind.startsWith("loop:")) conditions.push(`the ${rule.pathKind.slice(5)} own-ID loop threshold`);
|
||||
if (rule.sender) conditions.push(`decrypted sender "${rule.sender}"`);
|
||||
if (rule.tempRadio !== "any") conditions.push(`temporary radio ${rule.tempRadio}`);
|
||||
if (rule.snrMin != null || rule.snrMax != null) {
|
||||
if (rule.snrMin != null && rule.snrMax != null) conditions.push(`SNR ${rule.snrMin} through ${rule.snrMax} dB`);
|
||||
else if (rule.snrMin != null) conditions.push(`SNR at least ${rule.snrMin} dB`);
|
||||
else conditions.push(`SNR at most ${rule.snrMax} dB`);
|
||||
}
|
||||
const mode = rule.mode === "shadow"
|
||||
? "In shadow mode, report that it would "
|
||||
: rule.mode === "disabled"
|
||||
@@ -598,23 +563,36 @@
|
||||
return `${PHASE_LABELS[rule.phase]} rule ${rule.id}, owned by ${OWNER_LABELS[rule.owner]}, matches ${conditions.join(", ")}. ${mode}${actionPhrases(rule).join(" and ")}. Priority ${rule.priority}; every condition reads immutable receive-time facts.`;
|
||||
}
|
||||
|
||||
function limitsRemoteManagement(rule) {
|
||||
const matchesLoginType = rule.type === "any"
|
||||
|| rule.type === "class:login"
|
||||
|| LOGIN_TYPES.includes(rule.type);
|
||||
const canRestrict = rule.verdict === "drop"
|
||||
|| rule.rate != null
|
||||
|| rule.scopeGate === "require_allowed";
|
||||
return !rule.channel && matchesLoginType && canRestrict;
|
||||
}
|
||||
|
||||
function ruleWarnings(input) {
|
||||
const rule = normalizeRule(input);
|
||||
const warnings = [];
|
||||
const [minimumHops] = hopBounds(rule.hops);
|
||||
if (rule.verdict === "drop" && rule.route === "any" && rule.type === "any" && rule.hops === "all") {
|
||||
warnings.push("This is a global drop rule across flood and direct traffic.");
|
||||
if (rule.verdict === "drop" && rule.route === "flood" && rule.type === "any"
|
||||
&& rule.hops === "all" && !rule.channel && rule.incoming === "any"
|
||||
&& rule.pathKind === "none" && rule.tempRadio === "any") {
|
||||
warnings.push("This is a global drop rule across both flood route types.");
|
||||
}
|
||||
if (rule.verdict === "drop" && minimumHops === 0
|
||||
&& ["any", "class:login", "req", "response", "txt_msg", "anon_req", "path"].includes(rule.type)) {
|
||||
warnings.push("This rule can block remote administration traffic at hop 0. Keep a serial recovery path.");
|
||||
if (limitsRemoteManagement(rule)) {
|
||||
const zeroHop = minimumHops === 0 && rule.pathKind === "none";
|
||||
warnings.push(zeroHop
|
||||
? "Remote-management relay risk: this rule can restrict zero-hop login/admin floods. Direct routes and local delivery stay outside this policy, but multi-hop login reach can still be lost."
|
||||
: "Remote-management reach warning: this rule intentionally limits relayed login/admin floods at its hop or path condition. Direct routes and local delivery stay outside this policy.");
|
||||
}
|
||||
if (rule.type === "class:other") warnings.push("class:other intentionally includes current and future types outside group and login classes, including OTA.");
|
||||
if (rule.channel && rule.type === "any") warnings.push("A channel condition narrows type=any to authenticated group text/data packets.");
|
||||
if (rule.sender) warnings.push("Displayed sender names are spoofable and are moderation signals, not identities.");
|
||||
if (rule.pathKind !== "none") warnings.push("Pbyte and path-table matches use truncated routing hints, not authenticated identities.");
|
||||
if (rule.pathKind.startsWith("bucket:")) warnings.push("The selected bucket must exist on the target node; the policy stores a reference, not its IDs.");
|
||||
if (rule.snrMin != null || rule.snrMax != null) warnings.push("Signal-based policy can produce different decisions at neighboring nodes hearing the same packet.");
|
||||
if (rule.stop === "policy") warnings.push("stop=policy skips every later configurable phase when this rule matches, but not mandatory protocol/radio safety.");
|
||||
if (rule.mode === "shadow") warnings.push("Shadow mode records the match but applies no action and does not stop processing.");
|
||||
if (rule.targetKind === "region") warnings.push("The configured region must exist, allow flooding, and have a usable transport key at evaluation time.");
|
||||
@@ -631,7 +609,6 @@
|
||||
else if (rule.pathKind !== "none") bytes += 3;
|
||||
if (rule.sender) bytes += 3 + rule.sender.length;
|
||||
if (rule.tempRadio !== "any") bytes += 3;
|
||||
if (rule.snrMin != null || rule.snrMax != null) bytes += 6;
|
||||
if (rule.verdict === "drop") bytes += 2;
|
||||
if (rule.scopeGate !== "unchanged") bytes += 3;
|
||||
if (rule.targetKind !== "none") bytes += 3 + rule.target.length;
|
||||
@@ -673,6 +650,10 @@
|
||||
});
|
||||
});
|
||||
const estimated = rules.reduce((total, rule) => total + estimateRuleBytes(rule), 16);
|
||||
const managementRules = rules.filter((rule) => rule.mode === "active" && limitsRemoteManagement(rule));
|
||||
if (managementRules.length) {
|
||||
warnings.push(`${managementRules.length} active rule${managementRules.length === 1 ? "" : "s"} can limit relayed remote-login traffic. Direct routes and local packet delivery remain outside the policy, but end-to-end flood login reach is not guaranteed.`);
|
||||
}
|
||||
const budget = PROFILE_BUDGETS[profile] || PROFILE_BUDGETS.nrf52;
|
||||
if (estimated > budget) warnings.push(`Approximate packed size ${estimated} bytes exceeds the selected ${budget}-byte target budget.`);
|
||||
if (rules.length > 255) warnings.push("The draft exceeds the proposed 255 stable rule-ID limit.");
|
||||
@@ -780,7 +761,7 @@
|
||||
? clean(input.buckets).split(",").map((value) => requiredInteger(value, 1, 6, "Path bucket"))
|
||||
: [];
|
||||
return {
|
||||
route: enumValue(input.route, ["unscoped_flood", "scoped_flood", "direct"], "Packet route", "unscoped_flood"),
|
||||
route: enumValue(input.route, ["unscoped_flood", "scoped_flood"], "Packet route", "unscoped_flood"),
|
||||
type,
|
||||
hops: requiredInteger(input.hops, 0, 63, "Packet hops"),
|
||||
channel,
|
||||
@@ -790,7 +771,6 @@
|
||||
regionName: clean(input.regionName),
|
||||
sender,
|
||||
tempRadio: Boolean(input.tempRadio),
|
||||
snr: nullableNumber(input.snr, -30, 30, "Packet SNR"),
|
||||
blacklist: Boolean(input.blacklist),
|
||||
buckets: Array.from(new Set(buckets)),
|
||||
loopLevel: requiredInteger(input.loopLevel == null ? 0 : input.loopLevel, 0, 3, "Loop result"),
|
||||
@@ -824,7 +804,7 @@
|
||||
const packet = normalizePacket(inputPacket);
|
||||
const misses = [];
|
||||
if (rule.route === "flood" && !["unscoped_flood", "scoped_flood"].includes(packet.route)) misses.push("route is not flood");
|
||||
else if (rule.route !== "any" && rule.route !== "flood" && rule.route !== packet.route) misses.push(`route is ${packet.route}`);
|
||||
else if (rule.route !== "flood" && rule.route !== packet.route) misses.push(`route is ${packet.route}`);
|
||||
if (!typeMatches(rule.type, packet.type)) misses.push(`payload type ${packet.type} is outside ${rule.type}`);
|
||||
const [minimum, maximum] = hopBounds(rule.hops);
|
||||
if (packet.hops < minimum || packet.hops > maximum) misses.push(`hop ${packet.hops} is outside ${rule.hops}`);
|
||||
@@ -844,8 +824,6 @@
|
||||
if (rule.sender && rule.sender.toLowerCase() !== packet.sender.toLowerCase()) misses.push("decrypted sender differs or is unavailable");
|
||||
if (rule.tempRadio === "active" && !packet.tempRadio) misses.push("temporary radio is inactive");
|
||||
if (rule.tempRadio === "inactive" && packet.tempRadio) misses.push("temporary radio is active");
|
||||
if (rule.snrMin != null && (packet.snr == null || packet.snr < rule.snrMin)) misses.push(`SNR is below ${rule.snrMin} dB`);
|
||||
if (rule.snrMax != null && (packet.snr == null || packet.snr > rule.snrMax)) misses.push(`SNR is above ${rule.snrMax} dB`);
|
||||
return { matched: misses.length === 0, misses };
|
||||
}
|
||||
|
||||
@@ -936,40 +914,78 @@
|
||||
return { packet, decision, trace };
|
||||
}
|
||||
|
||||
function documentedDropRule(id, type, hops, priority) {
|
||||
return {
|
||||
...defaultRule(id),
|
||||
phase: "forward",
|
||||
owner: "filter",
|
||||
priority: priority == null ? 100 : priority,
|
||||
type,
|
||||
hops,
|
||||
channel: "",
|
||||
incoming: "any",
|
||||
targetKind: "none",
|
||||
target: "",
|
||||
timing: "inherit",
|
||||
verdict: "drop",
|
||||
};
|
||||
}
|
||||
|
||||
const EXAMPLES = Object.freeze({
|
||||
channel_scope: Object.freeze([
|
||||
{
|
||||
...defaultRule("rgdata-scope"),
|
||||
type: "class:group",
|
||||
channel: "#rgdata",
|
||||
incoming: "any",
|
||||
priority: 100,
|
||||
},
|
||||
]),
|
||||
blackhole: Object.freeze([
|
||||
{
|
||||
...defaultRule("blackhole-rewrite"),
|
||||
priority: 160,
|
||||
...defaultRule("blackhole-after-hop-3"),
|
||||
type: "grp_data",
|
||||
hops: "4+",
|
||||
channel: "#rgdata",
|
||||
incoming: "none",
|
||||
pathKind: "prefix",
|
||||
pathPrefix: "860C",
|
||||
stop: "phase",
|
||||
priority: 100,
|
||||
},
|
||||
]),
|
||||
wildcards: Object.freeze([
|
||||
scope_rewrite: Object.freeze([
|
||||
{
|
||||
...defaultRule("login-scope"),
|
||||
type: "class:login",
|
||||
channel: "",
|
||||
incoming: "any",
|
||||
target: "BlackHole86",
|
||||
priority: 140,
|
||||
...defaultRule("usa-to-blackhole"),
|
||||
type: "grp_data",
|
||||
channel: "#rgdata",
|
||||
incoming: "scope:usa",
|
||||
priority: 100,
|
||||
},
|
||||
]),
|
||||
prefix_rate: Object.freeze([
|
||||
{
|
||||
...defaultRule("other-scope-bucket2"),
|
||||
type: "class:other",
|
||||
channel: "",
|
||||
incoming: "any",
|
||||
pathKind: "bucket:2",
|
||||
target: "BlackHole86",
|
||||
timing: "slow",
|
||||
priority: 130,
|
||||
...documentedDropRule("prefix-860c-rate", "any", "all"),
|
||||
pathKind: "prefix",
|
||||
pathPrefix: "860C",
|
||||
verdict: "continue",
|
||||
rate: 10,
|
||||
burst: 10,
|
||||
},
|
||||
]),
|
||||
channel_stop: Object.freeze([
|
||||
{
|
||||
...documentedDropRule("rgdata-short-hop-stop", "grp_data", "0-2", 200),
|
||||
channel: "#rgdata",
|
||||
verdict: "continue",
|
||||
stop: "policy",
|
||||
},
|
||||
]),
|
||||
high_traffic: Object.freeze([
|
||||
documentedDropRule("limit-req", "req", "3+"),
|
||||
documentedDropRule("limit-response", "response", "9+"),
|
||||
documentedDropRule("limit-group-data", "grp_data", "3+"),
|
||||
documentedDropRule("limit-anon-request", "anon_req", "9+"),
|
||||
documentedDropRule("limit-path", "path", "9+"),
|
||||
documentedDropRule("limit-control", "control", "1+"),
|
||||
]),
|
||||
moderation: Object.freeze([
|
||||
{
|
||||
...defaultRule("public-noisy-user"),
|
||||
@@ -984,123 +1000,55 @@
|
||||
timing: "inherit",
|
||||
rate: 5,
|
||||
burst: 5,
|
||||
tag: "public-rate",
|
||||
},
|
||||
{
|
||||
...defaultRule("local-bot-path"),
|
||||
phase: "content",
|
||||
owner: "filter",
|
||||
type: "grp_txt",
|
||||
channel: "#local",
|
||||
incoming: "any",
|
||||
pathKind: "prefix",
|
||||
pathPrefix: "A1B2C3,D4E5F6",
|
||||
sender: "bot",
|
||||
targetKind: "none",
|
||||
target: "",
|
||||
timing: "inherit",
|
||||
verdict: "drop",
|
||||
priority: 180,
|
||||
},
|
||||
]),
|
||||
system: Object.freeze([
|
||||
blacklist: Object.freeze([
|
||||
{
|
||||
...defaultRule("system-flood-max"),
|
||||
phase: "forward",
|
||||
owner: "system",
|
||||
type: "any",
|
||||
...documentedDropRule("drop-blacklisted-path", "any", "all"),
|
||||
pathKind: "blacklist",
|
||||
},
|
||||
]),
|
||||
wildcards: Object.freeze([
|
||||
{
|
||||
...defaultRule("login-scope"),
|
||||
type: "class:login",
|
||||
channel: "",
|
||||
incoming: "any",
|
||||
hops: "9+",
|
||||
targetKind: "none",
|
||||
target: "",
|
||||
timing: "inherit",
|
||||
verdict: "drop",
|
||||
priority: 220,
|
||||
priority: 140,
|
||||
},
|
||||
{
|
||||
...defaultRule("system-loop-minimal"),
|
||||
phase: "forward",
|
||||
owner: "system",
|
||||
type: "any",
|
||||
...defaultRule("other-scope-bucket2"),
|
||||
type: "class:other",
|
||||
channel: "",
|
||||
incoming: "any",
|
||||
pathKind: "loop:minimal",
|
||||
targetKind: "none",
|
||||
target: "",
|
||||
timing: "inherit",
|
||||
verdict: "drop",
|
||||
priority: 230,
|
||||
pathKind: "bucket:2",
|
||||
timing: "slow",
|
||||
priority: 130,
|
||||
},
|
||||
]),
|
||||
factory: Object.freeze([
|
||||
{
|
||||
...defaultRule("ota-outside-temp-radio"),
|
||||
phase: "forward",
|
||||
...documentedDropRule("ota-outside-temp-radio", "ota", "all", 250),
|
||||
owner: "system",
|
||||
type: "ota",
|
||||
channel: "",
|
||||
incoming: "any",
|
||||
tempRadio: "inactive",
|
||||
targetKind: "none",
|
||||
target: "",
|
||||
timing: "inherit",
|
||||
verdict: "drop",
|
||||
priority: 250,
|
||||
},
|
||||
{
|
||||
...documentedDropRule("wardriving-after-hop-4", "any", "5+", 220),
|
||||
owner: "system",
|
||||
channel: "#wardriving",
|
||||
},
|
||||
]),
|
||||
});
|
||||
|
||||
const COMPLETE_EXAMPLES = Object.freeze({
|
||||
blackhole: EXAMPLES.blackhole,
|
||||
wildcards: EXAMPLES.wildcards,
|
||||
moderation: EXAMPLES.moderation,
|
||||
system: EXAMPLES.system,
|
||||
...EXAMPLES,
|
||||
system: EXAMPLES.factory,
|
||||
mixed: Object.freeze([
|
||||
{
|
||||
...defaultRule("group-gate-default"),
|
||||
phase: "scope_gate",
|
||||
owner: "scope",
|
||||
type: "class:group",
|
||||
channel: "",
|
||||
incoming: "any",
|
||||
targetKind: "none",
|
||||
target: "",
|
||||
timing: "inherit",
|
||||
scopeGate: "bypass_global",
|
||||
priority: 1,
|
||||
},
|
||||
{
|
||||
...defaultRule("bot-requires-region"),
|
||||
phase: "scope_gate",
|
||||
owner: "scope",
|
||||
type: "class:group",
|
||||
channel: "#bot",
|
||||
incoming: "any",
|
||||
targetKind: "none",
|
||||
target: "",
|
||||
timing: "inherit",
|
||||
scopeGate: "require_allowed",
|
||||
stop: "phase",
|
||||
priority: 200,
|
||||
},
|
||||
...clone(EXAMPLES.blackhole),
|
||||
...clone(EXAMPLES.channel_scope),
|
||||
...clone(EXAMPLES.prefix_rate),
|
||||
...clone(EXAMPLES.moderation),
|
||||
...clone(EXAMPLES.system),
|
||||
{
|
||||
...defaultRule("weak-signal-slow-queue"),
|
||||
phase: "schedule",
|
||||
owner: "filter",
|
||||
type: "any",
|
||||
channel: "",
|
||||
incoming: "any",
|
||||
snrMax: -8,
|
||||
targetKind: "none",
|
||||
target: "",
|
||||
timing: "slow",
|
||||
queue: "low",
|
||||
retryBucket: "bucket:3",
|
||||
retryAttempts: 2,
|
||||
priority: 80,
|
||||
},
|
||||
...clone(EXAMPLES.high_traffic),
|
||||
...clone(EXAMPLES.factory),
|
||||
]),
|
||||
});
|
||||
|
||||
@@ -1160,8 +1108,6 @@
|
||||
pathPrefix: field("path-prefix").value,
|
||||
sender: field("sender").value,
|
||||
tempRadio: field("temp-radio").value,
|
||||
snrMin: field("snr-min").value,
|
||||
snrMax: field("snr-max").value,
|
||||
verdict: field("verdict").value,
|
||||
scopeGate: field("scope-gate").value,
|
||||
targetKind: field("target-kind").value,
|
||||
@@ -1195,8 +1141,6 @@
|
||||
"path-prefix": rule.pathPrefix,
|
||||
sender: rule.sender,
|
||||
"temp-radio": rule.tempRadio,
|
||||
"snr-min": rule.snrMin,
|
||||
"snr-max": rule.snrMax,
|
||||
verdict: rule.verdict,
|
||||
"scope-gate": rule.scopeGate,
|
||||
"target-kind": rule.targetKind,
|
||||
@@ -1225,6 +1169,43 @@
|
||||
field("retry-attempts").disabled = field("retry-bucket").value === "none";
|
||||
}
|
||||
|
||||
function suggestCompatibleExecution(element) {
|
||||
const name = element.getAttribute("data-field");
|
||||
if (name === "scope-gate" && field("scope-gate").value !== "unchanged") {
|
||||
setValue("phase", "scope_gate");
|
||||
setValue("owner", "scope");
|
||||
setValue("target-kind", "none");
|
||||
setValue("retry-bucket", "none");
|
||||
setValue("sender", "");
|
||||
} else if (name === "target-kind" && field("target-kind").value !== "none") {
|
||||
setValue("phase", "rewrite");
|
||||
setValue("scope-gate", "unchanged");
|
||||
setValue("retry-bucket", "none");
|
||||
setValue("sender", "");
|
||||
if (field("target-kind").value === "region" && field("owner").value === "filter") {
|
||||
setValue("owner", "scope");
|
||||
}
|
||||
} else if (name === "retry-bucket" && field("retry-bucket").value !== "none") {
|
||||
setValue("phase", "schedule");
|
||||
setValue("owner", "filter");
|
||||
setValue("scope-gate", "unchanged");
|
||||
setValue("target-kind", "none");
|
||||
setValue("sender", "");
|
||||
} else if (name === "sender" && clean(field("sender").value)) {
|
||||
setValue("phase", "content");
|
||||
setValue("owner", "filter");
|
||||
setValue("scope-gate", "unchanged");
|
||||
setValue("target-kind", "none");
|
||||
setValue("retry-bucket", "none");
|
||||
} else if ((name === "verdict" && field("verdict").value === "drop")
|
||||
|| (name === "rate" && clean(field("rate").value))) {
|
||||
if (field("target-kind").value === "none") setValue("phase", "forward");
|
||||
if (field("owner").value === "scope") {
|
||||
setValue("owner", field("target-kind").value === "region" ? "admin" : "filter");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderWarnings(container, warnings) {
|
||||
container.replaceChildren();
|
||||
warnings.forEach((warning) => {
|
||||
@@ -1389,7 +1370,6 @@
|
||||
regionName: packetField("region-name").value,
|
||||
sender: packetField("sender").value,
|
||||
tempRadio: packetField("temp-radio").checked,
|
||||
snr: packetField("snr").value,
|
||||
blacklist: packetField("blacklist").value === "yes",
|
||||
buckets: packetField("buckets").value,
|
||||
loopLevel: packetField("loop-level").value,
|
||||
@@ -1442,8 +1422,14 @@
|
||||
}
|
||||
|
||||
root.querySelectorAll("[data-field]").forEach((element) => {
|
||||
element.addEventListener("input", updateLive);
|
||||
element.addEventListener("change", updateLive);
|
||||
element.addEventListener("input", () => {
|
||||
suggestCompatibleExecution(element);
|
||||
updateLive();
|
||||
});
|
||||
element.addEventListener("change", () => {
|
||||
suggestCompatibleExecution(element);
|
||||
updateLive();
|
||||
});
|
||||
});
|
||||
root.querySelector("[data-role='reset-form']").addEventListener("click", resetForm);
|
||||
saveButton.addEventListener("click", () => {
|
||||
@@ -1512,7 +1498,6 @@
|
||||
packetField("scope-name").value = "";
|
||||
packetField("region-name").value = "";
|
||||
packetField("sender").value = "";
|
||||
packetField("snr").value = "6";
|
||||
packetField("blacklist").value = "no";
|
||||
packetField("buckets").value = "";
|
||||
packetField("loop-level").value = "0";
|
||||
|
||||
@@ -31,6 +31,25 @@
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.filter-management-safety {
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.75rem 0.9rem;
|
||||
border-left: 0.25rem solid var(--md-primary-fg-color);
|
||||
border-radius: 0.25rem;
|
||||
background: var(--filter-accent);
|
||||
}
|
||||
|
||||
.filter-management-safety strong {
|
||||
display: block;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.filter-management-safety p {
|
||||
margin: 0;
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.filter-tool-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -45,6 +64,46 @@
|
||||
var(--md-default-bg-color);
|
||||
}
|
||||
|
||||
.filter-example-heading {
|
||||
display: grid;
|
||||
flex: 1 0 100%;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.filter-example-heading span {
|
||||
color: color-mix(in srgb, var(--md-default-fg-color) 64%, transparent);
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.filter-example-grid {
|
||||
display: grid;
|
||||
flex: 1 0 100%;
|
||||
grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
.filter-tool .filter-example-grid button {
|
||||
display: grid;
|
||||
gap: 0.3rem;
|
||||
align-content: start;
|
||||
min-height: 4.25rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.filter-example-grid button span {
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.filter-example-grid button code {
|
||||
color: color-mix(in srgb, var(--md-default-fg-color) 65%, transparent);
|
||||
background: transparent;
|
||||
font-size: 0.61rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.35;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.filter-tool-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.1fr) minmax(18rem, 0.9fr);
|
||||
@@ -229,6 +288,24 @@
|
||||
line-height: 1.48;
|
||||
}
|
||||
|
||||
.filter-advanced {
|
||||
margin-top: 1rem;
|
||||
padding: 0.7rem 0.8rem;
|
||||
border: 1px solid var(--filter-border);
|
||||
border-radius: 0.48rem;
|
||||
background: var(--filter-softer);
|
||||
}
|
||||
|
||||
.filter-advanced summary {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filter-advanced[open] summary {
|
||||
margin-bottom: 0.65rem;
|
||||
}
|
||||
|
||||
.filter-live-preview {
|
||||
display: grid;
|
||||
gap: 0.6rem;
|
||||
|
||||
+141
-86
@@ -20,14 +20,70 @@ drafts are not uploaded anywhere.
|
||||
|
||||
## Build a policy
|
||||
|
||||
The presets below reproduce common examples from
|
||||
[Flood Filtering and Moderation](flood_filtering.md) in the proposed rule model.
|
||||
|
||||
<div class="filter-tool" data-filter-tool>
|
||||
<div class="filter-management-safety" role="note">
|
||||
<strong>Remote login and direct routes</strong>
|
||||
<p>
|
||||
This policy controls flood retransmission only. Direct packets carry a
|
||||
supplied route and stay outside the filter, matching today's firmware.
|
||||
Local packet delivery also happens independently of the relay decision.
|
||||
Rules that can limit relayed REQ, RESPONSE, TXT_MSG, ANON_REQ, or PATH
|
||||
traffic receive a prominent warning because they can still reduce
|
||||
multi-hop remote-login reach. The analyzer warns instead of silently
|
||||
exempting those floods, because an exemption would make the documented
|
||||
high-traffic rules behave differently.
|
||||
</p>
|
||||
</div>
|
||||
<div class="filter-tool-toolbar" aria-label="Policy examples">
|
||||
<strong>Load an example:</strong>
|
||||
<button type="button" data-example="blackhole">BlackHole86 rewrite</button>
|
||||
<button type="button" data-example="wildcards">Login and other classes</button>
|
||||
<button type="button" data-example="moderation">Content moderation</button>
|
||||
<button type="button" data-example="system">System forwarding gates</button>
|
||||
<button type="button" data-example="mixed">Full mixed policy</button>
|
||||
<div class="filter-example-heading">
|
||||
<strong>Drop-in replacements for documented settings</strong>
|
||||
<span>Choose one to load its proposed-engine equivalent.</span>
|
||||
</div>
|
||||
<div class="filter-example-grid">
|
||||
<button type="button" data-example="channel_scope">
|
||||
<span>Scope all authenticated #rgdata</span>
|
||||
<code>set flood.channel.scope #rgdata scope=BlackHole86</code>
|
||||
</button>
|
||||
<button type="button" data-example="blackhole">
|
||||
<span>Scope unscoped #rgdata after hop 3</span>
|
||||
<code>type=grp_data hops=4+ channel=#rgdata in=none scope=BlackHole86</code>
|
||||
</button>
|
||||
<button type="button" data-example="scope_rewrite">
|
||||
<span>Rewrite #usa to #BlackHole86</span>
|
||||
<code>channel=#rgdata in=scope:usa scope=BlackHole86</code>
|
||||
</button>
|
||||
<button type="button" data-example="prefix_rate">
|
||||
<span>Rate-limit source path 860C</span>
|
||||
<code>type=any prefix=860C rate=10/min</code>
|
||||
</button>
|
||||
<button type="button" data-example="channel_stop">
|
||||
<span>Stop lower rules for short-hop #rgdata</span>
|
||||
<code>hops=0-2 channel=#rgdata priority=200 stop</code>
|
||||
</button>
|
||||
<button type="button" data-example="high_traffic">
|
||||
<span>Six-rule high-traffic mesh preset</span>
|
||||
<code>req 3+ | response 9+ | grp_data 3+ | login paths 9+</code>
|
||||
</button>
|
||||
<button type="button" data-example="moderation">
|
||||
<span>Limit a Public display name</span>
|
||||
<code>public "Noisy User" rate=5/min</code>
|
||||
</button>
|
||||
<button type="button" data-example="blacklist">
|
||||
<span>Drop a passive-blacklist path</span>
|
||||
<code>set flood.filter any all path=blacklist</code>
|
||||
</button>
|
||||
<button type="button" data-example="factory">
|
||||
<span>Factory OTA and #wardriving rows</span>
|
||||
<code>ota suspend=tempradio | #wardriving hops=5+</code>
|
||||
</button>
|
||||
<button type="button" data-example="wildcards">
|
||||
<span>Login and other wildcard scopes</span>
|
||||
<code>login:* | other:* path=bucket:2</code>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-tool-grid">
|
||||
@@ -40,65 +96,8 @@ drafts are not uploaded anywhere.
|
||||
<button type="button" data-role="reset-form">Reset form</button>
|
||||
</div>
|
||||
|
||||
<h3>Identity and execution</h3>
|
||||
<h3>Common match settings</h3>
|
||||
<div class="filter-form-grid">
|
||||
<label>
|
||||
Stable rule ID
|
||||
<input data-field="id" value="rule-1" maxlength="32" placeholder="blackhole-rewrite">
|
||||
</label>
|
||||
<label>
|
||||
Processing phase
|
||||
<select data-field="phase">
|
||||
<option value="scope_gate">1 - Incoming scope gate</option>
|
||||
<option value="rewrite">2 - Scope rewrite</option>
|
||||
<option value="forward">3 - Forwarding decision</option>
|
||||
<option value="content">4 - Decrypted content</option>
|
||||
<option value="schedule">5 - Scheduling and retry</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Rule owner / capability
|
||||
<select data-field="owner">
|
||||
<option value="scope">ACL 4 - Region/scope manager</option>
|
||||
<option value="filter">ACL 5 - Filter manager</option>
|
||||
<option value="admin">Administrator</option>
|
||||
<option value="system">Firmware-managed system rule</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Priority
|
||||
<input data-field="priority" type="number" min="0" max="255" inputmode="numeric" value="100">
|
||||
</label>
|
||||
<label>
|
||||
Mode
|
||||
<select data-field="mode">
|
||||
<option value="active">Active</option>
|
||||
<option value="shadow">Shadow - observe only</option>
|
||||
<option value="disabled">Disabled</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Stop behavior after a match
|
||||
<select data-field="stop">
|
||||
<option value="none">Continue processing</option>
|
||||
<option value="phase">Stop this phase</option>
|
||||
<option value="policy">Stop later policy phases</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<h3>Match immutable receive-time facts</h3>
|
||||
<div class="filter-form-grid">
|
||||
<label>
|
||||
Route class
|
||||
<select data-field="route">
|
||||
<option value="flood">Any flood route</option>
|
||||
<option value="unscoped_flood">Unscoped flood only</option>
|
||||
<option value="scoped_flood">Transport-scoped flood only</option>
|
||||
<option value="direct">Direct routes</option>
|
||||
<option value="any">Any route</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Payload type or class
|
||||
<select data-field="type">
|
||||
@@ -178,17 +177,14 @@ drafts are not uploaded anywhere.
|
||||
<option value="active">Active</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Minimum SNR dB (optional)
|
||||
<input data-field="snr-min" type="number" min="-30" max="30" step="0.25" placeholder="no minimum">
|
||||
</label>
|
||||
<label>
|
||||
Maximum SNR dB (optional)
|
||||
<input data-field="snr-max" type="number" min="-30" max="30" step="0.25" placeholder="no maximum">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<h3>Accumulate forwarding actions</h3>
|
||||
<h3>Common actions</h3>
|
||||
<p class="filter-field-help">
|
||||
Choosing a phase-specific action automatically selects a compatible
|
||||
phase and ACL owner. Open the advanced section to inspect or override
|
||||
those choices.
|
||||
</p>
|
||||
<div class="filter-form-grid">
|
||||
<label>
|
||||
Forwarding verdict
|
||||
@@ -265,6 +261,68 @@ drafts are not uploaded anywhere.
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<details class="filter-advanced">
|
||||
<summary>Advanced execution, ownership, and flood route</summary>
|
||||
<p class="filter-field-help">
|
||||
Direct packets already carry a supplied path and are intentionally
|
||||
outside this engine. The route condition below only distinguishes
|
||||
unscoped floods from transport-scoped floods.
|
||||
</p>
|
||||
<div class="filter-form-grid">
|
||||
<label>
|
||||
Stable rule ID
|
||||
<input data-field="id" value="rule-1" maxlength="32" placeholder="blackhole-rewrite">
|
||||
</label>
|
||||
<label>
|
||||
Processing phase
|
||||
<select data-field="phase">
|
||||
<option value="scope_gate">1 - Incoming scope gate</option>
|
||||
<option value="rewrite">2 - Scope rewrite</option>
|
||||
<option value="forward">3 - Forwarding decision</option>
|
||||
<option value="content">4 - Decrypted content</option>
|
||||
<option value="schedule">5 - Scheduling and retry</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Rule owner / capability
|
||||
<select data-field="owner">
|
||||
<option value="scope">ACL 4 - Region/scope manager</option>
|
||||
<option value="filter">ACL 5 - Filter manager</option>
|
||||
<option value="admin">Administrator</option>
|
||||
<option value="system">Firmware-managed system rule</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Priority
|
||||
<input data-field="priority" type="number" min="0" max="255" inputmode="numeric" value="100">
|
||||
</label>
|
||||
<label>
|
||||
Mode
|
||||
<select data-field="mode">
|
||||
<option value="active">Active</option>
|
||||
<option value="shadow">Shadow - observe only</option>
|
||||
<option value="disabled">Disabled</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Stop behavior after a match
|
||||
<select data-field="stop">
|
||||
<option value="none">Continue processing</option>
|
||||
<option value="phase">Stop this phase</option>
|
||||
<option value="policy">Stop later policy phases</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Flood route
|
||||
<select data-field="route">
|
||||
<option value="flood">Either flood route</option>
|
||||
<option value="unscoped_flood">Unscoped flood only</option>
|
||||
<option value="scoped_flood">Transport-scoped flood only</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="filter-live-preview">
|
||||
<div>
|
||||
<span class="filter-preview-label">Readable policy definition</span>
|
||||
@@ -326,7 +384,6 @@ drafts are not uploaded anywhere.
|
||||
<select data-packet="route">
|
||||
<option value="unscoped_flood">Unscoped flood</option>
|
||||
<option value="scoped_flood">Transport-scoped flood</option>
|
||||
<option value="direct">Direct route</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
@@ -382,10 +439,6 @@ drafts are not uploaded anywhere.
|
||||
Decrypted sender
|
||||
<input data-packet="sender" placeholder="Noisy User">
|
||||
</label>
|
||||
<label>
|
||||
Received SNR dB
|
||||
<input data-packet="snr" type="number" min="-30" max="30" step="0.25" value="6">
|
||||
</label>
|
||||
<label>
|
||||
Passive blacklist result
|
||||
<select data-packet="blacklist">
|
||||
@@ -473,17 +526,19 @@ drafts are not uploaded anywhere.
|
||||
|
||||
The simulator uses these rules:
|
||||
|
||||
1. Every matcher reads the same immutable receive-time packet facts.
|
||||
2. Rules run by phase, descending priority, then stable rule ID.
|
||||
3. A drop decision is sticky and cannot be undone by a later rule.
|
||||
4. The first matching scope, timing, queue, and retry action in execution order
|
||||
1. Only flood retransmission enters the policy. Direct routing and local packet
|
||||
delivery remain outside it.
|
||||
2. Every matcher reads the same immutable receive-time packet facts.
|
||||
3. Rules run by phase, descending priority, then stable rule ID.
|
||||
4. A drop decision is sticky and cannot be undone by a later rule.
|
||||
5. The first matching scope, timing, queue, and retry action in execution order
|
||||
wins.
|
||||
5. All matching token-bucket rate constraints remain attached to the decision.
|
||||
6. `stop=phase` skips later rules in that phase. `stop=policy` skips later
|
||||
6. All matching token-bucket rate constraints remain attached to the decision.
|
||||
7. `stop=phase` skips later rules in that phase. `stop=policy` skips later
|
||||
configurable rules, but never mandatory packet validation or radio safety.
|
||||
7. Shadow rules report what they would do without changing the decision or
|
||||
8. Shadow rules report what they would do without changing the decision or
|
||||
stopping other rules.
|
||||
8. Expensive facts such as channel authentication, decryption, and path-table
|
||||
9. Expensive facts such as channel authentication, decryption, and path-table
|
||||
lookup are resolved once per packet and reused by every matching rule.
|
||||
|
||||
The byte-budget display is deliberately approximate until the packed firmware
|
||||
|
||||
@@ -27,7 +27,6 @@ function packet(overrides) {
|
||||
regionName: "",
|
||||
sender: "",
|
||||
tempRadio: false,
|
||||
snr: 6,
|
||||
blacklist: false,
|
||||
buckets: [],
|
||||
loopLevel: 0,
|
||||
@@ -118,6 +117,26 @@ test("imports JSON and multiple readable definitions", () => {
|
||||
assert.strictEqual(tool.parsePolicyInput(readable).length, 2);
|
||||
});
|
||||
|
||||
test("loads proposed replacements for the documented commands", () => {
|
||||
Object.values(tool.EXAMPLES).forEach((rules) => rules.forEach(tool.normalizeRule));
|
||||
assert.strictEqual(
|
||||
tool.buildDefinition(tool.EXAMPLES.channel_scope[0]),
|
||||
"policy set rgdata-scope phase=rewrite owner=scope priority=100 when route=flood type=class:group hops=all channel=#rgdata do scope=#BlackHole86 timing=fast"
|
||||
);
|
||||
assert.strictEqual(
|
||||
tool.buildDefinition(tool.EXAMPLES.blackhole[0]),
|
||||
"policy set blackhole-after-hop-3 phase=rewrite owner=scope priority=100 when route=flood type=grp_data hops=4+ channel=#rgdata rx.scope=none do scope=#BlackHole86 timing=fast"
|
||||
);
|
||||
assert.strictEqual(
|
||||
tool.buildDefinition(tool.EXAMPLES.prefix_rate[0]),
|
||||
"policy set prefix-860c-rate phase=forward owner=filter priority=100 when route=flood type=any hops=all path=prefix:860C do rate=10/min burst=10"
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
tool.EXAMPLES.high_traffic.map((rule) => `${rule.type}:${rule.hops}`),
|
||||
["req:3+", "response:9+", "grp_data:3+", "anon_req:9+", "path:9+", "control:1+"]
|
||||
);
|
||||
});
|
||||
|
||||
test("orders by phase, descending priority, and stable ASCII ID", () => {
|
||||
const definitions = [
|
||||
"policy set z-last phase=forward owner=filter priority=20 when route=flood type=any hops=all do tag=z",
|
||||
@@ -206,6 +225,18 @@ test("matches login and future-safe other payload classes", () => {
|
||||
assertToolError(() => tool.normalizePacket(packet({ type: "req" })), /authenticated channel/);
|
||||
});
|
||||
|
||||
test("keeps direct routes and SNR outside the flood policy schema", () => {
|
||||
assertToolError(
|
||||
() => tool.parseDefinition("policy set direct-drop phase=forward owner=filter priority=1 when route=direct type=any hops=all do drop"),
|
||||
/Route matcher is invalid/
|
||||
);
|
||||
assertToolError(() => tool.normalizePacket(packet({ route: "direct" })), /Packet route is invalid/);
|
||||
assertToolError(
|
||||
() => tool.parseDefinition("policy set signal-drop phase=forward owner=filter priority=1 when route=flood type=any hops=all snr=..-8 do drop"),
|
||||
/Unsupported receive-time matcher/
|
||||
);
|
||||
});
|
||||
|
||||
test("matches ordered pbyte prefixes, buckets, and loop thresholds", () => {
|
||||
const prefix = tool.parseDefinition("policy set path-prefix phase=forward owner=filter priority=1 when route=flood type=any hops=all path=prefix:86,0C do drop");
|
||||
const bucket = tool.parseDefinition("policy set path-bucket phase=forward owner=filter priority=1 when route=flood type=any hops=all path=bucket:2 do drop");
|
||||
@@ -233,11 +264,16 @@ test("reports duplicate IDs, rewrite conflicts, and target overflow", () => {
|
||||
assert.ok(tool.policyWarnings(large, "stm32").some((warning) => /exceeds/.test(warning)));
|
||||
});
|
||||
|
||||
test("flags lockout, future-type, spoofing, and policy-stop risks", () => {
|
||||
const broad = tool.parseDefinition("policy set broad phase=forward owner=admin priority=255 when route=any type=any hops=all do drop stop=policy");
|
||||
test("flags remote-login reach, future-type, spoofing, and policy-stop risks", () => {
|
||||
const broad = tool.parseDefinition("policy set broad phase=forward owner=admin priority=255 when route=flood type=any hops=all do drop stop=policy");
|
||||
const other = tool.parseDefinition("policy set future phase=forward owner=filter priority=1 when route=flood type=class:other hops=all do drop");
|
||||
const sender = tool.parseDefinition('policy set sender phase=content owner=filter priority=1 when route=flood type=grp_txt hops=all sender="Noisy User" do drop');
|
||||
assert.ok(tool.ruleWarnings(broad).some((warning) => /remote administration/.test(warning)));
|
||||
const channelOnly = tool.parseDefinition("policy set channel-only phase=forward owner=filter priority=1 when route=flood type=any hops=all channel=#wardriving do drop");
|
||||
assert.ok(tool.ruleWarnings(broad).some((warning) => /Remote-management relay risk/.test(warning)));
|
||||
assert.ok(tool.policyWarnings([broad], "nrf52").some((warning) => /end-to-end flood login reach is not guaranteed/.test(warning)));
|
||||
assert.ok(tool.ruleWarnings(tool.EXAMPLES.high_traffic[0]).some((warning) => /Remote-management reach warning/.test(warning)));
|
||||
assert.ok(!tool.ruleWarnings(channelOnly).some((warning) => /Remote-management/.test(warning)));
|
||||
assert.ok(!tool.ruleWarnings(tool.EXAMPLES.blacklist[0]).some((warning) => /global drop/.test(warning)));
|
||||
assert.ok(tool.ruleWarnings(broad).some((warning) => /mandatory protocol/.test(warning)));
|
||||
assert.ok(tool.ruleWarnings(other).some((warning) => /future types/.test(warning)));
|
||||
assert.ok(tool.ruleWarnings(sender).some((warning) => /spoofable/.test(warning)));
|
||||
|
||||
Reference in New Issue
Block a user