@slicedearth/whoisleuth-cli 1.39.1 → 1.45.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.
Files changed (90) hide show
  1. package/README.md +23 -4
  2. package/bin/whoisleuth.mjs +31 -9
  3. package/cli/archive-inspect.mjs +37 -2
  4. package/cli/arguments.mjs +361 -256
  5. package/cli/artifact-verify.mjs +35 -1
  6. package/cli/bulk-checkpoint.mjs +72 -39
  7. package/cli/bulk-command-runner.mjs +34 -4
  8. package/cli/bulk.mjs +18 -7
  9. package/cli/case-pack.mjs +136 -0
  10. package/cli/ci-report.mjs +73 -0
  11. package/cli/collection-preflight.mjs +59 -0
  12. package/cli/command-reference.mjs +395 -0
  13. package/cli/completion.mjs +67 -13
  14. package/cli/config-profile.mjs +183 -0
  15. package/cli/ct-event-intake.mjs +159 -0
  16. package/cli/discovery-observation-snapshot.mjs +8 -14
  17. package/cli/discovery-scan-command-runner.mjs +29 -2
  18. package/cli/discovery-snapshot.mjs +8 -15
  19. package/cli/domain-control-monitor.mjs +104 -0
  20. package/cli/domain-control-observations.mjs +235 -0
  21. package/cli/errors.mjs +7 -1
  22. package/cli/evidence-command-arguments.mjs +11 -0
  23. package/cli/evidence-command-runner.mjs +1 -0
  24. package/cli/external-observation-mapping.mjs +162 -0
  25. package/cli/fail-policy.mjs +64 -0
  26. package/cli/formatters/html.mjs +1 -1
  27. package/cli/formatters/json.mjs +12 -4
  28. package/cli/formatters/terminal.mjs +1 -1
  29. package/cli/http.mjs +2 -1
  30. package/cli/investigation-manifest.mjs +113 -0
  31. package/cli/investigation-run.mjs +112 -0
  32. package/cli/lookup-brief.mjs +116 -0
  33. package/cli/lookup-command-runner.mjs +8 -1
  34. package/cli/lookup-diff.mjs +17 -19
  35. package/cli/lookup-reconcile.mjs +1 -1
  36. package/cli/mail-review.mjs +5 -5
  37. package/cli/manual.mjs +1 -1
  38. package/cli/network-command-runner.mjs +4 -1
  39. package/cli/offline-evidence-review.mjs +105 -8
  40. package/cli/open-asset-model-bridge.mjs +169 -0
  41. package/cli/registry-cohort.mjs +121 -0
  42. package/cli/registry-doctor.mjs +106 -11
  43. package/cli/registry-fixture-scaffold.mjs +74 -0
  44. package/cli/runner.mjs +388 -315
  45. package/docs/cli-reference.md +266 -21
  46. package/docs/cli.md +43 -7
  47. package/frontend/src/lib/analysis/acquisition-due-diligence.js +2 -2
  48. package/frontend/src/lib/analysis/brand-profile-model.js +50 -6
  49. package/frontend/src/lib/analysis/case-evidence-model.js +1 -1
  50. package/frontend/src/lib/analysis/case-record-core.js +1 -1
  51. package/frontend/src/lib/analysis/case-record-operations.js +1 -1
  52. package/frontend/src/lib/analysis/case-report.js +573 -0
  53. package/frontend/src/lib/analysis/case-response-model.js +30 -1
  54. package/frontend/src/lib/analysis/detection-rule-model.js +36 -0
  55. package/frontend/src/lib/analysis/external-findings-import.js +320 -0
  56. package/frontend/src/lib/analysis/lookup-asset-graph.js +8 -8
  57. package/frontend/src/lib/analysis/lookup-decision-support.js +9 -2
  58. package/frontend/src/lib/analysis/lookup-presentation.js +14 -30
  59. package/frontend/src/lib/analysis/lookup-source-refresh.js +36 -63
  60. package/frontend/src/lib/analysis/scoring.js +1 -1
  61. package/frontend/src/lib/analysis/utils.js +1 -1
  62. package/frontend/src/lib/analysis/visualization-models.js +2 -3
  63. package/frontend/src/lib/analysis/workspace-archive-crypto.js +1 -4
  64. package/frontend/src/lib/analysis/workspace-archive.js +5 -7
  65. package/lib/abusech-auth.mjs +1 -1
  66. package/lib/availability.mjs +1 -1
  67. package/lib/bounded-contract-normalizers.mjs +33 -0
  68. package/lib/bounded-file.mjs +69 -0
  69. package/lib/bounded-json-response.mjs +157 -0
  70. package/lib/browser-library-profile.mjs +167 -38
  71. package/lib/dns-convergence-review.mjs +183 -0
  72. package/lib/dns-delegation-health.mjs +196 -3
  73. package/lib/domain-change-packet.mjs +133 -0
  74. package/lib/domain-change-review.mjs +307 -0
  75. package/lib/domain-control-flight-recorder.mjs +268 -0
  76. package/lib/domain-portfolio-review.mjs +151 -0
  77. package/lib/evidence-export.mjs +1 -1
  78. package/lib/html-signals.mjs +5 -11
  79. package/lib/lookup-cache.mjs +5 -1
  80. package/lib/nameserver-preflight-review.mjs +159 -0
  81. package/lib/page-language-signals.mjs +124 -0
  82. package/lib/rdap-transport.mjs +6 -6
  83. package/lib/risk-scoring.mjs +42 -1
  84. package/lib/safe-fetch.mjs +37 -32
  85. package/lib/static-html-analysis.mjs +29 -1
  86. package/lib/threat-intelligence-runtime.mjs +1 -2
  87. package/lib/trust-store-comparison.mjs +146 -0
  88. package/lib/website-technology.mjs +22 -12
  89. package/lib/zone-intent-review.mjs +534 -0
  90. package/package.json +1 -1
