@slicedearth/whoisleuth-cli 2.7.0 → 2.8.0

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 CHANGED
@@ -33,7 +33,7 @@
33
33
  "bugs": {
34
34
  "url": "https://github.com/slicedearth/whoisleuth/issues"
35
35
  },
36
- "version": "2.7.0",
36
+ "version": "2.8.0",
37
37
  "dependencies": {
38
38
  "@nuintun/qrcode": "5.0.3",
39
39
  "@peculiar/x509": "2.2.0",
@@ -0,0 +1,42 @@
1
+ import { hammingDistanceHex, isInformativePerceptualHash } from "./perceptual-hash-comparison.mjs";
2
+ function normalizedDomain(value) {
3
+ return value.trim().toLowerCase().replace(/\.$/u, '');
4
+ }
5
+ /** Shared exact comparison and role precedence for browser and CLI consumers. */
6
+ export function profileDomainMatch(domain, profile = null) {
7
+ if (!profile || !domain)
8
+ return null;
9
+ const target = normalizedDomain(domain);
10
+ const lists = [
11
+ ['official', profile.officialDomains, 'Exact official-domain declaration'],
12
+ ['partner', profile.approvedPartnerDomains, 'Exact approved-partner declaration'],
13
+ ['allowlisted', profile.allowlistedDomains, 'Exact domain allowlist entry'],
14
+ ];
15
+ for (const [kind, values, reason] of lists) {
16
+ const matched = values.find(value => normalizedDomain(value) === target);
17
+ if (matched !== undefined)
18
+ return { kind, matchedDomain: normalizedDomain(matched), reason };
19
+ }
20
+ return null;
21
+ }
22
+ export function profileDomainKind(domain, profile = null) {
23
+ return profileDomainMatch(domain, profile)?.kind ?? null;
24
+ }
25
+ export function profileSignals(domain, evidence, profile = null) {
26
+ const trusted = profileDomainKind(domain, profile);
27
+ if (!profile || trusted)
28
+ return { trusted, faviconMatch: false, faviconNearMatch: false, reusesOfficialAssets: false };
29
+ const exact = Boolean(evidence.faviconHash && profile.officialFaviconHash && evidence.faviconHash === profile.officialFaviconHash);
30
+ const distance = isInformativePerceptualHash(evidence.faviconPHash) && isInformativePerceptualHash(profile.officialFaviconPHash)
31
+ ? hammingDistanceHex(evidence.faviconPHash, profile.officialFaviconPHash)
32
+ : null;
33
+ const official = new Set(profile.officialDomains.map(normalizedDomain));
34
+ const reused = Array.isArray(evidence.externalAssetHosts)
35
+ && evidence.externalAssetHosts.some((host) => typeof host === 'string' && official.has(normalizedDomain(host)));
36
+ return {
37
+ trusted: null,
38
+ faviconMatch: exact,
39
+ faviconNearMatch: !exact && distance !== null && distance <= 8,
40
+ reusesOfficialAssets: reused,
41
+ };
42
+ }
@@ -1,5 +1,6 @@
1
1
  import { normalizeBulkSessionResult, } from "../workspace/bulk-session-model.mjs";
2
2
  import { SORTED_JSON_V2, sha256ArtifactDigestV2 } from "../evidence/artifact-integrity.mjs";
3
+ import { webCollectionAllowsComparison } from "../evidence/collection-quality.mjs";
3
4
  import { BULK_REVIEW_STALE_AFTER_DAYS } from "./bulk-review-policy.mjs";
4
5
  import { BULK_DOMAIN_COMPARISON_EXPORT_VERSION, BULK_DOMAIN_COMPARISON_SCHEMA, BULK_DOMAIN_COMPARISON_VERSION, } from "../contracts/investigation-portability.mjs";
5
6
  export { BULK_DOMAIN_COMPARISON_EXPORT_VERSION, BULK_DOMAIN_COMPARISON_SCHEMA, BULK_DOMAIN_COMPARISON_VERSION };
@@ -90,7 +91,7 @@ function row(id, category, label, left, right, method, context, limitations = []
90
91
  label,
91
92
  left: options.retained === false ? 'Not recorded in compact Bulk evidence' : display(left),
92
93
  right: options.retained === false ? 'Not recorded in compact Bulk evidence' : display(right),
93
- state: state(left, right, context.leftSourceState, context.rightSourceState, options),
94
+ state: context.webComparable === false ? 'unavailable' : state(left, right, context.leftSourceState, context.rightSourceState, options),
94
95
  method: text(method, 320),
95
96
  source: text(context.source, 80),
96
97
  leftSourceState: context.leftSourceState,
@@ -98,7 +99,10 @@ function row(id, category, label, left, right, method, context, limitations = []
98
99
  observedAt: context.observedAt,
99
100
  leftEvidenceHref: context.leftEvidenceHref,
100
101
  rightEvidenceHref: context.rightEvidenceHref,
101
- limitations: limitations.map((item) => text(item, 320)).filter(Boolean).slice(0, 6),
102
+ limitations: [
103
+ ...(context.webComparable === false ? ['Incomplete or unknown web collection; retained values are not comparable.'] : []),
104
+ ...limitations,
105
+ ].map((item) => text(item, 320)).filter(Boolean).slice(0, 6),
102
106
  };
103
107
  }
104
108
  function sourceSummary(value) {
@@ -157,6 +161,11 @@ export function buildBulkDomainComparison(leftRaw, rightRaw, observedAtRaw = nul
157
161
  const registry = context(left, right, 'Registry and registrar evidence', ['rdap', 'availability'], observedAt, options);
158
162
  const dns = context(left, right, 'DNS evidence', ['dns'], observedAt, options);
159
163
  const http = context(left, right, 'HTTP and static page evidence', ['http'], observedAt, options);
164
+ const webContext = (field, base = http) => ({
165
+ ...base,
166
+ webComparable: webCollectionAllowsComparison(field, left.webCollectionQuality, left.scanDepth)
167
+ && webCollectionAllowsComparison(field, right.webCollectionQuality, right.scanDepth),
168
+ });
160
169
  const tls = context(left, right, 'TLS evidence', ['tls'], observedAt, options);
161
170
  const compactTechnology = comparisonEvidenceContext(left, right, 'technology', {
162
171
  ...http,
@@ -203,13 +212,13 @@ export function buildBulkDomainComparison(leftRaw, rightRaw, observedAtRaw = nul
203
212
  row('null-mx', 'mail', 'Null MX observed', left.hasNullMx, right.hasNullMx, 'Compact Deep DNS observation', dns),
204
213
  row('spf', 'mail', 'SPF observed', left.hasSpf, right.hasSpf, 'Compact Deep DNS observation', dns),
205
214
  row('dmarc', 'mail', 'DMARC observed', left.hasDmarc, right.hasDmarc, 'Compact Deep DNS observation', dns),
206
- row('website', 'web', 'Website activity', left.activityStatus, right.activityStatus, 'Bounded homepage observation', http),
207
- row('page-title', 'identity', 'Page title', left.pageTitle, right.pageTitle, 'Normalised bounded title equality', http),
208
- row('favicon', 'identity', 'Favicon fingerprint', left.faviconHash, right.faviconHash, 'Exact SHA-256 comparison', http),
215
+ row('website', 'web', 'Website activity', left.activityStatus, right.activityStatus, 'Bounded homepage observation', webContext('activityStatus')),
216
+ row('page-title', 'identity', 'Page title', left.pageTitle, right.pageTitle, 'Normalised bounded title equality', webContext('pageTitle')),
217
+ row('favicon', 'identity', 'Favicon fingerprint', left.faviconHash, right.faviconHash, 'Exact SHA-256 comparison', webContext('faviconHash')),
209
218
  row('tracking', 'identity', 'Tracking identifiers', left.relationship.trackingIdentifiers, right.relationship.trackingIdentifiers, 'Normalised exact set comparison', http, ['Shared identifiers are an investigative lead, not proof of ownership.']),
210
- row('password-field', 'identity', 'Password field', left.hasPasswordField, right.hasPasswordField, 'Bounded static form observation', http),
211
- row('phishing-language', 'identity', 'Phishing-language indicator', left.phishingLanguageMatch, right.phishingLanguageMatch, 'Bounded explainable page-language signal', http, ['A wording match is a review lead, not proof of malicious intent.']),
212
- row('official-assets', 'identity', 'Official asset reuse', profileContextsComparable ? left.reusesOfficialAssets : null, profileContextsComparable ? right.reusesOfficialAssets : null, 'Configured Brand Profile comparison', profileContext, profileLimitations),
219
+ row('password-field', 'identity', 'Password field', left.hasPasswordField, right.hasPasswordField, 'Bounded static form observation', webContext('hasPasswordField')),
220
+ row('phishing-language', 'identity', 'Phishing-language indicator', left.phishingLanguageMatch, right.phishingLanguageMatch, 'Bounded explainable page-language signal', webContext('phishingLanguageMatch'), ['A wording match is a review lead, not proof of malicious intent.']),
221
+ row('official-assets', 'identity', 'Official asset reuse', profileContextsComparable ? left.reusesOfficialAssets : null, profileContextsComparable ? right.reusesOfficialAssets : null, 'Configured Brand Profile comparison', webContext('reusesOfficialAssets', profileContext), profileLimitations),
213
222
  row('technology', 'technology', 'Technology identifiers', left.comparisonEvidence?.technology.ids ?? [], right.comparisonEvidence?.technology.ids ?? [], 'Normalised exact set comparison of at most 12 curated identifiers', compactTechnology, ['An unmatched identifier is not evidence that a technology is absent. Shared technologies do not establish common ownership or control.']),
214
223
  row('source-health', 'source', 'Recorded source coverage', sourceSummary(left), sourceSummary(right), 'Source-by-source compact state comparison', source, ['A source-state difference can reflect collection conditions rather than a domain change.']),
215
224
  ];
@@ -516,7 +516,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
516
516
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
517
517
  SOFTWARE.
518
518
  ================================================================================
519
- asn1js@3.0.10
519
+ asn1js@3.1.0
520
520
  Relationship: transitive production dependency
521
521
  Declared licence: BSD-3-Clause
522
522
  Licence source: LICENSE