@slicedearth/whoisleuth-cli 2.0.1 → 2.2.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/cli/arguments.mjs +21 -5
- package/cli/artifact-validation/case-response.mjs +771 -0
- package/cli/artifact-validation/investigation-capsule.mjs +426 -0
- package/cli/artifact-validation/lookup-evidence.mjs +718 -0
- package/cli/artifact-validation/signed-review.mjs +598 -0
- package/cli/artifact-validation/structure-primitives.mjs +105 -0
- package/cli/artifact-verify.mjs +1 -1
- package/cli/assurance-command-runner.mjs +324 -0
- package/cli/command-reference.mjs +1175 -843
- package/cli/completion.mjs +56 -9
- package/cli/discriminated-command-handlers.mjs +11 -0
- package/cli/doctor.mjs +2 -2
- package/cli/evidence-command-runner.mjs +41 -31
- package/cli/export-evidence.mjs +1 -1
- package/cli/fail-policy.mjs +33 -8
- package/cli/formatters/terminal-command-formats.mjs +437 -0
- package/cli/formatters/terminal-lookup.mjs +758 -0
- package/cli/formatters/terminal-metadata.mjs +58 -0
- package/cli/formatters/terminal-shared.mjs +83 -0
- package/cli/formatters/terminal.mjs +5 -1232
- package/cli/history-command-runner.mjs +132 -0
- package/cli/inline-command-families.mjs +45 -0
- package/cli/inline-command-runner.mjs +55 -0
- package/cli/investigation-plan.mjs +9 -2
- package/cli/investigation-run.mjs +114 -9
- package/cli/lookup-brief.mjs +141 -23
- package/cli/lookup-browser-view.mjs +289 -0
- package/cli/lookup-browser.mjs +3 -288
- package/cli/mail-review.mjs +2 -6
- package/cli/offline-artifact-validation.mjs +19 -0
- package/cli/review-command-runner.mjs +269 -0
- package/cli/runner.mjs +15 -972
- package/cli/saved-lookup.mjs +19 -2
- package/cli/support-command-runner.mjs +229 -0
- package/cli/workflow-command-runner.mjs +129 -0
- package/docs/cli-reference.md +16 -9
- package/docs/cli.md +16 -2
- package/lib/availability.mjs +12 -9
- package/lib/bounded-contract-normalizers.mjs +2 -9
- package/lib/browser-library-profile.mjs +1 -2
- package/lib/client-behavior-profile.mjs +1 -2
- package/lib/credential-surface-profile.mjs +1 -1
- package/lib/domain-posture-analysis.mjs +10 -1
- package/lib/domain-posture.mjs +51 -23
- package/lib/evidence-export.mjs +47 -5
- package/lib/evidence-report-markdown.mjs +4 -0
- package/lib/evidence-report.mjs +50 -0
- package/lib/generated/cisa-kev-catalog.mjs +34 -4
- package/lib/generated/registrar-standing-catalogue.mjs +367 -0
- package/lib/json-record.mjs +12 -0
- package/lib/lookup-child-profile-contract.mjs +858 -0
- package/lib/lookup-contract-primitives.mjs +98 -0
- package/lib/lookup-diagnostics.mjs +96 -0
- package/lib/lookup-network-evidence-bounds.mjs +1 -0
- package/lib/lookup-response.mjs +367 -0
- package/lib/lookup.mjs +28 -443
- package/lib/page-role-profile.mjs +1 -3
- package/lib/registrar-standing-catalogue-contract.mjs +70 -0
- package/lib/registrar-standing-contract.mjs +300 -0
- package/lib/registrar-standing.mjs +269 -0
- package/lib/registry-insights.mjs +2 -8
- package/lib/risk-scoring.mjs +26 -10
- package/lib/sslbl-certificates.generated.mjs +138 -136
- package/lib/structured-data-identity.mjs +1 -3
- package/lib/technology-evidence-role.mjs +51 -0
- package/lib/threat-intelligence-runtime.mjs +3 -3
- package/lib/threat-intelligence-types.mjs +13 -0
- package/lib/threatfox-intelligence.mjs +1 -3
- package/lib/tls-intelligence.mjs +1 -2
- package/lib/urlhaus-intelligence.mjs +1 -3
- package/lib/urlscan-intelligence.mjs +1 -3
- package/lib/website-security-posture.mjs +1 -2
- package/lib/website-technology.mjs +72 -23
- package/lib/whois-common-formats.mjs +276 -0
- package/lib/whois-dialects.mjs +534 -0
- package/lib/whois-parser-support.mjs +305 -0
- package/lib/whois-parser.mjs +135 -0
- package/lib/whois.mjs +6 -1187
- package/package.json +2 -2
- package/packages/cases/case-evidence-model.mjs +9 -0
- package/packages/cases/case-migration-model.mjs +17 -11
- package/packages/cases/case-record-operations.mjs +235 -0
- package/packages/cases/case-response-model.mjs +13 -9
- package/packages/cases/case-response-packet.mjs +297 -112
- package/packages/comparison/page-similarity.mjs +23 -0
- package/packages/contracts/case-portability.mjs +235 -61
- package/packages/contracts/cli-lookup.mjs +4 -3
- package/packages/contracts/domain-control-manifest.mjs +1 -1
- package/packages/contracts/investigation-portability.mjs +1 -1
- package/packages/contracts/risk-calibration.mjs +2 -2
- package/packages/contracts/workspace-portability.mjs +34 -6
- package/packages/interchange/external-findings-import.mjs +113 -45
- package/packages/investigation/evidence-coverage-ledger.mjs +21 -0
- package/packages/investigation/lookup-claim-readiness.mjs +2 -2
- package/packages/workspace/website-snapshot-model.mjs +79 -6
- package/third-party-notices.txt +1 -1
- package/cli/artifact-structure.mjs +0 -2507
|
@@ -36,6 +36,7 @@ export const CLI_LOOKUP_OPTIONAL_ROOT_KEYS = Object.freeze([
|
|
|
36
36
|
'whois',
|
|
37
37
|
'availability',
|
|
38
38
|
'registryInsights',
|
|
39
|
+
'registrarStanding',
|
|
39
40
|
'reverseDns',
|
|
40
41
|
'networkContext',
|
|
41
42
|
'securityTxt',
|
|
@@ -59,7 +60,7 @@ export const CLI_LOOKUP_COMPATIBILITY = defineSchemaCompatibility({
|
|
|
59
60
|
writeSemantics: 'normalized_rewrite',
|
|
60
61
|
byteBudget: MAX_CLI_LOOKUP_BYTES,
|
|
61
62
|
owner: 'packages/contracts/cli-lookup.mts',
|
|
62
|
-
note: 'Saved Lookup documents preserve version 1 domain, IP and ASN compatibility while version 2 can additionally retain bounded Deep homepage metadata.',
|
|
63
|
+
note: 'Saved Lookup documents preserve version 1 domain, IP and ASN compatibility while version 2 can additionally retain bounded Deep homepage metadata and optional official registrar-standing context.',
|
|
63
64
|
});
|
|
64
65
|
export const CLI_LOOKUP_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
|
|
65
66
|
id: 'cli-lookup',
|
|
@@ -234,7 +235,7 @@ export const CLI_LOOKUP_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
|
|
|
234
235
|
id: 'cli-lookup.sensitive-collection.v1',
|
|
235
236
|
classification: 'analyst_authored_sensitive',
|
|
236
237
|
projection: 'full_manifest',
|
|
237
|
-
includedCategories: ['target-identifiers', 'query-and-mode', 'raw-rdap-publications', 'raw-whois-responses', 'published-contacts', 'source-endpoints', 'source-timings', 'normalised-evidence', 'collection-context'],
|
|
238
|
+
includedCategories: ['target-identifiers', 'query-and-mode', 'raw-rdap-publications', 'raw-whois-responses', 'published-contacts', 'source-endpoints', 'source-timings', 'normalised-evidence', 'registrar-standing-official-projection', 'collection-context'],
|
|
238
239
|
excludedCategories: ['credentials', 'cookies', 'sessions', 'authorisation-material'],
|
|
239
240
|
notePolicy: 'not_applicable',
|
|
240
241
|
retention: 'operator_controlled_output',
|
|
@@ -245,7 +246,7 @@ export const CLI_LOOKUP_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
|
|
|
245
246
|
id: 'cli-lookup.sensitive-file.v1',
|
|
246
247
|
classification: 'analyst_authored_sensitive',
|
|
247
248
|
projection: 'full_manifest',
|
|
248
|
-
includedCategories: ['target-identifiers', 'query-and-mode', 'raw-rdap-publications', 'raw-whois-responses', 'published-contacts', 'source-endpoints', 'source-timings', 'normalised-evidence', 'collection-context'],
|
|
249
|
+
includedCategories: ['target-identifiers', 'query-and-mode', 'raw-rdap-publications', 'raw-whois-responses', 'published-contacts', 'source-endpoints', 'source-timings', 'normalised-evidence', 'registrar-standing-official-projection', 'collection-context'],
|
|
249
250
|
excludedCategories: ['credentials', 'cookies', 'sessions', 'authorisation-material'],
|
|
250
251
|
notePolicy: 'not_applicable',
|
|
251
252
|
retention: 'deliberate_local_file',
|
|
@@ -428,7 +428,7 @@ export const DOMAIN_CONTROL_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
|
|
|
428
428
|
{ id: 'domain-control.node.build-document', role: 'builder', runtime: 'node', module: 'lib/domain-control-manifest.mts', exportName: 'buildDomainControlManifest' },
|
|
429
429
|
{ id: 'domain-control.node.verify-integrity', role: 'integrity_verifier', runtime: 'node', module: 'lib/domain-control-manifest.mts', exportName: 'verifyDomainControlManifest' },
|
|
430
430
|
{ id: 'domain-control.node.review', role: 'reviewer', runtime: 'node', module: 'lib/domain-control-manifest.mts', exportName: 'reviewDomainControlManifest' },
|
|
431
|
-
{ id: 'domain-control.cli.offline-structure', role: 'structure_validator', runtime: 'cli', module: 'cli/artifact-
|
|
431
|
+
{ id: 'domain-control.cli.offline-structure', role: 'structure_validator', runtime: 'cli', module: 'cli/offline-artifact-validation.mts', exportName: 'validateSignedDigestArtifactStructure' },
|
|
432
432
|
{ id: 'domain-control.cli.offline-verify', role: 'integrity_verifier', runtime: 'cli', module: 'cli/artifact-verify.mts', exportName: 'verifyOfflineArtifact' },
|
|
433
433
|
{ id: 'domain-control.cli.interchange-report', role: 'interchange_reporter', runtime: 'cli', module: 'cli/interchange-report.mts', exportName: 'buildInterchangeFidelityReport' },
|
|
434
434
|
{ id: 'domain-control.cli.sign-package', role: 'signer', runtime: 'cli', module: 'cli/evidence-signing.mts', exportName: 'signEvidencePackage' },
|
|
@@ -244,7 +244,7 @@ export const INVESTIGATION_PORTABILITY_LIFECYCLE_FAMILY = defineSchemaLifecycleF
|
|
|
244
244
|
] },
|
|
245
245
|
],
|
|
246
246
|
hooks: [
|
|
247
|
-
{ id: 'investigation.offline.structure', role: 'structure_validator', runtime: 'cli', module: 'cli/artifact-
|
|
247
|
+
{ id: 'investigation.offline.structure', role: 'structure_validator', runtime: 'cli', module: 'cli/offline-artifact-validation.mts', exportName: 'validateOfflineArtifactStructure' },
|
|
248
248
|
{ id: 'investigation.offline.verify', role: 'integrity_verifier', runtime: 'cli', module: 'cli/artifact-verify.mts', exportName: 'verifyOfflineArtifact' },
|
|
249
249
|
{ id: 'investigation.acquisition.build', role: 'builder', runtime: 'shared', module: 'packages/investigation/acquisition-decision-packet.mts', exportName: 'buildAcquisitionDecisionPacket' },
|
|
250
250
|
{ id: 'investigation.passport.build', role: 'builder', runtime: 'shared', module: 'packages/investigation/lookup-claim-passport.mts', exportName: 'buildLookupClaimPassport' },
|
|
@@ -338,7 +338,7 @@ export const RISK_CALIBRATION_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
|
|
|
338
338
|
id: 'risk-calibration-report-v3-detailed',
|
|
339
339
|
path: 'test/fixtures/risk-calibration-report-v3-detailed.json',
|
|
340
340
|
bytes: 7_026,
|
|
341
|
-
sha256: '
|
|
341
|
+
sha256: '5c0c435e81478e90bc0b64aa7c20fd82d131b5261a57d9550df3f03334bc20e1',
|
|
342
342
|
contentDigestSha256: null,
|
|
343
343
|
schema: RISK_CALIBRATION_REPORT_SCHEMA,
|
|
344
344
|
version: RISK_CALIBRATION_REPORT_VERSION,
|
|
@@ -352,7 +352,7 @@ export const RISK_CALIBRATION_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
|
|
|
352
352
|
id: 'risk-calibration-report-v3-summary',
|
|
353
353
|
path: 'test/fixtures/risk-calibration-report-v3-summary.json',
|
|
354
354
|
bytes: 5_486,
|
|
355
|
-
sha256: '
|
|
355
|
+
sha256: 'b06c312eed14680ce30fd58a84a93908d29ea9e71bfee8174b90df286b730549',
|
|
356
356
|
contentDigestSha256: null,
|
|
357
357
|
schema: RISK_CALIBRATION_REPORT_SCHEMA,
|
|
358
358
|
version: RISK_CALIBRATION_REPORT_VERSION,
|
|
@@ -95,8 +95,8 @@ export const MAX_RELATIONSHIP_OBSERVATION_LIMITATIONS = 8;
|
|
|
95
95
|
export const MAX_RELATIONSHIP_OBSERVATION_VALUE_LENGTH = 20_000;
|
|
96
96
|
export const MAX_RELATIONSHIP_OBSERVATION_STORE_BYTES = 768 * 1024;
|
|
97
97
|
export const WEBSITE_SNAPSHOT_SCHEMA = 'whoisleuth.website-profile-snapshots';
|
|
98
|
-
export const WEBSITE_SNAPSHOT_SCHEMA_VERSION =
|
|
99
|
-
export const WEBSITE_SNAPSHOT_BROWSER_SUPPORTED_VERSIONS = Object.freeze([WEBSITE_SNAPSHOT_SCHEMA_VERSION]);
|
|
98
|
+
export const WEBSITE_SNAPSHOT_SCHEMA_VERSION = 5;
|
|
99
|
+
export const WEBSITE_SNAPSHOT_BROWSER_SUPPORTED_VERSIONS = Object.freeze([4, WEBSITE_SNAPSHOT_SCHEMA_VERSION]);
|
|
100
100
|
export const SUPPORTED_WEBSITE_SNAPSHOT_SCHEMA_VERSIONS = WEBSITE_SNAPSHOT_BROWSER_SUPPORTED_VERSIONS;
|
|
101
101
|
export const MAX_WEBSITE_SNAPSHOTS = 60;
|
|
102
102
|
export const MAX_WEBSITE_SNAPSHOTS_PER_DOMAIN = 12;
|
|
@@ -265,9 +265,9 @@ export const RELATIONSHIP_OBSERVATION_BROWSER_COMPATIBILITY = defineSchemaCompat
|
|
|
265
265
|
export const WEBSITE_SNAPSHOT_BROWSER_COMPATIBILITY = defineSchemaCompatibility({
|
|
266
266
|
id: 'browser.website-snapshots', kind: 'browser_store', schema: null,
|
|
267
267
|
currentVersion: WEBSITE_SNAPSHOT_SCHEMA_VERSION, supportedVersions: WEBSITE_SNAPSHOT_BROWSER_SUPPORTED_VERSIONS,
|
|
268
|
-
acceptsUnversionedLegacy: false, futureVersionBehavior: 'preserve_without_write', migration: '
|
|
268
|
+
acceptsUnversionedLegacy: false, futureVersionBehavior: 'preserve_without_write', migration: 'normalize_to_current',
|
|
269
269
|
writeSemantics: 'normalized_rewrite', byteBudget: MAX_WEBSITE_SNAPSHOT_STORE_BYTES, owner: WORKSPACE_CONTRACT_OWNER,
|
|
270
|
-
note: '
|
|
270
|
+
note: 'Public version 4 records normalise to version 5 with explicit unknown profile provenance; raw lookup payloads, contact data, certificate bytes, and fetched bodies remain excluded.',
|
|
271
271
|
});
|
|
272
272
|
export const BULK_SESSION_BROWSER_COMPATIBILITY = defineSchemaCompatibility({
|
|
273
273
|
id: 'browser.bulk-sessions', kind: 'browser_store', schema: null, currentVersion: BULK_SESSION_SCHEMA_VERSION,
|
|
@@ -335,9 +335,9 @@ export const RELATIONSHIP_OBSERVATION_EXPORT_COMPATIBILITY = defineSchemaCompati
|
|
|
335
335
|
export const WEBSITE_SNAPSHOT_EXPORT_COMPATIBILITY = defineSchemaCompatibility({
|
|
336
336
|
id: 'export.website-snapshots', kind: 'export', schema: WEBSITE_SNAPSHOT_SCHEMA,
|
|
337
337
|
currentVersion: WEBSITE_SNAPSHOT_SCHEMA_VERSION, supportedVersions: SUPPORTED_WEBSITE_SNAPSHOT_SCHEMA_VERSIONS,
|
|
338
|
-
acceptsUnversionedLegacy: false, futureVersionBehavior: 'reject', migration: '
|
|
338
|
+
acceptsUnversionedLegacy: false, futureVersionBehavior: 'reject', migration: 'normalize_to_current',
|
|
339
339
|
writeSemantics: 'non_destructive_merge', byteBudget: MAX_WEBSITE_SNAPSHOT_IMPORT_BYTES, owner: WORKSPACE_CONTRACT_OWNER,
|
|
340
|
-
note: '
|
|
340
|
+
note: 'Public schema 4 archive sections normalise to version 5 with explicit unknown profile provenance and merge non-destructively without retaining fetched bodies.',
|
|
341
341
|
});
|
|
342
342
|
export const BULK_SESSION_EXPORT_COMPATIBILITY = defineSchemaCompatibility({
|
|
343
343
|
id: 'export.bulk-sessions', kind: 'export', schema: BULK_SESSION_SCHEMA, currentVersion: BULK_SESSION_SCHEMA_VERSION,
|
|
@@ -800,6 +800,20 @@ const WORKSPACE_LIFECYCLE_FIXTURE_SOURCE = Object.freeze([
|
|
|
800
800
|
"contentDigestSha256": null,
|
|
801
801
|
"schema": "whoisleuth.browser.website-snapshot-store",
|
|
802
802
|
"version": 4,
|
|
803
|
+
"role": "historical",
|
|
804
|
+
"expectation": "normalises_to_current_output",
|
|
805
|
+
"expectedOutputFixtureId": "workspace.browser.website.v5",
|
|
806
|
+
"scope": "repository",
|
|
807
|
+
"shapeId": "workspace.browser.website.shape"
|
|
808
|
+
},
|
|
809
|
+
{
|
|
810
|
+
"id": "workspace.browser.website.v5",
|
|
811
|
+
"path": "test/fixtures/workspace-lifecycle/browser-website-v5.json",
|
|
812
|
+
"bytes": 90,
|
|
813
|
+
"sha256": "16550a8c24d5c7bcfe271086071fee7f3392279990cc6fe91e3b9c95f07d0759",
|
|
814
|
+
"contentDigestSha256": null,
|
|
815
|
+
"schema": "whoisleuth.browser.website-snapshot-store",
|
|
816
|
+
"version": 5,
|
|
803
817
|
"role": "current",
|
|
804
818
|
"expectation": "accepted_exact",
|
|
805
819
|
"expectedOutputFixtureId": null,
|
|
@@ -954,6 +968,20 @@ const WORKSPACE_LIFECYCLE_FIXTURE_SOURCE = Object.freeze([
|
|
|
954
968
|
"contentDigestSha256": null,
|
|
955
969
|
"schema": "whoisleuth.website-profile-snapshots",
|
|
956
970
|
"version": 4,
|
|
971
|
+
"role": "historical",
|
|
972
|
+
"expectation": "normalises_to_current_output",
|
|
973
|
+
"expectedOutputFixtureId": "workspace.portable.website.v5",
|
|
974
|
+
"scope": "repository",
|
|
975
|
+
"shapeId": "workspace.portable.website.shape"
|
|
976
|
+
},
|
|
977
|
+
{
|
|
978
|
+
"id": "workspace.portable.website.v5",
|
|
979
|
+
"path": "test/fixtures/workspace-lifecycle/portable-website-v5.json",
|
|
980
|
+
"bytes": 135,
|
|
981
|
+
"sha256": "0cab1675a632793141107fac330a130e7d300a7726e9963f8409199902bf0d23",
|
|
982
|
+
"contentDigestSha256": null,
|
|
983
|
+
"schema": "whoisleuth.website-profile-snapshots",
|
|
984
|
+
"version": 5,
|
|
957
985
|
"role": "current",
|
|
958
986
|
"expectation": "accepted_exact",
|
|
959
987
|
"expectedOutputFixtureId": null,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalizeDomain, openOrCreateCase, updateCase, } from "../cases/case-model.mjs";
|
|
2
|
+
import { canonicalRegistrableDomain } from "../../lib/registrable-domain.mjs";
|
|
2
3
|
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
3
4
|
import { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, MAX_EXTERNAL_FINDING_DOMAINS, MAX_EXTERNAL_FINDINGS, MAX_EXTERNAL_FINDINGS_IMPORT_BYTES, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, } from "../contracts/external-observation-interchange.mjs";
|
|
4
5
|
import { hasUnsafeRetainedText } from "./retained-text.mjs";
|
|
@@ -311,6 +312,58 @@ function structuredPinFields(finding) {
|
|
|
311
312
|
certificateObservation,
|
|
312
313
|
};
|
|
313
314
|
}
|
|
315
|
+
function mergeExternalFindingIntoCase(current, caseId, finding, source, now) {
|
|
316
|
+
const target = current.find((candidate) => candidate.id === caseId) ?? null;
|
|
317
|
+
if (!target)
|
|
318
|
+
throw new Error('The selected Case is unavailable for this finding.');
|
|
319
|
+
if (existingPinKey(target, finding, source.name)) {
|
|
320
|
+
return { cases: [...current], record: target, added: false };
|
|
321
|
+
}
|
|
322
|
+
const sourceLimitations = [
|
|
323
|
+
finding.evidenceClass === 'deployment_observation'
|
|
324
|
+
? `Imported as an observation made by ${source.name}; this browser session did not collect or independently verify it.`
|
|
325
|
+
: `Reported by ${source.name}; WHOISleuth did not collect or independently verify this provider finding.`,
|
|
326
|
+
...(source.reference ? [`Source reference: ${source.reference}`] : []),
|
|
327
|
+
...finding.limitations,
|
|
328
|
+
];
|
|
329
|
+
const existingPinIds = new Set(target.evidencePins.map((pin) => pin.id));
|
|
330
|
+
let updated = updateCase([...current], target.id, {
|
|
331
|
+
evidencePin: {
|
|
332
|
+
...structuredPinFields(finding),
|
|
333
|
+
label: `External ${finding.category} finding`,
|
|
334
|
+
value: finding.structuredObservation?.value ?? pinValue(finding),
|
|
335
|
+
source: importedSourceLabel(finding, source.name),
|
|
336
|
+
observedAt: finding.observedAt,
|
|
337
|
+
completeness: finding.completeness,
|
|
338
|
+
limitations: sourceLimitations,
|
|
339
|
+
},
|
|
340
|
+
}, now);
|
|
341
|
+
const addedPin = updated.record.evidencePins.find((pin) => !existingPinIds.has(pin.id)) ?? null;
|
|
342
|
+
if (!addedPin)
|
|
343
|
+
throw new Error('The imported finding could not be retained as Case evidence.');
|
|
344
|
+
updated = updateCase(updated.cases, target.id, {
|
|
345
|
+
sighting: {
|
|
346
|
+
state: finding.evidenceClass === 'deployment_observation'
|
|
347
|
+
? 'observed_by_deployment'
|
|
348
|
+
: 'reported_by_provider',
|
|
349
|
+
category: finding.category === 'dns'
|
|
350
|
+
? 'delegation'
|
|
351
|
+
: finding.category === 'certificate'
|
|
352
|
+
? 'certificate'
|
|
353
|
+
: finding.category === 'registration'
|
|
354
|
+
? 'registration'
|
|
355
|
+
: finding.category === 'http' || finding.category === 'page'
|
|
356
|
+
? 'website'
|
|
357
|
+
: 'other',
|
|
358
|
+
source: source.name,
|
|
359
|
+
observedAt: finding.observedAt,
|
|
360
|
+
completeness: finding.completeness,
|
|
361
|
+
evidencePinId: addedPin.id,
|
|
362
|
+
limitations: sourceLimitations,
|
|
363
|
+
},
|
|
364
|
+
}, now);
|
|
365
|
+
return { cases: updated.cases, record: updated.record, added: true };
|
|
366
|
+
}
|
|
314
367
|
export function mergeExternalFindingsIntoCases(current, document, now = new Date().toISOString()) {
|
|
315
368
|
let cases = [...current];
|
|
316
369
|
const createdDomains = new Set();
|
|
@@ -322,54 +375,12 @@ export function mergeExternalFindingsIntoCases(current, document, now = new Date
|
|
|
322
375
|
const opened = openOrCreateCase(cases, { domain: finding.domain, source: 'import' }, now);
|
|
323
376
|
cases = opened.cases;
|
|
324
377
|
const target = cases.find((candidate) => candidate.id === opened.record.id) ?? opened.record;
|
|
325
|
-
|
|
378
|
+
const merged = mergeExternalFindingIntoCase(cases, target.id, finding, document.source, now);
|
|
379
|
+
cases = merged.cases;
|
|
380
|
+
if (!merged.added) {
|
|
326
381
|
duplicatesSkipped += 1;
|
|
327
382
|
continue;
|
|
328
383
|
}
|
|
329
|
-
const sourceLimitations = [
|
|
330
|
-
finding.evidenceClass === 'deployment_observation'
|
|
331
|
-
? `Imported as an observation made by ${document.source.name}; this browser session did not collect or independently verify it.`
|
|
332
|
-
: `Reported by ${document.source.name}; WHOISleuth did not collect or independently verify this provider finding.`,
|
|
333
|
-
...(document.source.reference ? [`Source reference: ${document.source.reference}`] : []),
|
|
334
|
-
...finding.limitations,
|
|
335
|
-
];
|
|
336
|
-
const existingPinIds = new Set(target.evidencePins.map((pin) => pin.id));
|
|
337
|
-
const updated = updateCase(cases, target.id, {
|
|
338
|
-
evidencePin: {
|
|
339
|
-
...structuredPinFields(finding),
|
|
340
|
-
label: `External ${finding.category} finding`,
|
|
341
|
-
value: finding.structuredObservation?.value ?? pinValue(finding),
|
|
342
|
-
source: importedSourceLabel(finding, document.source.name),
|
|
343
|
-
observedAt: finding.observedAt,
|
|
344
|
-
completeness: finding.completeness,
|
|
345
|
-
limitations: sourceLimitations,
|
|
346
|
-
},
|
|
347
|
-
}, now);
|
|
348
|
-
cases = updated.cases;
|
|
349
|
-
const addedPin = updated.record.evidencePins.find((pin) => !existingPinIds.has(pin.id)) ?? null;
|
|
350
|
-
if (addedPin) {
|
|
351
|
-
cases = updateCase(cases, updated.record.id, {
|
|
352
|
-
sighting: {
|
|
353
|
-
state: finding.evidenceClass === 'deployment_observation'
|
|
354
|
-
? 'observed_by_deployment'
|
|
355
|
-
: 'reported_by_provider',
|
|
356
|
-
category: finding.category === 'dns'
|
|
357
|
-
? 'delegation'
|
|
358
|
-
: finding.category === 'certificate'
|
|
359
|
-
? 'certificate'
|
|
360
|
-
: finding.category === 'registration'
|
|
361
|
-
? 'registration'
|
|
362
|
-
: finding.category === 'http' || finding.category === 'page'
|
|
363
|
-
? 'website'
|
|
364
|
-
: 'other',
|
|
365
|
-
source: document.source.name,
|
|
366
|
-
observedAt: finding.observedAt,
|
|
367
|
-
completeness: finding.completeness,
|
|
368
|
-
evidencePinId: addedPin.id,
|
|
369
|
-
limitations: sourceLimitations,
|
|
370
|
-
},
|
|
371
|
-
}, now).cases;
|
|
372
|
-
}
|
|
373
384
|
findingsAdded += 1;
|
|
374
385
|
if (existing)
|
|
375
386
|
updatedDomains.add(finding.domain);
|
|
@@ -384,3 +395,60 @@ export function mergeExternalFindingsIntoCases(current, document, now = new Date
|
|
|
384
395
|
duplicatesSkipped,
|
|
385
396
|
};
|
|
386
397
|
}
|
|
398
|
+
function targetedFinding(finding, caseDomain) {
|
|
399
|
+
if (finding.domain === caseDomain)
|
|
400
|
+
return finding;
|
|
401
|
+
const prefix = `Captured hostname ${finding.domain}. `;
|
|
402
|
+
const maximumSummaryLength = 900;
|
|
403
|
+
const remaining = maximumSummaryLength - prefix.length;
|
|
404
|
+
const summary = remaining > 1 && finding.summary.length > remaining
|
|
405
|
+
? `${prefix}${finding.summary.slice(0, remaining - 1).trimEnd()}…`
|
|
406
|
+
: `${prefix}${finding.summary}`;
|
|
407
|
+
return { ...finding, domain: caseDomain, summary };
|
|
408
|
+
}
|
|
409
|
+
export function externalFindingsCaseTargets(document, caseDomain) {
|
|
410
|
+
if (canonicalRegistrableDomain(caseDomain) !== caseDomain) {
|
|
411
|
+
throw new Error('The selected Case does not have a canonical registrable-domain identity.');
|
|
412
|
+
}
|
|
413
|
+
const targets = new Set();
|
|
414
|
+
for (const finding of document.findings) {
|
|
415
|
+
if (canonicalRegistrableDomain(finding.domain) !== caseDomain) {
|
|
416
|
+
throw new Error(`The imported finding for ${finding.domain} does not belong to the selected Case for ${caseDomain}.`);
|
|
417
|
+
}
|
|
418
|
+
targets.add(finding.domain);
|
|
419
|
+
}
|
|
420
|
+
return Object.freeze([...targets]);
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Merges already-validated external findings into one selected Case. A Case is
|
|
424
|
+
* keyed to its registrable domain, while rendered captures may retain an exact
|
|
425
|
+
* hostname. Only same-domain findings are accepted, the exact hostname remains
|
|
426
|
+
* explicit in the retained evidence, and this operation can never open another
|
|
427
|
+
* Case as a side effect.
|
|
428
|
+
*/
|
|
429
|
+
export function mergeExternalFindingsIntoCase(current, caseId, document, now = new Date().toISOString()) {
|
|
430
|
+
const selected = current.find((candidate) => candidate.id === caseId) ?? null;
|
|
431
|
+
if (!selected)
|
|
432
|
+
throw new Error('Select an existing Case before importing rendered-capture evidence.');
|
|
433
|
+
externalFindingsCaseTargets(document, selected.domain);
|
|
434
|
+
let cases = [...current];
|
|
435
|
+
let record = selected;
|
|
436
|
+
let findingsAdded = 0;
|
|
437
|
+
let duplicatesSkipped = 0;
|
|
438
|
+
for (const original of document.findings) {
|
|
439
|
+
const finding = targetedFinding(original, selected.domain);
|
|
440
|
+
const merged = mergeExternalFindingIntoCase(cases, selected.id, finding, document.source, now);
|
|
441
|
+
cases = merged.cases;
|
|
442
|
+
record = merged.record;
|
|
443
|
+
if (merged.added)
|
|
444
|
+
findingsAdded += 1;
|
|
445
|
+
else
|
|
446
|
+
duplicatesSkipped += 1;
|
|
447
|
+
}
|
|
448
|
+
return {
|
|
449
|
+
cases,
|
|
450
|
+
record,
|
|
451
|
+
findingsAdded,
|
|
452
|
+
duplicatesSkipped,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
@@ -149,6 +149,9 @@ export function buildLookupEvidenceCoverageLedger(input) {
|
|
|
149
149
|
const rdapDiagnostic = record(diagnostics.rdap);
|
|
150
150
|
const whoisDiagnostic = record(diagnostics.whois);
|
|
151
151
|
const registrarRdap = record(input.registrarRdap);
|
|
152
|
+
const registrarStanding = record(input.registrarStanding);
|
|
153
|
+
const registrarStandingAccreditation = record(registrarStanding.accreditation);
|
|
154
|
+
const registrarStandingCompliance = record(registrarStanding.compliance);
|
|
152
155
|
const reverseDns = record(input.reverseDns);
|
|
153
156
|
const observedNetworkContext = record(input.observedNetworkContext);
|
|
154
157
|
const dnsEvidence = record(input.dnsEvidence);
|
|
@@ -199,6 +202,24 @@ export function buildLookupEvidenceCoverageLedger(input) {
|
|
|
199
202
|
limitations: stringList(registrarRdap.limitations),
|
|
200
203
|
});
|
|
201
204
|
}
|
|
205
|
+
if (registrarStanding.version === 1) {
|
|
206
|
+
const health = [registrarStandingAccreditation.sourceHealth, registrarStandingCompliance.sourceHealth];
|
|
207
|
+
items.push({
|
|
208
|
+
id: 'registrar-standing',
|
|
209
|
+
label: 'Registrar standing',
|
|
210
|
+
category: 'registry',
|
|
211
|
+
status: health.includes('unavailable')
|
|
212
|
+
? 'unavailable'
|
|
213
|
+
: health.includes('stale')
|
|
214
|
+
? 'partial'
|
|
215
|
+
: registrarStandingAccreditation.state === 'unknown'
|
|
216
|
+
? 'unknown'
|
|
217
|
+
: 'complete',
|
|
218
|
+
complete: health.every((state) => state === 'current'),
|
|
219
|
+
truncated: registrarStandingCompliance.truncated,
|
|
220
|
+
limitations: stringList(registrarStanding.limitations),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
202
223
|
if (reverseDns.source === 'reverse_dns' || record(diagnostics.reverseDns).status) {
|
|
203
224
|
items.push({
|
|
204
225
|
id: 'reverse-dns',
|
|
@@ -239,8 +239,8 @@ export function buildLookupClaimReadiness(input) {
|
|
|
239
239
|
requirement('http-observation', coverage),
|
|
240
240
|
requirement('page-identity-observation', coverage),
|
|
241
241
|
requirement('reviewed-case-recipient', coverage, {
|
|
242
|
-
state: input.
|
|
243
|
-
...(input.
|
|
242
|
+
state: input.hasReviewedCaseRecipient === true ? 'complete' : 'unknown',
|
|
243
|
+
...(input.hasReviewedCaseRecipient === true
|
|
244
244
|
? {}
|
|
245
245
|
: { limitation: 'A reviewed case and recipient route are not both available.' }),
|
|
246
246
|
}),
|
|
@@ -7,6 +7,9 @@ const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
|
|
|
7
7
|
const DIGEST_RE = /^[a-f0-9]{16,128}$/iu;
|
|
8
8
|
const SHA256_RE = /^[a-f0-9]{64}$/iu;
|
|
9
9
|
const SERIAL_RE = /^[a-f0-9]{1,128}$/iu;
|
|
10
|
+
const SNAPSHOT_TECHNOLOGY_ROLES = new Set([
|
|
11
|
+
'observed_edge', 'application_platform', 'framework_runtime', 'embedded_dependency',
|
|
12
|
+
]);
|
|
10
13
|
function record(value) {
|
|
11
14
|
return ordinaryWorkspaceRecord(value, 'Website-snapshot input');
|
|
12
15
|
}
|
|
@@ -47,7 +50,45 @@ function technology(value) {
|
|
|
47
50
|
const name = text(item?.name, 120);
|
|
48
51
|
if (!id || !name)
|
|
49
52
|
return null;
|
|
50
|
-
|
|
53
|
+
const roles = values(item?.roles, 4, (role) => {
|
|
54
|
+
const normalized = text(role, 40);
|
|
55
|
+
return SNAPSHOT_TECHNOLOGY_ROLES.has(normalized) ? normalized : null;
|
|
56
|
+
});
|
|
57
|
+
return {
|
|
58
|
+
id,
|
|
59
|
+
name,
|
|
60
|
+
category: text(item?.category, 80) || 'technology',
|
|
61
|
+
confidence: text(item?.confidence, 40) || 'unknown',
|
|
62
|
+
roles,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function profileVersion(value) {
|
|
66
|
+
return Number.isSafeInteger(value) && Number(value) > 0 && Number(value) <= 10_000
|
|
67
|
+
? Number(value)
|
|
68
|
+
: null;
|
|
69
|
+
}
|
|
70
|
+
function profileProvenance(value, legacy = false) {
|
|
71
|
+
if (legacy) {
|
|
72
|
+
return {
|
|
73
|
+
technology: { version: null, state: 'legacy_unknown' },
|
|
74
|
+
securityPosture: { version: null, state: 'legacy_unknown' },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const item = record(value);
|
|
78
|
+
const technologyItem = record(item?.technology);
|
|
79
|
+
const securityPostureItem = record(item?.securityPosture);
|
|
80
|
+
const technologyVersion = profileVersion(technologyItem?.version);
|
|
81
|
+
const securityPostureVersion = profileVersion(securityPostureItem?.version);
|
|
82
|
+
return {
|
|
83
|
+
technology: {
|
|
84
|
+
version: technologyVersion,
|
|
85
|
+
state: technologyVersion === null ? 'legacy_unknown' : 'known',
|
|
86
|
+
},
|
|
87
|
+
securityPosture: {
|
|
88
|
+
version: securityPostureVersion,
|
|
89
|
+
state: securityPostureVersion === null ? 'legacy_unknown' : 'known',
|
|
90
|
+
},
|
|
91
|
+
};
|
|
51
92
|
}
|
|
52
93
|
function posture(value) {
|
|
53
94
|
const item = record(value);
|
|
@@ -154,7 +195,7 @@ function identityValues(value) {
|
|
|
154
195
|
const formActionOrigins = values(item?.formActionOrigins, 20, origin);
|
|
155
196
|
return { resourceHosts, trackingIdentifiers, formActionOrigins };
|
|
156
197
|
}
|
|
157
|
-
export function normalizeWebsiteProfileSnapshot(raw) {
|
|
198
|
+
export function normalizeWebsiteProfileSnapshot(raw, sourceVersion) {
|
|
158
199
|
const value = record(raw);
|
|
159
200
|
const domain = normalizeDomain(value?.domain);
|
|
160
201
|
const observedAt = timestamp(value?.observedAt);
|
|
@@ -169,6 +210,7 @@ export function normalizeWebsiteProfileSnapshot(raw) {
|
|
|
169
210
|
savedAt,
|
|
170
211
|
complete: value?.complete === true,
|
|
171
212
|
truncated: value?.truncated === true,
|
|
213
|
+
profileProvenance: profileProvenance(value?.profileProvenance, sourceVersion === 4),
|
|
172
214
|
technologies: values(value?.technologies, 40, technology),
|
|
173
215
|
posture: values(value?.posture, 40, posture),
|
|
174
216
|
identity: identity(value?.identity),
|
|
@@ -192,7 +234,10 @@ export function normalizeWebsiteSnapshotStore(raw) {
|
|
|
192
234
|
throw new Error('This website-snapshot collection uses an unsupported schema and cannot be read safely.');
|
|
193
235
|
}
|
|
194
236
|
const sourceValues = Array.isArray(raw) ? raw : Array.isArray(value?.snapshots) ? value.snapshots : [];
|
|
195
|
-
const
|
|
237
|
+
const sourceVersion = value?.schema === WEBSITE_SNAPSHOT_SCHEMA && Number.isSafeInteger(value.version)
|
|
238
|
+
? Number(value.version)
|
|
239
|
+
: undefined;
|
|
240
|
+
const snapshots = values(sourceValues, MAX_WEBSITE_SNAPSHOTS * 2, (candidate) => normalizeWebsiteProfileSnapshot(candidate, sourceVersion));
|
|
196
241
|
const perDomain = new Map();
|
|
197
242
|
const retained = [];
|
|
198
243
|
for (const snapshot of snapshots.sort((left, right) => right.savedAt.localeCompare(left.savedAt))) {
|
|
@@ -272,9 +317,29 @@ export function compareWebsiteSnapshots(beforeRaw, afterRaw) {
|
|
|
272
317
|
dependencyTransitions: [],
|
|
273
318
|
};
|
|
274
319
|
}
|
|
320
|
+
const technologyVersionBefore = before.profileProvenance.technology.version;
|
|
321
|
+
const technologyVersionAfter = after.profileProvenance.technology.version;
|
|
322
|
+
const postureVersionBefore = before.profileProvenance.securityPosture.version;
|
|
323
|
+
const postureVersionAfter = after.profileProvenance.securityPosture.version;
|
|
324
|
+
const technologyComparability = technologyVersionBefore === null || technologyVersionAfter === null
|
|
325
|
+
? 'legacy_unknown'
|
|
326
|
+
: technologyVersionBefore === technologyVersionAfter ? 'comparable' : 'detector_changed';
|
|
327
|
+
const postureComparability = postureVersionBefore === null || postureVersionAfter === null
|
|
328
|
+
? 'legacy_unknown'
|
|
329
|
+
: postureVersionBefore === postureVersionAfter ? 'comparable' : 'detector_changed';
|
|
275
330
|
const changes = [
|
|
276
|
-
...compareMap('technology', new Map(before.technologies.map((item) => [item.id, `${item.name}|${item.category}|${item.confidence}`])), new Map(after.technologies.map((item) => [item.id, `${item.name}|${item.category}|${item.confidence}`])))
|
|
277
|
-
|
|
331
|
+
...(technologyComparability === 'comparable' ? compareMap('technology', new Map(before.technologies.map((item) => [item.id, `${item.name}|${item.category}|${item.confidence}|${item.roles.join(',')}`])), new Map(after.technologies.map((item) => [item.id, `${item.name}|${item.category}|${item.confidence}|${item.roles.join(',')}`]))) : [{
|
|
332
|
+
field: 'technology.profileVersion',
|
|
333
|
+
state: technologyComparability === 'detector_changed' ? 'changed' : 'incomparable',
|
|
334
|
+
before: technologyVersionBefore === null ? null : String(technologyVersionBefore),
|
|
335
|
+
after: technologyVersionAfter === null ? null : String(technologyVersionAfter),
|
|
336
|
+
}]),
|
|
337
|
+
...(postureComparability === 'comparable' ? compareMap('posture', new Map(before.posture.map((item) => [item.id, item.state])), new Map(after.posture.map((item) => [item.id, item.state]))) : [{
|
|
338
|
+
field: 'posture.profileVersion',
|
|
339
|
+
state: postureComparability === 'detector_changed' ? 'changed' : 'incomparable',
|
|
340
|
+
before: postureVersionBefore === null ? null : String(postureVersionBefore),
|
|
341
|
+
after: postureVersionAfter === null ? null : String(postureVersionAfter),
|
|
342
|
+
}]),
|
|
278
343
|
...compareMap('source', new Map(before.sources.map((item) => [item.source, item.state])), new Map(after.sources.map((item) => [item.source, item.state]))),
|
|
279
344
|
...compareMap('dependency', new Map(before.dependencies.map((item) => [`${item.recordType}:${item.target}`, `${item.state}|${item.qualification}`])), new Map(after.dependencies.map((item) => [`${item.recordType}:${item.target}`, `${item.state}|${item.qualification}`]))),
|
|
280
345
|
];
|
|
@@ -358,5 +423,13 @@ export function compareWebsiteSnapshots(beforeRaw, afterRaw) {
|
|
|
358
423
|
if (before.complete !== after.complete || before.truncated !== after.truncated) {
|
|
359
424
|
changes.push({ field: 'completeness', state: 'incomparable', before: `${before.complete}/${before.truncated}`, after: `${after.complete}/${after.truncated}` });
|
|
360
425
|
}
|
|
361
|
-
return {
|
|
426
|
+
return {
|
|
427
|
+
compatible: true,
|
|
428
|
+
profileComparability: {
|
|
429
|
+
technology: technologyComparability,
|
|
430
|
+
securityPosture: postureComparability,
|
|
431
|
+
},
|
|
432
|
+
changes,
|
|
433
|
+
dependencyTransitions,
|
|
434
|
+
};
|
|
362
435
|
}
|
package/third-party-notices.txt
CHANGED
|
@@ -713,7 +713,7 @@ MIT License
|
|
|
713
713
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
714
714
|
SOFTWARE
|
|
715
715
|
================================================================================
|
|
716
|
-
undici@8.10.
|
|
716
|
+
undici@8.10.1
|
|
717
717
|
Relationship: direct production dependency
|
|
718
718
|
Declared licence: MIT
|
|
719
719
|
Licence source: LICENSE
|