@@ -5,7 +5,7 @@ import { normalizeDomain } from "./case-model.js";
5
5
  import { normalizePageBaseline } from "./page-baseline.js";
6
6
  import { isInformativeFaviconHash } from "./utils.js";
7
7
  export const BRAND_PROFILE_SCHEMA = 'whoisleuth.brand-profiles';
8
- export const BRAND_PROFILE_SCHEMA_VERSION = 5;
8
+ export const BRAND_PROFILE_SCHEMA_VERSION = 6;
9
9
  export const MAX_PROFILES = 100;
10
10
  export const MAX_PROFILE_VALUES = 200;
11
11
  export const MAX_PROFILE_VALUE_INPUTS = MAX_PROFILE_VALUES * 4;
@@ -22,6 +22,8 @@ export const MAX_PROTECTION_ATTESTATIONS = 6;
22
22
  export const MAX_DESIRED_POSTURE_BASELINES = 20;
23
23
  export const MAX_DESIRED_POSTURE_RECORDS = 32;
24
24
  export const MAX_DESIRED_POSTURE_SUPPRESSIONS = 12;
25
+ export const MAX_DESIRED_POSTURE_OBSERVATIONS = 12;
26
+ export const MAX_DESIRED_POSTURE_CHANGE_WINDOWS = 8;
25
27
  const SAFE_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
26
28
  const SHA256_RE = /^[a-f0-9]{64}$/i;
27
29
  const CONTROL_RE = /[\x00-\x1f\x7f]/;
@@ -65,7 +67,7 @@ function boundedText(value, maximum = MAX_PROFILE_TEXT_LENGTH) {
65
67
  return '';
66
68
  return value.slice(0, maximum * 4).replace(/\s+/g, ' ').trim().slice(0, maximum).trim();
67
69
  }
