@aglyn/shared-util-email 1.0.0-beta.219 → 1.0.0-beta.221
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/index.d.ts +1 -0
- package/src/index.js +4 -0
- package/src/index.js.map +1 -1
- package/src/lib/hosted-page-screen.d.ts +32 -0
- package/src/lib/hosted-page-screen.js +233 -24
- package/src/lib/hosted-page-screen.js.map +1 -1
- package/src/lib/link-reputation.d.ts +74 -0
- package/src/lib/link-reputation.js +123 -0
- package/src/lib/link-reputation.js.map +1 -0
- package/src/lib/outbound-phishing-screen.d.ts +93 -3
- package/src/lib/outbound-phishing-screen.js +480 -31
- package/src/lib/outbound-phishing-screen.js.map +1 -1
- package/src/lib/outbound-screen-gate.d.ts +4 -3
- package/src/lib/outbound-screen-gate.js +19 -5
- package/src/lib/outbound-screen-gate.js.map +1 -1
- package/src/lib/risk-notice-catalog.d.ts +32 -2
- package/src/lib/risk-notice-catalog.js +86 -55
- package/src/lib/risk-notice-catalog.js.map +1 -1
- package/src/lib/send-email.d.ts +23 -0
- package/src/lib/send-email.js +30 -3
- package/src/lib/send-email.js.map +1 -1
- package/src/lib/sending-domain.d.ts +15 -0
- package/src/lib/sending-domain.js.map +1 -1
- package/src/lib/system-email-catalog.js +34 -8
- package/src/lib/system-email-catalog.js.map +1 -1
- package/src/lib/tenant-emails.generated.js +73 -18
- package/src/lib/tenant-emails.generated.js.map +1 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { PhishingScreenSignal } from './outbound-phishing-screen';
|
|
18
|
+
/** The threat lists a host is looked up against. */
|
|
19
|
+
export type LinkThreatType = 'SOCIAL_ENGINEERING' | 'MALWARE' | 'UNWANTED_SOFTWARE';
|
|
20
|
+
export declare const LINK_THREAT_TYPES: readonly LinkThreatType[];
|
|
21
|
+
/** A host the list names, and what it names it for. */
|
|
22
|
+
export interface HostReputationHit {
|
|
23
|
+
host: string;
|
|
24
|
+
threats: LinkThreatType[];
|
|
25
|
+
}
|
|
26
|
+
/** What one lookup answered, host by host. */
|
|
27
|
+
export interface LinkReputationAnswer {
|
|
28
|
+
/** Hosts the list names. */
|
|
29
|
+
hits: HostReputationHit[];
|
|
30
|
+
/** Hosts the list does not name. */
|
|
31
|
+
clean: string[];
|
|
32
|
+
/** Hosts nobody could answer for: an error, a timeout, a switched-off lookup. */
|
|
33
|
+
unknown: string[];
|
|
34
|
+
}
|
|
35
|
+
export interface LinkReputationLookupOptions {
|
|
36
|
+
/** How long the caller will wait, overall. */
|
|
37
|
+
deadlineMs?: number;
|
|
38
|
+
}
|
|
39
|
+
export type LinkReputationLookup = (hosts: readonly string[], options?: LinkReputationLookupOptions) => Promise<LinkReputationAnswer>;
|
|
40
|
+
/** The most foreign hosts one page, message or redirect is looked up for. */
|
|
41
|
+
export declare const MAX_REPUTATION_HOSTS_PER_LOOKUP = 20;
|
|
42
|
+
/** Installs the lookup. Called once, from `@aglyn/tenant-data-admin`. */
|
|
43
|
+
export declare function setLinkReputationLookup(lookup: LinkReputationLookup | null): void;
|
|
44
|
+
/** The installed lookup, or null. */
|
|
45
|
+
export declare function getLinkReputationLookup(): LinkReputationLookup | null;
|
|
46
|
+
/** Test seam: forget any installed lookup. */
|
|
47
|
+
export declare function resetLinkReputationLookupForTests(): void;
|
|
48
|
+
/** A host as the lookup takes it, or null for anything that is not one. */
|
|
49
|
+
export declare function normalizeReputationHost(host: string): string | null;
|
|
50
|
+
/**
|
|
51
|
+
* The hosts worth asking about: foreign ones. Not the workspace's own, not
|
|
52
|
+
* a domain under one of `excludeDomains` (the platform's own), not a listed
|
|
53
|
+
* brand's own domain and not a common social, maps or review link — those
|
|
54
|
+
* are judged by the screen's rules, and asking about them would only spend
|
|
55
|
+
* lookups. Deduplicated, in the order found, at most
|
|
56
|
+
* {@link MAX_REPUTATION_HOSTS_PER_LOOKUP} unless `max` says otherwise.
|
|
57
|
+
*/
|
|
58
|
+
export declare function foreignHostsForReputation(hosts: readonly string[], options?: {
|
|
59
|
+
ownDomains?: readonly (string | null | undefined)[];
|
|
60
|
+
excludeDomains?: readonly (string | null | undefined)[];
|
|
61
|
+
max?: number;
|
|
62
|
+
}): string[];
|
|
63
|
+
/** A listing, as the screen's signal. */
|
|
64
|
+
export declare function webRiskSignals(hits: readonly HostReputationHit[]): PhishingScreenSignal[];
|
|
65
|
+
/**
|
|
66
|
+
* Look the foreign hosts up through the installed lookup and answer the
|
|
67
|
+
* signals they carry. Nothing installed, nothing foreign, an error: no
|
|
68
|
+
* signals. Never throws.
|
|
69
|
+
*/
|
|
70
|
+
export declare function linkReputationSignals(hosts: readonly string[], options?: {
|
|
71
|
+
ownDomains?: readonly (string | null | undefined)[];
|
|
72
|
+
excludeDomains?: readonly (string | null | undefined)[];
|
|
73
|
+
deadlineMs?: number;
|
|
74
|
+
}): Promise<PhishingScreenSignal[]>;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /*==========================================
|
|
17
|
+
* LINK REPUTATION (AGL-3451): what the phishing screen asks about a host
|
|
18
|
+
* nobody's rules can judge.
|
|
19
|
+
*
|
|
20
|
+
* The screen's own rules catch a page or a message that LOOKS like a lure —
|
|
21
|
+
* a brand's lookalike host, a password field, a brand beside a sign-in. The
|
|
22
|
+
* 2026-10-01 harvesters linked to `temps-juenes.com` and
|
|
23
|
+
* `conservascaorvi.com`, which wear no brand at all, so nothing held them.
|
|
24
|
+
* A host already known to be bad anywhere is caught here instead: every
|
|
25
|
+
* FOREIGN host a page, an email or a redirect points at is looked up against
|
|
26
|
+
* a reputation list (Google Web Risk), and a listed one is a
|
|
27
|
+
* `web-risk-link` signal — STRONG, so it holds for every workspace.
|
|
28
|
+
*
|
|
29
|
+
* Split like the send seam's gate: the pure half lives here (which hosts are
|
|
30
|
+
* foreign, how a listing becomes a signal), and the lookup — a network call
|
|
31
|
+
* and a cache — is installed by `@aglyn/tenant-data-admin`
|
|
32
|
+
* (`web-risk.ts`) through {@link setLinkReputationLookup}. Nothing installed
|
|
33
|
+
* means nothing looked up.
|
|
34
|
+
*
|
|
35
|
+
* ## Not evidence when it fails
|
|
36
|
+
*
|
|
37
|
+
* A lookup that errors or times out says nothing about the host. It never
|
|
38
|
+
* holds by itself; the lookup reports the host as unknown and logs a warning.
|
|
39
|
+
*
|
|
40
|
+
* ## Only the host
|
|
41
|
+
*
|
|
42
|
+
* The lookup sees `https://<host>/` and nothing else: never the path, the
|
|
43
|
+
* query, the page or the message. A host is enough to catch a domain that
|
|
44
|
+
* exists to harvest, and it is the least a third party can be told.
|
|
45
|
+
*=========================================*/ import { isAnyOfficialBrandDomain, isCommonLinkDomain, registrableDomain } from "./outbound-phishing-screen.js";
|
|
46
|
+
export const LINK_THREAT_TYPES = [
|
|
47
|
+
'SOCIAL_ENGINEERING',
|
|
48
|
+
'MALWARE',
|
|
49
|
+
'UNWANTED_SOFTWARE'
|
|
50
|
+
];
|
|
51
|
+
/** The most foreign hosts one page, message or redirect is looked up for. */ export const MAX_REPUTATION_HOSTS_PER_LOOKUP = 20;
|
|
52
|
+
let installedLookup = null;
|
|
53
|
+
/** Installs the lookup. Called once, from `@aglyn/tenant-data-admin`. */ export function setLinkReputationLookup(lookup) {
|
|
54
|
+
installedLookup = lookup;
|
|
55
|
+
}
|
|
56
|
+
/** The installed lookup, or null. */ export function getLinkReputationLookup() {
|
|
57
|
+
return installedLookup;
|
|
58
|
+
}
|
|
59
|
+
/** Test seam: forget any installed lookup. */ export function resetLinkReputationLookupForTests() {
|
|
60
|
+
installedLookup = null;
|
|
61
|
+
}
|
|
62
|
+
const HOST_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?)+$/;
|
|
63
|
+
/** A host as the lookup takes it, or null for anything that is not one. */ export function normalizeReputationHost(host) {
|
|
64
|
+
const value = String(host != null ? host : '').trim().toLowerCase().replace(/\.+$/, '');
|
|
65
|
+
return value.length <= 253 && HOST_PATTERN.test(value) ? value : null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The hosts worth asking about: foreign ones. Not the workspace's own, not
|
|
69
|
+
* a domain under one of `excludeDomains` (the platform's own), not a listed
|
|
70
|
+
* brand's own domain and not a common social, maps or review link — those
|
|
71
|
+
* are judged by the screen's rules, and asking about them would only spend
|
|
72
|
+
* lookups. Deduplicated, in the order found, at most
|
|
73
|
+
* {@link MAX_REPUTATION_HOSTS_PER_LOOKUP} unless `max` says otherwise.
|
|
74
|
+
*/ export function foreignHostsForReputation(hosts, options = {}) {
|
|
75
|
+
var _options_max;
|
|
76
|
+
const under = (list)=>(list != null ? list : []).map((domain)=>normalizeReputationHost(String(domain != null ? domain : ''))).filter((domain)=>Boolean(domain));
|
|
77
|
+
const own = [
|
|
78
|
+
...under(options.ownDomains),
|
|
79
|
+
...under(options.excludeDomains)
|
|
80
|
+
];
|
|
81
|
+
const max = (_options_max = options.max) != null ? _options_max : MAX_REPUTATION_HOSTS_PER_LOOKUP;
|
|
82
|
+
const out = [];
|
|
83
|
+
for (const raw of hosts){
|
|
84
|
+
if (out.length >= max) break;
|
|
85
|
+
const host = normalizeReputationHost(raw);
|
|
86
|
+
if (!host || out.includes(host)) continue;
|
|
87
|
+
if (own.some((domain)=>host === domain || host.endsWith(`.${domain}`))) continue;
|
|
88
|
+
const registrable = registrableDomain(host);
|
|
89
|
+
if (isAnyOfficialBrandDomain(registrable) || isCommonLinkDomain(registrable)) continue;
|
|
90
|
+
out.push(host);
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
/** A listing, as the screen's signal. */ export function webRiskSignals(hits) {
|
|
95
|
+
return hits.map((hit)=>({
|
|
96
|
+
code: 'web-risk-link',
|
|
97
|
+
host: hit.host,
|
|
98
|
+
threats: [
|
|
99
|
+
...new Set(hit.threats)
|
|
100
|
+
].sort()
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Look the foreign hosts up through the installed lookup and answer the
|
|
105
|
+
* signals they carry. Nothing installed, nothing foreign, an error: no
|
|
106
|
+
* signals. Never throws.
|
|
107
|
+
*/ export async function linkReputationSignals(hosts, options = {}) {
|
|
108
|
+
const lookup = installedLookup;
|
|
109
|
+
if (!lookup) return [];
|
|
110
|
+
const foreign = foreignHostsForReputation(hosts, options);
|
|
111
|
+
if (!foreign.length) return [];
|
|
112
|
+
try {
|
|
113
|
+
const answer = await lookup(foreign, {
|
|
114
|
+
deadlineMs: options.deadlineMs
|
|
115
|
+
});
|
|
116
|
+
return webRiskSignals(answer.hits.filter((hit)=>foreign.includes(hit.host)));
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.warn('[link-reputation] the lookup failed — no signal', error);
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
//# sourceMappingURL=link-reputation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/link-reputation.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * LINK REPUTATION (AGL-3451): what the phishing screen asks about a host\n * nobody's rules can judge.\n *\n * The screen's own rules catch a page or a message that LOOKS like a lure —\n * a brand's lookalike host, a password field, a brand beside a sign-in. The\n * 2026-10-01 harvesters linked to `temps-juenes.com` and\n * `conservascaorvi.com`, which wear no brand at all, so nothing held them.\n * A host already known to be bad anywhere is caught here instead: every\n * FOREIGN host a page, an email or a redirect points at is looked up against\n * a reputation list (Google Web Risk), and a listed one is a\n * `web-risk-link` signal — STRONG, so it holds for every workspace.\n *\n * Split like the send seam's gate: the pure half lives here (which hosts are\n * foreign, how a listing becomes a signal), and the lookup — a network call\n * and a cache — is installed by `@aglyn/tenant-data-admin`\n * (`web-risk.ts`) through {@link setLinkReputationLookup}. Nothing installed\n * means nothing looked up.\n *\n * ## Not evidence when it fails\n *\n * A lookup that errors or times out says nothing about the host. It never\n * holds by itself; the lookup reports the host as unknown and logs a warning.\n *\n * ## Only the host\n *\n * The lookup sees `https://<host>/` and nothing else: never the path, the\n * query, the page or the message. A host is enough to catch a domain that\n * exists to harvest, and it is the least a third party can be told.\n *=========================================*/\n\nimport type { PhishingScreenSignal } from './outbound-phishing-screen'\nimport {\n isAnyOfficialBrandDomain,\n isCommonLinkDomain,\n registrableDomain,\n} from './outbound-phishing-screen'\n\n/** The threat lists a host is looked up against. */\nexport type LinkThreatType = 'SOCIAL_ENGINEERING' | 'MALWARE' | 'UNWANTED_SOFTWARE'\n\nexport const LINK_THREAT_TYPES: readonly LinkThreatType[] = [\n 'SOCIAL_ENGINEERING',\n 'MALWARE',\n 'UNWANTED_SOFTWARE',\n]\n\n/** A host the list names, and what it names it for. */\nexport interface HostReputationHit {\n host: string\n threats: LinkThreatType[]\n}\n\n/** What one lookup answered, host by host. */\nexport interface LinkReputationAnswer {\n /** Hosts the list names. */\n hits: HostReputationHit[]\n /** Hosts the list does not name. */\n clean: string[]\n /** Hosts nobody could answer for: an error, a timeout, a switched-off lookup. */\n unknown: string[]\n}\n\nexport interface LinkReputationLookupOptions {\n /** How long the caller will wait, overall. */\n deadlineMs?: number\n}\n\nexport type LinkReputationLookup = (\n hosts: readonly string[],\n options?: LinkReputationLookupOptions,\n) => Promise<LinkReputationAnswer>\n\n/** The most foreign hosts one page, message or redirect is looked up for. */\nexport const MAX_REPUTATION_HOSTS_PER_LOOKUP = 20\n\nlet installedLookup: LinkReputationLookup | null = null\n\n/** Installs the lookup. Called once, from `@aglyn/tenant-data-admin`. */\nexport function setLinkReputationLookup(lookup: LinkReputationLookup | null): void {\n installedLookup = lookup\n}\n\n/** The installed lookup, or null. */\nexport function getLinkReputationLookup(): LinkReputationLookup | null {\n return installedLookup\n}\n\n/** Test seam: forget any installed lookup. */\nexport function resetLinkReputationLookupForTests(): void {\n installedLookup = null\n}\n\nconst HOST_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?)+$/\n\n/** A host as the lookup takes it, or null for anything that is not one. */\nexport function normalizeReputationHost(host: string): string | null {\n const value = String(host ?? '')\n .trim()\n .toLowerCase()\n .replace(/\\.+$/, '')\n return value.length <= 253 && HOST_PATTERN.test(value) ? value : null\n}\n\n/**\n * The hosts worth asking about: foreign ones. Not the workspace's own, not\n * a domain under one of `excludeDomains` (the platform's own), not a listed\n * brand's own domain and not a common social, maps or review link — those\n * are judged by the screen's rules, and asking about them would only spend\n * lookups. Deduplicated, in the order found, at most\n * {@link MAX_REPUTATION_HOSTS_PER_LOOKUP} unless `max` says otherwise.\n */\nexport function foreignHostsForReputation(\n hosts: readonly string[],\n options: {\n ownDomains?: readonly (string | null | undefined)[]\n excludeDomains?: readonly (string | null | undefined)[]\n max?: number\n } = {},\n): string[] {\n const under = (list: readonly (string | null | undefined)[] | undefined) =>\n (list ?? [])\n .map((domain) => normalizeReputationHost(String(domain ?? '')))\n .filter((domain): domain is string => Boolean(domain))\n const own = [...under(options.ownDomains), ...under(options.excludeDomains)]\n const max = options.max ?? MAX_REPUTATION_HOSTS_PER_LOOKUP\n const out: string[] = []\n for (const raw of hosts) {\n if (out.length >= max) break\n const host = normalizeReputationHost(raw)\n if (!host || out.includes(host)) continue\n if (own.some((domain) => host === domain || host.endsWith(`.${domain}`))) continue\n const registrable = registrableDomain(host)\n if (isAnyOfficialBrandDomain(registrable) || isCommonLinkDomain(registrable)) continue\n out.push(host)\n }\n return out\n}\n\n/** A listing, as the screen's signal. */\nexport function webRiskSignals(hits: readonly HostReputationHit[]): PhishingScreenSignal[] {\n return hits.map((hit) => ({\n code: 'web-risk-link' as const,\n host: hit.host,\n threats: [...new Set(hit.threats)].sort(),\n }))\n}\n\n/**\n * Look the foreign hosts up through the installed lookup and answer the\n * signals they carry. Nothing installed, nothing foreign, an error: no\n * signals. Never throws.\n */\nexport async function linkReputationSignals(\n hosts: readonly string[],\n options: {\n ownDomains?: readonly (string | null | undefined)[]\n excludeDomains?: readonly (string | null | undefined)[]\n deadlineMs?: number\n } = {},\n): Promise<PhishingScreenSignal[]> {\n const lookup = installedLookup\n if (!lookup) return []\n const foreign = foreignHostsForReputation(hosts, options)\n if (!foreign.length) return []\n try {\n const answer = await lookup(foreign, { deadlineMs: options.deadlineMs })\n return webRiskSignals(answer.hits.filter((hit) => foreign.includes(hit.host)))\n } catch (error) {\n console.warn('[link-reputation] the lookup failed — no signal', error)\n return []\n }\n}\n"],"names":["isAnyOfficialBrandDomain","isCommonLinkDomain","registrableDomain","LINK_THREAT_TYPES","MAX_REPUTATION_HOSTS_PER_LOOKUP","installedLookup","setLinkReputationLookup","lookup","getLinkReputationLookup","resetLinkReputationLookupForTests","HOST_PATTERN","normalizeReputationHost","host","value","String","trim","toLowerCase","replace","length","test","foreignHostsForReputation","hosts","options","under","list","map","domain","filter","Boolean","own","ownDomains","excludeDomains","max","out","raw","includes","some","endsWith","registrable","push","webRiskSignals","hits","hit","code","threats","Set","sort","linkReputationSignals","foreign","answer","deadlineMs","error","console","warn"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2CA6B2C,GAG3C,SACEA,wBAAwB,EACxBC,kBAAkB,EAClBC,iBAAiB,QACZ,gCAA4B;AAKnC,OAAO,MAAMC,oBAA+C;IAC1D;IACA;IACA;CACD,CAAA;AA4BD,2EAA2E,GAC3E,OAAO,MAAMC,kCAAkC,GAAE;AAEjD,IAAIC,kBAA+C;AAEnD,uEAAuE,GACvE,OAAO,SAASC,wBAAwBC,MAAmC;IACzEF,kBAAkBE;AACpB;AAEA,mCAAmC,GACnC,OAAO,SAASC;IACd,OAAOH;AACT;AAEA,4CAA4C,GAC5C,OAAO,SAASI;IACdJ,kBAAkB;AACpB;AAEA,MAAMK,eAAe;AAErB,yEAAyE,GACzE,OAAO,SAASC,wBAAwBC,IAAY;IAClD,MAAMC,QAAQC,OAAOF,eAAAA,OAAQ,IAC1BG,IAAI,GACJC,WAAW,GACXC,OAAO,CAAC,QAAQ;IACnB,OAAOJ,MAAMK,MAAM,IAAI,OAAOR,aAAaS,IAAI,CAACN,SAASA,QAAQ;AACnE;AAEA;;;;;;;CAOC,GACD,OAAO,SAASO,0BACdC,KAAwB,EACxBC,UAII,CAAC,CAAC;QAOMA;IALZ,MAAMC,QAAQ,CAACC,OACb,CAACA,eAAAA,OAAQ,EAAE,EACRC,GAAG,CAAC,CAACC,SAAWf,wBAAwBG,OAAOY,iBAAAA,SAAU,MACzDC,MAAM,CAAC,CAACD,SAA6BE,QAAQF;IAClD,MAAMG,MAAM;WAAIN,MAAMD,QAAQQ,UAAU;WAAMP,MAAMD,QAAQS,cAAc;KAAE;IAC5E,MAAMC,OAAMV,eAAAA,QAAQU,GAAG,YAAXV,eAAelB;IAC3B,MAAM6B,MAAgB,EAAE;IACxB,KAAK,MAAMC,OAAOb,MAAO;QACvB,IAAIY,IAAIf,MAAM,IAAIc,KAAK;QACvB,MAAMpB,OAAOD,wBAAwBuB;QACrC,IAAI,CAACtB,QAAQqB,IAAIE,QAAQ,CAACvB,OAAO;QACjC,IAAIiB,IAAIO,IAAI,CAAC,CAACV,SAAWd,SAASc,UAAUd,KAAKyB,QAAQ,CAAC,CAAC,CAAC,EAAEX,QAAQ,IAAI;QAC1E,MAAMY,cAAcpC,kBAAkBU;QACtC,IAAIZ,yBAAyBsC,gBAAgBrC,mBAAmBqC,cAAc;QAC9EL,IAAIM,IAAI,CAAC3B;IACX;IACA,OAAOqB;AACT;AAEA,uCAAuC,GACvC,OAAO,SAASO,eAAeC,IAAkC;IAC/D,OAAOA,KAAKhB,GAAG,CAAC,CAACiB,MAAS,CAAA;YACxBC,MAAM;YACN/B,MAAM8B,IAAI9B,IAAI;YACdgC,SAAS;mBAAI,IAAIC,IAAIH,IAAIE,OAAO;aAAE,CAACE,IAAI;QACzC,CAAA;AACF;AAEA;;;;CAIC,GACD,OAAO,eAAeC,sBACpB1B,KAAwB,EACxBC,UAII,CAAC,CAAC;IAEN,MAAMf,SAASF;IACf,IAAI,CAACE,QAAQ,OAAO,EAAE;IACtB,MAAMyC,UAAU5B,0BAA0BC,OAAOC;IACjD,IAAI,CAAC0B,QAAQ9B,MAAM,EAAE,OAAO,EAAE;IAC9B,IAAI;QACF,MAAM+B,SAAS,MAAM1C,OAAOyC,SAAS;YAAEE,YAAY5B,QAAQ4B,UAAU;QAAC;QACtE,OAAOV,eAAeS,OAAOR,IAAI,CAACd,MAAM,CAAC,CAACe,MAAQM,QAAQb,QAAQ,CAACO,IAAI9B,IAAI;IAC7E,EAAE,OAAOuC,OAAO;QACdC,QAAQC,IAAI,CAAC,mDAAmDF;QAChE,OAAO,EAAE;IACX;AACF"}
|
|
@@ -58,9 +58,22 @@ export interface PhishingScreenBrand {
|
|
|
58
58
|
}
|
|
59
59
|
export declare const PHISHING_SCREEN_BRANDS: readonly PhishingScreenBrand[];
|
|
60
60
|
/**
|
|
61
|
-
* Account
|
|
62
|
-
*
|
|
63
|
-
|
|
61
|
+
* Account and credential phrasing: the lure that asks the reader to DO
|
|
62
|
+
* something with an account — verify it, confirm it, sign in to it.
|
|
63
|
+
*/
|
|
64
|
+
export declare const ACCOUNT_LURE_PATTERNS: readonly RegExp[];
|
|
65
|
+
/**
|
|
66
|
+
* Document-share phrasing (AGL-3447): the lure that says a file is waiting
|
|
67
|
+
* behind a secure door — "Secure Document Access Portal", "Continue to
|
|
68
|
+
* Document", "shared a file with you", "encrypted message". The credential
|
|
69
|
+
* harvest is on the far side of the button, so these are the words the page
|
|
70
|
+
* or the mail itself carries.
|
|
71
|
+
*/
|
|
72
|
+
export declare const DOCUMENT_SHARE_LURE_PATTERNS: readonly RegExp[];
|
|
73
|
+
/**
|
|
74
|
+
* Every lure — the half of the three-part lure that asks the reader to act.
|
|
75
|
+
* Only ever counted beside a brand AND a foreign link; alone, several of
|
|
76
|
+
* these are ordinary transactional copy.
|
|
64
77
|
*/
|
|
65
78
|
export declare const PHISHING_LURE_PATTERNS: readonly RegExp[];
|
|
66
79
|
/** A signal the screen found. */
|
|
@@ -97,6 +110,65 @@ export type PhishingScreenSignal = {
|
|
|
97
110
|
code: 'brand-action-page';
|
|
98
111
|
brand: string;
|
|
99
112
|
action: string;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The same, read over the page as a WHOLE (AGL-3447): a brand that is not
|
|
116
|
+
* the workspace's anywhere on a page that carries an account or
|
|
117
|
+
* document-share lure anywhere, or whose call to action leaves the site
|
|
118
|
+
* worded as one, and that links away (`hosted-page-screen.ts`). Soft.
|
|
119
|
+
*/
|
|
120
|
+
| {
|
|
121
|
+
code: 'brand-lure-page';
|
|
122
|
+
brand: string;
|
|
123
|
+
lure: string;
|
|
124
|
+
host: string;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* A published page's call to action — its only one, or one worded as an
|
|
128
|
+
* account or document action — sends visitors to a host that is not the
|
|
129
|
+
* site's, a listed brand's or a common link, on a page that carries an
|
|
130
|
+
* account or document-share lure (`hosted-page-screen.ts`, AGL-3447). No
|
|
131
|
+
* brand needed. Soft.
|
|
132
|
+
*/
|
|
133
|
+
| {
|
|
134
|
+
code: 'offsite-action-page';
|
|
135
|
+
action: string;
|
|
136
|
+
lure: string;
|
|
137
|
+
host: string;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* A site redirect whose path reads as an account or document-share lure
|
|
141
|
+
* sends visitors to such a host (`screenSiteRedirect`, AGL-3447). Soft.
|
|
142
|
+
*/
|
|
143
|
+
| {
|
|
144
|
+
code: 'offsite-redirect';
|
|
145
|
+
source: string;
|
|
146
|
+
lure: string;
|
|
147
|
+
host: string;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Account or password phrasing beside a link elsewhere that carries the
|
|
151
|
+
* reader's own address (AGL-3453). STRONG when the address is the whole
|
|
152
|
+
* fragment (`#victim@example.com`), which only a landing page's script
|
|
153
|
+
* reads; soft when it is a query parameter or part of a longer fragment.
|
|
154
|
+
* The address itself is never stored.
|
|
155
|
+
*/
|
|
156
|
+
| {
|
|
157
|
+
code: 'recipient-prefill-link';
|
|
158
|
+
host: string;
|
|
159
|
+
lure: string;
|
|
160
|
+
place: 'fragment' | 'parameter';
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* A link, embed, redirect or form action whose host Google Web Risk lists
|
|
164
|
+
* as phishing, malware or unwanted software (AGL-3451,
|
|
165
|
+
* `link-reputation.ts`). Strong: a harvester already known to be bad
|
|
166
|
+
* anywhere is bad whatever page or message surrounds it.
|
|
167
|
+
*/
|
|
168
|
+
| {
|
|
169
|
+
code: 'web-risk-link';
|
|
170
|
+
host: string;
|
|
171
|
+
threats: string[];
|
|
100
172
|
};
|
|
101
173
|
export interface PhishingScreenInput {
|
|
102
174
|
subject?: string | null;
|
|
@@ -163,6 +235,21 @@ declare function isAnyOfficialDomain(registrable: string): boolean;
|
|
|
163
235
|
export declare const MEDIA_EMBED_HOSTS: ReadonlySet<string>;
|
|
164
236
|
/** The brand a host impersonates, or null. */
|
|
165
237
|
export declare function lookalikeBrandForHost(host: string): PhishingScreenBrand | null;
|
|
238
|
+
/**
|
|
239
|
+
* What a reader SEES in a body (AGL-3453): its text with the markup taken
|
|
240
|
+
* out, the way a mail client or a browser draws it.
|
|
241
|
+
*
|
|
242
|
+
* A kit splits every letter of a brand and a lure with a tag —
|
|
243
|
+
* `G<SPAN class=…>o<SPAN …>o…gle`, `P<SPAN …>a<SPAN …>s…word E<SPAN …>x…pired`
|
|
244
|
+
* — so the raw HTML never contains the word a reader reads. Tags are
|
|
245
|
+
* therefore removed with NO separator, which rejoins the letters; `<style>`,
|
|
246
|
+
* `<script>` and `<head>` are dropped with their contents; comments and
|
|
247
|
+
* zero-width characters are dropped; entities are decoded; whitespace is
|
|
248
|
+
* collapsed. A second reading that puts a space where a line or block
|
|
249
|
+
* breaks is added when it differs, so two words in two paragraphs are still
|
|
250
|
+
* two words. Links are NOT read from this: an href lives inside a tag.
|
|
251
|
+
*/
|
|
252
|
+
export declare function visibleTextOf(html: string | null | undefined): string;
|
|
166
253
|
/** The hosts every link in the text points at, lowercased. */
|
|
167
254
|
export declare function linkHostsIn(text: string): string[];
|
|
168
255
|
/** Lowercased alphanumerics only, so "Wells-Fargo Advisors" contains `wellsfargo`. */
|
|
@@ -183,6 +270,8 @@ export declare function isCommonLinkDomain(registrable: string): boolean;
|
|
|
183
270
|
export declare function screenOutboundEmail(input: PhishingScreenInput): PhishingScreenVerdict;
|
|
184
271
|
/** The brand's label for a stored id, for the staff surface. */
|
|
185
272
|
export declare function phishingScreenBrandLabel(id: string): string;
|
|
273
|
+
/** "phishing or social engineering and malware", from Web Risk's threat types. */
|
|
274
|
+
export declare function describeLinkThreats(threats: readonly string[]): string;
|
|
186
275
|
/** One line per signal, in words staff read on the review row. */
|
|
187
276
|
export declare function describePhishingScreenSignals(signals: readonly PhishingScreenSignal[]): string[];
|
|
188
277
|
/** A workspace younger than this many days has the soft rules applied. */
|
|
@@ -202,6 +291,7 @@ export declare const ACCOUNT_SUBDOMAIN_HOSTS: ReadonlySet<string>;
|
|
|
202
291
|
export declare function phishingSignalTier(signal: {
|
|
203
292
|
code: string;
|
|
204
293
|
host?: string;
|
|
294
|
+
place?: string;
|
|
205
295
|
}): PhishingSignalTier;
|
|
206
296
|
/** Is a workspace of this age (days, `null` = unreadable) screened by the soft rules? */
|
|
207
297
|
export declare function isYoungWorkspaceAge(ageDays: number | null | undefined): boolean;
|