mirror of
https://github.com/jakejarvis/rdapper.git
synced 2026-09-22 23:05:31 -04:00
fix: tighten isPrivacyName to avoid false positives on ambiguous privacy words
- Split keyword list into `PRIVACY_STRONG_KEYWORDS` (matched as plain substrings) and two weaker sets (`PRIVACY_WEAK_WORDS` / `PRIVACY_CONTEXT_WORDS`) that only trigger when at least two distinct terms appear together, preventing names like "Private Equity Partners LLC" or "Protection One" from being misclassified - Remove empty-placeholder values (`-`, `n/a`, `none`, etc.) from the privacy check; they are not privacy notices and should be handled separately by callers - Add `privacy.test.ts` covering redaction notices, known privacy-service names, ordinary names with ambiguous words, and empty placeholders - Update README comment to clarify that `privacyEnabled` is set by name heuristics (privacy-service and redaction phrases) or RFC 9537 redactions
This commit is contained in:
@@ -555,7 +555,7 @@ interface DomainRecord {
|
||||
country?: string;
|
||||
countryCode?: string;
|
||||
}>;
|
||||
privacyEnabled?: boolean; // registrant appears privacy-redacted based on keyword heuristics or RFC 9537 redactions
|
||||
privacyEnabled?: boolean; // registrant appears privacy-redacted based on name heuristics (privacy-service and redaction phrases) or RFC 9537 redactions
|
||||
redactions?: Array<{
|
||||
name: string; // e.g. "Registrant Email"
|
||||
prePath?: string;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { isPrivacyName } from "./privacy";
|
||||
|
||||
test("isPrivacyName flags redaction notices and privacy services", () => {
|
||||
for (const v of [
|
||||
"REDACTED FOR PRIVACY",
|
||||
"Data Redacted",
|
||||
"Privacy Protect, LLC",
|
||||
"WhoisGuard Protected",
|
||||
"Domains By Proxy, LLC",
|
||||
"Private Registration",
|
||||
"Domain Protection Services",
|
||||
"Identity Protection Service",
|
||||
"Withheld for Privacy ehf",
|
||||
"Datos Privados",
|
||||
]) {
|
||||
expect(isPrivacyName(v), v).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test("isPrivacyName does not flag ordinary names with ambiguous words", () => {
|
||||
for (const v of ["Private Equity Partners LLC", "Protection One", "Jane Private", "Acme Inc"]) {
|
||||
expect(isPrivacyName(v), v).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
test("isPrivacyName does not treat empty placeholders as privacy", () => {
|
||||
for (const v of ["-", "n/a", "N/A", "none", "not available"]) {
|
||||
expect(isPrivacyName(v), v).toBe(false);
|
||||
}
|
||||
});
|
||||
+43
-23
@@ -1,41 +1,61 @@
|
||||
export const PRIVACY_NAME_KEYWORDS = [
|
||||
"redacted",
|
||||
"privacy",
|
||||
"private",
|
||||
/**
|
||||
* Phrases that indicate privacy/redaction on their own. Matched as case-insensitive substrings.
|
||||
*/
|
||||
export const PRIVACY_STRONG_KEYWORDS = [
|
||||
"redacted", // also covers "redacted for privacy", "redacted.forprivacy"
|
||||
"withheld",
|
||||
"not disclosed",
|
||||
"protected",
|
||||
"protection",
|
||||
"privado", // Spanish
|
||||
"datos privados", // Spanish
|
||||
"data protected",
|
||||
"data redacted",
|
||||
"gdpr redacted",
|
||||
"gdpr masked",
|
||||
"non-public data",
|
||||
"statutory masking",
|
||||
"redacted.forprivacy",
|
||||
"registration private",
|
||||
"private registration",
|
||||
"hidden upon user request",
|
||||
"not available from registry",
|
||||
"whois privacy",
|
||||
"whoisguard",
|
||||
"privacy protect",
|
||||
"privacy service",
|
||||
"domain privacy",
|
||||
"contact privacy",
|
||||
"domains by proxy",
|
||||
"proxy service",
|
||||
"for privacy",
|
||||
];
|
||||
|
||||
// Completely unusable/empty values that should be filtered
|
||||
export const NO_DATA_VALUES = [
|
||||
"-",
|
||||
".",
|
||||
"n/a",
|
||||
"na",
|
||||
"no data",
|
||||
"not available",
|
||||
"not applicable",
|
||||
"none",
|
||||
/**
|
||||
* Words too ambiguous to trust alone ("Private Equity LLC", "Protection One"). They only count
|
||||
* when at least two distinct terms from WEAK + CONTEXT appear as whole words.
|
||||
*/
|
||||
const PRIVACY_WEAK_WORDS = ["privacy", "private", "protect", "protected", "protection"];
|
||||
const PRIVACY_CONTEXT_WORDS = [
|
||||
"whois",
|
||||
"proxy",
|
||||
"domain",
|
||||
"domains",
|
||||
"registration",
|
||||
"guard",
|
||||
"service",
|
||||
"services",
|
||||
"masked",
|
||||
"anonymous",
|
||||
"identity",
|
||||
"contact",
|
||||
];
|
||||
const WEAK_WORD_RE = new RegExp(`\\b(?:${PRIVACY_WEAK_WORDS.join("|")})\\b`, "g");
|
||||
const CONTEXT_WORD_RE = new RegExp(
|
||||
`\\b(?:${[...PRIVACY_WEAK_WORDS, ...PRIVACY_CONTEXT_WORDS].join("|")})\\b`,
|
||||
"g",
|
||||
);
|
||||
|
||||
/** True when a registrant name/organization looks like a privacy service or redaction notice. */
|
||||
export function isPrivacyName(value: string): boolean {
|
||||
const v = value.toLowerCase().trim();
|
||||
// Check for complete no-data values
|
||||
if (NO_DATA_VALUES.includes(v)) return true;
|
||||
// Check for privacy keywords
|
||||
return PRIVACY_NAME_KEYWORDS.some((k) => v.includes(k));
|
||||
if (PRIVACY_STRONG_KEYWORDS.some((k) => v.includes(k))) return true;
|
||||
if (!WEAK_WORD_RE.test(v)) return false;
|
||||
WEAK_WORD_RE.lastIndex = 0;
|
||||
return new Set(v.match(CONTEXT_WORD_RE)).size >= 2;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user