68
- function safeId(value) {
70
+ export function normalizeBrandProfileId(value) {
69
71
  return typeof value === 'string' && SAFE_ID_RE.test(value) ? value : null;
70
72
  }
71
73
  function timestamp(value, fallback) {
@@ -203,6 +205,35 @@ function normalizeDesiredPostureObservation(value) {
203
205
  }
204
206
  return checks.length ? { observedAt, checks } : null;
205
207
  }
208
+ function normalizeDesiredPostureObservationHistory(value, previous) {
209
+ const candidates = Array.isArray(value) ? value : previous ? [previous] : [];
210
+ const byTime = new Map();
211
+ for (const item of candidates.slice(0, MAX_DESIRED_POSTURE_OBSERVATIONS * 4)) {
212
+ const normalized = normalizeDesiredPostureObservation(item);
213
+ if (normalized)
214
+ byTime.set(normalized.observedAt, normalized);
215
+ }
216
+ return [...byTime.values()]
217
+ .sort((left, right) => Date.parse(left.observedAt) - Date.parse(right.observedAt))
218
+ .slice(-MAX_DESIRED_POSTURE_OBSERVATIONS);
219
+ }
220
+ function normalizeDesiredPostureChangeWindows(value) {
221
+ if (!Array.isArray(value))
222
+ return [];
223
+ const output = [];
224
+ for (const item of value.slice(0, MAX_DESIRED_POSTURE_CHANGE_WINDOWS * 4)) {
225
+ const candidate = record(item);
226
+ const startsAt = timestamp(candidate.startsAt, null);
227
+ const endsAt = timestamp(candidate.endsAt, null);
228
+ const summary = boundedText(candidate.summary, 300);
229
+ if (!startsAt || !endsAt || Date.parse(endsAt) <= Date.parse(startsAt) || !summary)
230
+ continue;
231
+ output.push({ startsAt, endsAt, summary });
232
+ if (output.length >= MAX_DESIRED_POSTURE_CHANGE_WINDOWS)
233
+ break;
234
+ }
235
+ return output.sort((left, right) => Date.parse(left.startsAt) - Date.parse(right.startsAt));
236
+ }
206
237
  function normalizeTlsSanPatterns(value) {
207
238
  if (!Array.isArray(value))
208
239
  return [];
@@ -250,6 +281,8 @@ export function normalizeDesiredPostureBaselines(value, officialDomains, fallbac
250
281
  break;
251
282
  }
252
283
  seen.add(domain);
284
+ const previousObservation = normalizeDesiredPostureObservation(candidate.previousObservation);
285
+ const observationHistory = normalizeDesiredPostureObservationHistory(candidate.observationHistory, previousObservation);
253
286
  output.push({
254
287
  version: 1,
255
288
  domain,
@@ -266,9 +299,18 @@ export function normalizeDesiredPostureBaselines(value, officialDomains, fallbac
266
299
  ? candidate.registrarLock
267
300
  : 'unconfigured',
268
301
  renewalReviewAt: timestamp(candidate.renewalReviewAt, null),
302
+ zoneIntent: ['active_service', 'defensive_registration', 'no_service', 'parked', 'redirect_only'].includes(String(candidate.zoneIntent))
303
+ ? candidate.zoneIntent
304
+ : 'unconfigured',
305
+ lifecycle: ['active', 'change_planned', 'retiring', 'retired'].includes(String(candidate.lifecycle))
306
+ ? candidate.lifecycle
307
+ : 'active',
308
+ recoveryDependency: boundedText(candidate.recoveryDependency, 200),
309
+ approvedChangeWindows: normalizeDesiredPostureChangeWindows(candidate.approvedChangeWindows),
269
310
  suppressions,
270
311
  note: boundedText(candidate.note, MAX_PROFILE_TEXT_LENGTH),
271
- previousObservation: normalizeDesiredPostureObservation(candidate.previousObservation),
312
+ previousObservation: observationHistory.at(-1) ?? previousObservation,
313
+ observationHistory,
272
314
  updatedAt: timestamp(candidate.updatedAt, fallback),
273
315
  });
274
316
  if (output.length >= MAX_DESIRED_POSTURE_BASELINES)
@@ -295,7 +337,9 @@ export function normalizeBrandProfile(raw, options = {}) {
295
337
  const pageBaseline = candidateBaseline && officialDomains.includes(candidateBaseline.domain)
296
338
  ? candidateBaseline
297
339
  : null;
298
- const profileId = safeId(existing?.id) || safeId(value.id) || (typeof options.makeId === 'function' ? safeId(options.makeId()) : null);
340
+ const profileId = normalizeBrandProfileId(existing?.id)
341
+ || normalizeBrandProfileId(value.id)
342
+ || (typeof options.makeId === 'function' ? normalizeBrandProfileId(options.makeId()) : null);
299
343
  const name = boundedText(value.name, MAX_PROFILE_NAME_LENGTH);
300
344
  if (!profileId || !name)
301
345
  return null;
@@ -376,8 +420,8 @@ export function mergeBrandProfiles(localRaw, importedRaw, options = {}) {
376
420
  if (importedVersion !== null && importedVersion > BRAND_PROFILE_SCHEMA_VERSION) {
377
421
  throw new Error(`This Brand Profile file uses newer schema ${importedVersion}. Update the app before importing it.`);
378
422
  }
379
- if (![2, 3, 4, BRAND_PROFILE_SCHEMA_VERSION].includes(importedVersion ?? 0)) {
380
- throw new Error(`Expected a WHOISleuth Brand Profile export using schema 2, 3, 4, or ${BRAND_PROFILE_SCHEMA_VERSION}.`);
423
+ if (![2, 3, 4, 5, BRAND_PROFILE_SCHEMA_VERSION].includes(importedVersion ?? 0)) {
424
+ throw new Error(`Expected a WHOISleuth Brand Profile export using schema 2, 3, 4, 5, or ${BRAND_PROFILE_SCHEMA_VERSION}.`);
381
425
  }
382
426
  const local = normalizeBrandProfileStore(localRaw).profiles;
383
427
  const byName = new Map(local.map((profile) => [profile.name.toLowerCase(), profile]));
@@ -2,7 +2,7 @@
2
2
  // record normalization, and analyst updates.
3
3
  import { normalizeHttpSummary } from "./http-summary.js";
4
4
  import { normalizeOpportunityModelVersion, normalizeRiskModelVersion } from "./scoring.js";
5
- import { CASE_DISPOSITIONS, CASE_IMPORT_VERSIONS, CASE_SCHEMA_VERSION, CASE_SOURCES, CASE_STATUSES, DEFAULT_DISPOSITION, DEFAULT_SOURCE, DEFAULT_STATUS, EVIDENCE_SOURCES, MAX_CASES, MAX_CASE_IMPORT_BYTES, MAX_CASE_STORE_BYTES, MAX_DOMAIN_LENGTH, MAX_EVIDENCE_CHANGES, MAX_EVIDENCE_DETAIL_LENGTH, MAX_EVIDENCE_FACTORS, MAX_EVIDENCE_MUTATIONS, MAX_EVIDENCE_NAMESERVERS, MAX_EVIDENCE_SNAPSHOTS_PER_CASE, MAX_EVIDENCE_STRING_LENGTH, MAX_EVIDENCE_TITLE_LENGTH, MAX_NOTES_PER_CASE, MAX_NOTE_LENGTH, MAX_TAGS_PER_CASE, MAX_TAG_LENGTH, } from "./case-record-contracts.js";
5
+ import { MAX_EVIDENCE_CHANGES, MAX_EVIDENCE_DETAIL_LENGTH, MAX_EVIDENCE_FACTORS, MAX_EVIDENCE_MUTATIONS, MAX_EVIDENCE_NAMESERVERS, MAX_EVIDENCE_SNAPSHOTS_PER_CASE, MAX_EVIDENCE_STRING_LENGTH, MAX_EVIDENCE_TITLE_LENGTH, } from "./case-record-contracts.js";
6
6
  import { CONCLUSIVE_AVAILABILITY, DEFAULT_EVIDENCE_SOURCE, EVIDENCE_SOURCE_RANK, EVIDENCE_SOURCE_SET, REGISTERED_LIKE, hashString, isoOrNull, objectRecord, safeId, } from "./case-record-core.js";
7
7
  function evidenceString(value, max = MAX_EVIDENCE_STRING_LENGTH) {
8
8
  if (value == null)
@@ -1,6 +1,6 @@
1
1
  // Pure, framework-neutral analyst-case records, evidence histories, bounded
2
2
  // record normalization, and analyst updates.
3
- import { CASE_DISPOSITIONS, CASE_REVIEW_REASONS, CASE_IMPORT_VERSIONS, CASE_SCHEMA_VERSION, CASE_SOURCES, CASE_STATUSES, DEFAULT_DISPOSITION, DEFAULT_SOURCE, DEFAULT_STATUS, EVIDENCE_SOURCES, MAX_CASES, MAX_CASE_IMPORT_BYTES, MAX_CASE_STORE_BYTES, MAX_DOMAIN_LENGTH, MAX_EVIDENCE_CHANGES, MAX_EVIDENCE_DETAIL_LENGTH, MAX_EVIDENCE_FACTORS, MAX_EVIDENCE_MUTATIONS, MAX_EVIDENCE_NAMESERVERS, MAX_EVIDENCE_SNAPSHOTS_PER_CASE, MAX_EVIDENCE_STRING_LENGTH, MAX_EVIDENCE_TITLE_LENGTH, MAX_NOTES_PER_CASE, MAX_NOTE_LENGTH, MAX_TAGS_PER_CASE, MAX_TAG_LENGTH, } from "./case-record-contracts.js";
3
+ import { CASE_DISPOSITIONS, CASE_REVIEW_REASONS, CASE_SOURCES, CASE_STATUSES, DEFAULT_DISPOSITION, DEFAULT_SOURCE, DEFAULT_STATUS, EVIDENCE_SOURCES, MAX_DOMAIN_LENGTH, MAX_NOTES_PER_CASE, MAX_NOTE_LENGTH, MAX_TAGS_PER_CASE, MAX_TAG_LENGTH, } from "./case-record-contracts.js";
4
4
  // Forward-version policy (two distinct guarantees):
5
5
  // - A locally-stored envelope that declares a version greater than this is
6
6
  // still read best-effort on load (known fields kept, unknown dropped), but
@@ -1,7 +1,7 @@
1
1
  // Pure, framework-neutral analyst-case records, evidence histories, bounded
2
2
  // record normalization, and analyst updates.
3
3
  import { appendCaseAction, appendCaseAssertion, appendCaseDecision, appendCaseEvidencePin, appendCaseEvidencePins, appendCaseManualTrailEvent, appendCaseSighting, normalizeCaseActions, normalizeCaseAssertions, normalizeCaseDecisions, normalizeCaseEvidencePins, normalizeCaseManualTrail, normalizeCaseSightings, updateCaseAction, updateCaseAssertion, } from "./case-response-model.js";
4
- import { CASE_DISPOSITIONS, CASE_IMPORT_VERSIONS, CASE_SCHEMA_VERSION, CASE_SOURCES, CASE_STATUSES, DEFAULT_DISPOSITION, DEFAULT_SOURCE, DEFAULT_STATUS, EVIDENCE_SOURCES, MAX_CASES, MAX_CASE_IMPORT_BYTES, MAX_CASE_STORE_BYTES, MAX_DOMAIN_LENGTH, MAX_EVIDENCE_CHANGES, MAX_EVIDENCE_DETAIL_LENGTH, MAX_EVIDENCE_FACTORS, MAX_EVIDENCE_MUTATIONS, MAX_EVIDENCE_NAMESERVERS, MAX_EVIDENCE_SNAPSHOTS_PER_CASE, MAX_EVIDENCE_STRING_LENGTH, MAX_EVIDENCE_TITLE_LENGTH, MAX_NOTES_PER_CASE, MAX_NOTE_LENGTH, MAX_TAGS_PER_CASE, MAX_TAG_LENGTH, } from "./case-record-contracts.js";
4
+ import { MAX_CASES, MAX_NOTES_PER_CASE, } from "./case-record-contracts.js";
5
5
  import { DEFAULT_EVIDENCE_SOURCE, EVIDENCE_SOURCE_SET, deterministicId, isoOrNow, makeId, normalizeDisposition, normalizeDomain, normalizeNoteBody, normalizeReviewReasonCode, normalizeNotes, normalizeSource, normalizeStatus, normalizeTags, objectRecord, safeId, } from "./case-record-core.js";
6
6
  import { normalizeEvidenceHistory, } from "./case-evidence-model.js";
7
7
  // ---------------------------------------------------------------------------