@slicedearth/whoisleuth-cli 2.6.0 → 2.7.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/DISCLOSURE +8 -5
- package/NOTICE +3 -2
- package/README.md +60 -67
- package/cli/artifact-validation/case-response.mjs +6 -857
- package/cli/artifact-validation/lookup-evidence.mjs +15 -2
- package/cli/artifact-verify.mjs +32 -1
- package/cli/case-command.mjs +17 -17
- package/cli/case-pack.mjs +7 -1
- package/cli/ci-report.mjs +17 -5
- package/cli/command-definition.mjs +5 -5
- package/cli/doctor.mjs +37 -3
- package/cli/formatters/html.mjs +3 -0
- package/cli/formatters/json.mjs +8 -1
- package/cli/formatters/terminal-lookup.mjs +24 -1
- package/cli/history-command-definitions.mjs +1 -1
- package/cli/investigation-plan.mjs +15 -0
- package/cli/lookup-brief.mjs +11 -5
- package/cli/lookup-command-runner.mjs +20 -13
- package/cli/review-command-definitions.mjs +11 -11
- package/cli/saved-lookup.mjs +22 -7
- package/cli/source-reliability.mjs +12 -10
- package/cli/support-arguments.mjs +1 -1
- package/cli/support-command-definitions.mjs +5 -4
- package/cli/support-command-runner.mjs +6 -4
- package/cli/workflow-command-definitions.mjs +2 -1
- package/docs/cli-reference.md +49 -11
- package/docs/cli.md +109 -32
- package/docs/contextual-reviews.md +10 -0
- package/docs/infrastructure-observations.md +9 -0
- package/lib/dmarc-discovery.mjs +4 -2
- package/lib/dns-inheritance-review.mjs +14 -1
- package/lib/evidence-export.mjs +10 -4
- package/lib/evidence-report.mjs +2 -1
- package/lib/generated/cisa-kev-catalog.mjs +17 -4
- package/lib/generated/registrar-standing-catalogue.mjs +7 -7
- package/lib/generated/retire-browser-catalog.mjs +178 -5
- package/lib/idn-confusables.mjs +5 -1
- package/lib/lookup-collection-receipt.mjs +52 -0
- package/lib/lookup-diagnostics.mjs +2 -12
- package/lib/lookup-progress.mjs +29 -31
- package/lib/lookup-response-contract.mjs +9 -0
- package/lib/lookup-response.mjs +15 -14
- package/lib/lookup-source-progress.mjs +4 -2
- package/lib/lookup.mjs +33 -2
- package/lib/rdap-capabilities.mjs +43 -9
- package/lib/registry-insights.mjs +17 -6
- package/lib/sslbl-certificates.generated.mjs +143 -142
- package/lib/whois-authority.mjs +56 -1
- package/lib/whois-chain.mjs +2 -1
- package/lib/whois-contracts.mjs +2 -0
- package/lib/whois-normalization.mjs +4 -4
- package/package.json +6 -6
- package/packages/analysis/bounded-json.mjs +12 -2
- package/packages/cases/case-attachment-model.mjs +36 -1
- package/packages/cases/case-checkpoint-receipts.mjs +68 -0
- package/packages/cases/case-evidence-links.mjs +28 -0
- package/packages/cases/case-evidence-model.mjs +39 -6
- package/packages/cases/case-migration-model.mjs +9 -1
- package/packages/cases/case-object-coverage.mjs +106 -20
- package/packages/cases/case-record-operations.mjs +13 -0
- package/packages/cases/case-record-projection.mjs +28 -2
- package/packages/cases/case-report.mjs +5 -4
- package/packages/cases/case-response-packet-validation.mjs +880 -0
- package/packages/cases/case-response-packet-vocabulary.mjs +12 -1
- package/packages/cases/case-response-packet.mjs +2 -15
- package/packages/cases/case-response-review-inputs.mjs +18 -1
- package/packages/cases/case-type-presentation.mjs +10 -0
- package/packages/cases/case-workflow-metadata.mjs +0 -8
- package/packages/comparison/capture-context.mjs +3 -2
- package/packages/contracts/analyst-review-state-contract.mjs +1 -1
- package/packages/contracts/case-portability.mjs +59 -28
- package/packages/contracts/cli-doctor.mjs +4 -0
- package/packages/contracts/cli-lookup.mjs +57 -46
- package/packages/contracts/lookup-evidence.mjs +12 -2
- package/packages/contracts/registry-insights.mjs +6 -0
- package/packages/evidence/dmarc-policy-context.mjs +80 -0
- package/packages/evidence/lookup-collection-receipt.mjs +140 -0
- package/packages/evidence/lookup-collection-review.mjs +74 -0
- package/packages/interchange/external-findings-import.mjs +8 -4
- package/packages/investigation/bulk-mail-exposure.mjs +2 -2
- package/packages/investigation/domain-history-review.mjs +7 -1
- package/packages/workspace/brand-keyword-campaign.mjs +11 -1
- package/packages/workspace/bulk-session-model.mjs +8 -0
- package/third-party-notices.txt +29 -29
|
@@ -2,9 +2,12 @@ import { defineSchemaCompatibility } from "./schema-compatibility.mjs";
|
|
|
2
2
|
import { defineSchemaLifecycleFamily } from "./schema-lifecycle.mjs";
|
|
3
3
|
export const CLI_LOOKUP_SCHEMA = 'whoisleuth.cli.lookup';
|
|
4
4
|
export const PUBLIC_CLI_LOOKUP_VERSION = 1;
|
|
5
|
-
export const
|
|
5
|
+
export const HOMEPAGE_CLI_LOOKUP_VERSION = 2;
|
|
6
|
+
export const COLLECTION_RECEIPT_CLI_LOOKUP_VERSION = 3;
|
|
7
|
+
export const CLI_LOOKUP_VERSION = COLLECTION_RECEIPT_CLI_LOOKUP_VERSION;
|
|
6
8
|
export const SUPPORTED_CLI_LOOKUP_VERSIONS = Object.freeze([
|
|
7
9
|
PUBLIC_CLI_LOOKUP_VERSION,
|
|
10
|
+
HOMEPAGE_CLI_LOOKUP_VERSION,
|
|
8
11
|
CLI_LOOKUP_VERSION,
|
|
9
12
|
]);
|
|
10
13
|
export const MAX_CLI_LOOKUP_BYTES = 8 * 1024 * 1024;
|
|
@@ -43,6 +46,7 @@ export const CLI_LOOKUP_OPTIONAL_ROOT_KEYS = Object.freeze([
|
|
|
43
46
|
'sslbl',
|
|
44
47
|
'threatIntelligence',
|
|
45
48
|
'collectionContext',
|
|
49
|
+
'collectionReceipt',
|
|
46
50
|
]);
|
|
47
51
|
export const CLI_LOOKUP_DIAGNOSTIC_REQUIRED_KEYS = Object.freeze([
|
|
48
52
|
'rdap',
|
|
@@ -60,7 +64,7 @@ export const CLI_LOOKUP_COMPATIBILITY = defineSchemaCompatibility({
|
|
|
60
64
|
writeSemantics: 'normalized_rewrite',
|
|
61
65
|
byteBudget: MAX_CLI_LOOKUP_BYTES,
|
|
62
66
|
owner: 'packages/contracts/cli-lookup.mts',
|
|
63
|
-
note: 'Saved Lookup documents preserve
|
|
67
|
+
note: 'Saved Lookup documents preserve versions 1 and 2; version 3 may retain a bounded collection receipt for the completed operation. Homepage metadata and registrar-standing context remain supported from version 2.',
|
|
64
68
|
});
|
|
65
69
|
export const CLI_LOOKUP_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
|
|
66
70
|
id: 'cli-lookup',
|
|
@@ -87,11 +91,11 @@ export const CLI_LOOKUP_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
|
|
|
87
91
|
{
|
|
88
92
|
compatibilityId: CLI_LOOKUP_COMPATIBILITY.id,
|
|
89
93
|
schema: CLI_LOOKUP_SCHEMA,
|
|
90
|
-
version:
|
|
94
|
+
version: HOMEPAGE_CLI_LOOKUP_VERSION,
|
|
91
95
|
role: 'document',
|
|
92
|
-
lifecycle: '
|
|
96
|
+
lifecycle: 'legacy',
|
|
93
97
|
readable: true,
|
|
94
|
-
emitted:
|
|
98
|
+
emitted: false,
|
|
95
99
|
exactKeys: false,
|
|
96
100
|
extensionPolicy: 'preserve_bounded',
|
|
97
101
|
futureVersionBehaviour: 'reject',
|
|
@@ -100,8 +104,29 @@ export const CLI_LOOKUP_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
|
|
|
100
104
|
byteBudget: MAX_CLI_LOOKUP_BYTES,
|
|
101
105
|
fixtureIds: ['cli-lookup-v2', 'cli-lookup-asn-v2'],
|
|
102
106
|
},
|
|
107
|
+
{
|
|
108
|
+
compatibilityId: CLI_LOOKUP_COMPATIBILITY.id,
|
|
109
|
+
schema: CLI_LOOKUP_SCHEMA,
|
|
110
|
+
version: CLI_LOOKUP_VERSION,
|
|
111
|
+
role: 'document', lifecycle: 'current', readable: true, emitted: true,
|
|
112
|
+
exactKeys: false, extensionPolicy: 'preserve_bounded', futureVersionBehaviour: 'reject',
|
|
113
|
+
migrationTarget: null, canonicalisation: null, byteBudget: MAX_CLI_LOOKUP_BYTES,
|
|
114
|
+
fixtureIds: ['cli-lookup-v3', 'cli-lookup-asn-v3'],
|
|
115
|
+
},
|
|
103
116
|
],
|
|
104
117
|
fixtures: [
|
|
118
|
+
{
|
|
119
|
+
id: 'cli-lookup-v3', path: 'test/fixtures/cli-lookup-v3.json',
|
|
120
|
+
bytes: 7151, sha256: 'ee06201deb99ef1cdc5e79dc026a733fb14494593ee7ffdff3035b1fbc19d61e', contentDigestSha256: null,
|
|
121
|
+
schema: CLI_LOOKUP_SCHEMA, version: CLI_LOOKUP_VERSION, role: 'current',
|
|
122
|
+
expectation: 'accepted_exact', expectedOutputFixtureId: null, scope: 'repository',
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
id: 'cli-lookup-asn-v3', path: 'test/fixtures/cli-lookup-asn-v3.json',
|
|
126
|
+
bytes: 3506, sha256: 'c0a3dfe9d9c13d3f03079c6c76b33b353a6029b9a9893cbdb743b3f4d864c74e', contentDigestSha256: null,
|
|
127
|
+
schema: CLI_LOOKUP_SCHEMA, version: CLI_LOOKUP_VERSION, role: 'current',
|
|
128
|
+
expectation: 'accepted_exact', expectedOutputFixtureId: null, scope: 'repository',
|
|
129
|
+
},
|
|
105
130
|
{
|
|
106
131
|
id: 'cli-lookup-v1',
|
|
107
132
|
path: 'test/fixtures/cli-lookup-v1.json',
|
|
@@ -122,8 +147,8 @@ export const CLI_LOOKUP_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
|
|
|
122
147
|
sha256: 'd98356ede18ddd12fde8cf10b0cfa43c42f6c9a09f1951e0605e1ec912892778',
|
|
123
148
|
contentDigestSha256: null,
|
|
124
149
|
schema: CLI_LOOKUP_SCHEMA,
|
|
125
|
-
version:
|
|
126
|
-
role: '
|
|
150
|
+
version: HOMEPAGE_CLI_LOOKUP_VERSION,
|
|
151
|
+
role: 'historical',
|
|
127
152
|
expectation: 'accepted_exact',
|
|
128
153
|
expectedOutputFixtureId: null,
|
|
129
154
|
scope: 'repository',
|
|
@@ -148,8 +173,8 @@ export const CLI_LOOKUP_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
|
|
|
148
173
|
sha256: 'a3ee74df02050d6efd935784734c8c4cfd5fcb9833393de68935f5040faf2856',
|
|
149
174
|
contentDigestSha256: null,
|
|
150
175
|
schema: CLI_LOOKUP_SCHEMA,
|
|
151
|
-
version:
|
|
152
|
-
role: '
|
|
176
|
+
version: HOMEPAGE_CLI_LOOKUP_VERSION,
|
|
177
|
+
role: 'historical',
|
|
153
178
|
expectation: 'accepted_exact',
|
|
154
179
|
expectedOutputFixtureId: null,
|
|
155
180
|
scope: 'repository',
|
|
@@ -158,26 +183,12 @@ export const CLI_LOOKUP_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
|
|
|
158
183
|
metadata: {
|
|
159
184
|
enforcement: 'declarative_only',
|
|
160
185
|
shapes: [
|
|
161
|
-
{
|
|
162
|
-
id:
|
|
163
|
-
schema: CLI_LOOKUP_SCHEMA,
|
|
164
|
-
versions: [PUBLIC_CLI_LOOKUP_VERSION],
|
|
165
|
-
objects: [
|
|
166
|
-
{ path: '$', requiredKeys: CLI_LOOKUP_REQUIRED_ROOT_KEYS, optionalKeys: CLI_LOOKUP_OPTIONAL_ROOT_KEYS, unknownKeys: 'preserve_bounded' },
|
|
167
|
-
{ path: '$.diagnostics', requiredKeys: CLI_LOOKUP_DIAGNOSTIC_REQUIRED_KEYS, optionalKeys: [], unknownKeys: 'preserve_bounded' },
|
|
168
|
-
{ path: '$.diagnostics.rdap', requiredKeys: ['status'], optionalKeys: [], unknownKeys: 'preserve_bounded' },
|
|
169
|
-
{ path: '$.diagnostics.whois', requiredKeys: ['status'], optionalKeys: [], unknownKeys: 'preserve_bounded' },
|
|
170
|
-
],
|
|
171
|
-
fixedArrays: [],
|
|
172
|
-
normalisation: 'preserve_bounded_document',
|
|
173
|
-
target: null,
|
|
174
|
-
},
|
|
175
|
-
{
|
|
176
|
-
id: 'cli-lookup.document.v2',
|
|
186
|
+
...SUPPORTED_CLI_LOOKUP_VERSIONS.map(version => ({
|
|
187
|
+
id: `cli-lookup.document.v${version}`,
|
|
177
188
|
schema: CLI_LOOKUP_SCHEMA,
|
|
178
|
-
versions: [
|
|
189
|
+
versions: [version],
|
|
179
190
|
objects: [
|
|
180
|
-
{ path: '$', requiredKeys: CLI_LOOKUP_REQUIRED_ROOT_KEYS, optionalKeys: CLI_LOOKUP_OPTIONAL_ROOT_KEYS, unknownKeys: 'preserve_bounded' },
|
|
191
|
+
{ path: '$', requiredKeys: CLI_LOOKUP_REQUIRED_ROOT_KEYS, optionalKeys: CLI_LOOKUP_OPTIONAL_ROOT_KEYS.filter(key => version >= COLLECTION_RECEIPT_CLI_LOOKUP_VERSION || key !== 'collectionReceipt'), unknownKeys: 'preserve_bounded' },
|
|
181
192
|
{ path: '$.diagnostics', requiredKeys: CLI_LOOKUP_DIAGNOSTIC_REQUIRED_KEYS, optionalKeys: [], unknownKeys: 'preserve_bounded' },
|
|
182
193
|
{ path: '$.diagnostics.rdap', requiredKeys: ['status'], optionalKeys: [], unknownKeys: 'preserve_bounded' },
|
|
183
194
|
{ path: '$.diagnostics.whois', requiredKeys: ['status'], optionalKeys: [], unknownKeys: 'preserve_bounded' },
|
|
@@ -185,7 +196,7 @@ export const CLI_LOOKUP_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
|
|
|
185
196
|
fixedArrays: [],
|
|
186
197
|
normalisation: 'preserve_bounded_document',
|
|
187
198
|
target: null,
|
|
188
|
-
},
|
|
199
|
+
})),
|
|
189
200
|
],
|
|
190
201
|
boundProfiles: [
|
|
191
202
|
{
|
|
@@ -214,9 +225,9 @@ export const CLI_LOOKUP_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
|
|
|
214
225
|
],
|
|
215
226
|
serialisationProfiles: [
|
|
216
227
|
{
|
|
217
|
-
id: 'cli-lookup.json.v1-
|
|
228
|
+
id: 'cli-lookup.json.v1-v3',
|
|
218
229
|
schema: CLI_LOOKUP_SCHEMA,
|
|
219
|
-
versions:
|
|
230
|
+
versions: SUPPORTED_CLI_LOOKUP_VERSIONS,
|
|
220
231
|
mediaType: 'application/json',
|
|
221
232
|
encoding: 'utf-8',
|
|
222
233
|
bom: false,
|
|
@@ -234,7 +245,7 @@ export const CLI_LOOKUP_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
|
|
|
234
245
|
id: 'cli-lookup.sensitive-collection.v1',
|
|
235
246
|
classification: 'analyst_authored_sensitive',
|
|
236
247
|
projection: 'full_manifest',
|
|
237
|
-
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'],
|
|
248
|
+
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', 'collection-receipt'],
|
|
238
249
|
excludedCategories: ['credentials', 'cookies', 'sessions', 'authorisation-material'],
|
|
239
250
|
notePolicy: 'not_applicable',
|
|
240
251
|
retention: 'operator_controlled_output',
|
|
@@ -245,7 +256,7 @@ export const CLI_LOOKUP_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
|
|
|
245
256
|
id: 'cli-lookup.sensitive-file.v1',
|
|
246
257
|
classification: 'analyst_authored_sensitive',
|
|
247
258
|
projection: 'full_manifest',
|
|
248
|
-
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'],
|
|
259
|
+
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', 'collection-receipt'],
|
|
249
260
|
excludedCategories: ['credentials', 'cookies', 'sessions', 'authorisation-material'],
|
|
250
261
|
notePolicy: 'not_applicable',
|
|
251
262
|
retention: 'deliberate_local_file',
|
|
@@ -257,7 +268,7 @@ export const CLI_LOOKUP_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
|
|
|
257
268
|
classification: 'metadata_only',
|
|
258
269
|
projection: 'metadata_only',
|
|
259
270
|
includedCategories: ['schema-and-version', 'source-state-counts', 'bounded-timings', 'integrity-state'],
|
|
260
|
-
excludedCategories: ['target-identifiers', 'query-and-mode', 'raw-rdap-publications', 'raw-whois-responses', 'published-contacts', 'source-endpoints', 'normalised-evidence', 'collection-context'],
|
|
271
|
+
excludedCategories: ['target-identifiers', 'query-and-mode', 'raw-rdap-publications', 'raw-whois-responses', 'published-contacts', 'source-endpoints', 'normalised-evidence', 'collection-context', 'collection-receipt'],
|
|
261
272
|
notePolicy: 'not_applicable',
|
|
262
273
|
retention: 'transient_report',
|
|
263
274
|
network: 'none',
|
|
@@ -268,7 +279,7 @@ export const CLI_LOOKUP_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
|
|
|
268
279
|
classification: 'metadata_only',
|
|
269
280
|
projection: 'metadata_only',
|
|
270
281
|
includedCategories: ['schema-and-version', 'source-state-counts', 'bounded-timings', 'truncation-and-rate-limit-counts', 'aggregate-lookup-mode-counts'],
|
|
271
|
-
excludedCategories: ['target-identifiers', 'query-values', 'per-document-linkage', 'raw-rdap-publications', 'raw-whois-responses', 'published-contacts', 'source-endpoints', 'normalised-evidence', 'collection-context'],
|
|
282
|
+
excludedCategories: ['target-identifiers', 'query-values', 'per-document-linkage', 'raw-rdap-publications', 'raw-whois-responses', 'published-contacts', 'source-endpoints', 'normalised-evidence', 'collection-context', 'collection-receipt'],
|
|
272
283
|
notePolicy: 'not_applicable',
|
|
273
284
|
retention: 'transient_report',
|
|
274
285
|
network: 'none',
|
|
@@ -292,10 +303,10 @@ export const CLI_LOOKUP_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
|
|
|
292
303
|
operation: 'collect-and-serialise-fast-or-deep',
|
|
293
304
|
acceptedContracts: [],
|
|
294
305
|
emittedContract: { schema: CLI_LOOKUP_SCHEMA, version: CLI_LOOKUP_VERSION },
|
|
295
|
-
shapeIds: [
|
|
306
|
+
shapeIds: [`cli-lookup.document.v${CLI_LOOKUP_VERSION}`],
|
|
296
307
|
boundProfileIds: ['cli-lookup.document-bounds.v1'],
|
|
297
308
|
hookIds: ['cli-lookup.cli.build', 'cli-lookup.cli.serialise'],
|
|
298
|
-
serialisationProfileId: 'cli-lookup.json.v1-
|
|
309
|
+
serialisationProfileId: 'cli-lookup.json.v1-v3',
|
|
299
310
|
privacyProfileId: 'cli-lookup.sensitive-collection.v1',
|
|
300
311
|
expiryPolicyId: 'cli-lookup.expiry.not-applicable.v1',
|
|
301
312
|
requestMode: 'explicit_bounded_passive_fast_or_deep',
|
|
@@ -307,9 +318,9 @@ export const CLI_LOOKUP_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
|
|
|
307
318
|
id: 'cli-lookup.cli-read-all',
|
|
308
319
|
plane: 'cli',
|
|
309
320
|
operation: 'read-all-query-types',
|
|
310
|
-
acceptedContracts: [{ schema: CLI_LOOKUP_SCHEMA, versions:
|
|
321
|
+
acceptedContracts: [{ schema: CLI_LOOKUP_SCHEMA, versions: SUPPORTED_CLI_LOOKUP_VERSIONS, mode: 'direct' }],
|
|
311
322
|
emittedContract: null,
|
|
312
|
-
shapeIds:
|
|
323
|
+
shapeIds: SUPPORTED_CLI_LOOKUP_VERSIONS.map(version => `cli-lookup.document.v${version}`),
|
|
313
324
|
boundProfileIds: ['cli-lookup.document-bounds.v1'],
|
|
314
325
|
hookIds: ['cli-lookup.cli.parse-all'],
|
|
315
326
|
serialisationProfileId: null,
|
|
@@ -324,9 +335,9 @@ export const CLI_LOOKUP_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
|
|
|
324
335
|
id: 'cli-lookup.cli-read-domain',
|
|
325
336
|
plane: 'cli',
|
|
326
337
|
operation: 'read-domain-only',
|
|
327
|
-
acceptedContracts: [{ schema: CLI_LOOKUP_SCHEMA, versions:
|
|
338
|
+
acceptedContracts: [{ schema: CLI_LOOKUP_SCHEMA, versions: SUPPORTED_CLI_LOOKUP_VERSIONS, mode: 'direct', discriminator: { path: '$.type', values: ['domain'] } }],
|
|
328
339
|
emittedContract: null,
|
|
329
|
-
shapeIds:
|
|
340
|
+
shapeIds: SUPPORTED_CLI_LOOKUP_VERSIONS.map(version => `cli-lookup.document.v${version}`),
|
|
330
341
|
boundProfileIds: ['cli-lookup.document-bounds.v1'],
|
|
331
342
|
hookIds: ['cli-lookup.cli.parse-domain'],
|
|
332
343
|
serialisationProfileId: null,
|
|
@@ -341,9 +352,9 @@ export const CLI_LOOKUP_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
|
|
|
341
352
|
id: 'cli-lookup.cli-source-report',
|
|
342
353
|
plane: 'cli',
|
|
343
354
|
operation: 'build-source-reliability-report',
|
|
344
|
-
acceptedContracts: [{ schema: CLI_LOOKUP_SCHEMA, versions:
|
|
355
|
+
acceptedContracts: [{ schema: CLI_LOOKUP_SCHEMA, versions: SUPPORTED_CLI_LOOKUP_VERSIONS, mode: 'direct' }],
|
|
345
356
|
emittedContract: null,
|
|
346
|
-
shapeIds:
|
|
357
|
+
shapeIds: SUPPORTED_CLI_LOOKUP_VERSIONS.map(version => `cli-lookup.document.v${version}`),
|
|
347
358
|
boundProfileIds: ['cli-lookup.document-bounds.v1'],
|
|
348
359
|
hookIds: ['cli-lookup.cli.normalise', 'cli-lookup.cli.source-report'],
|
|
349
360
|
serialisationProfileId: null,
|
|
@@ -358,9 +369,9 @@ export const CLI_LOOKUP_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
|
|
|
358
369
|
id: 'cli-lookup.cli-offline-verify',
|
|
359
370
|
plane: 'cli',
|
|
360
371
|
operation: 'verify-structure',
|
|
361
|
-
acceptedContracts: [{ schema: CLI_LOOKUP_SCHEMA, versions:
|
|
372
|
+
acceptedContracts: [{ schema: CLI_LOOKUP_SCHEMA, versions: SUPPORTED_CLI_LOOKUP_VERSIONS, mode: 'direct' }],
|
|
362
373
|
emittedContract: null,
|
|
363
|
-
shapeIds:
|
|
374
|
+
shapeIds: SUPPORTED_CLI_LOOKUP_VERSIONS.map(version => `cli-lookup.document.v${version}`),
|
|
364
375
|
boundProfileIds: ['cli-lookup.document-bounds.v1'],
|
|
365
376
|
hookIds: ['cli-lookup.cli.parse-all', 'cli-lookup.cli.offline-verify'],
|
|
366
377
|
serialisationProfileId: null,
|
|
@@ -375,9 +386,9 @@ export const CLI_LOOKUP_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
|
|
|
375
386
|
id: 'cli-lookup.cli-sharing-review',
|
|
376
387
|
plane: 'cli',
|
|
377
388
|
operation: 'build-redacted-sharing-review',
|
|
378
|
-
acceptedContracts: [{ schema: CLI_LOOKUP_SCHEMA, versions:
|
|
389
|
+
acceptedContracts: [{ schema: CLI_LOOKUP_SCHEMA, versions: SUPPORTED_CLI_LOOKUP_VERSIONS, mode: 'direct' }],
|
|
379
390
|
emittedContract: null,
|
|
380
|
-
shapeIds:
|
|
391
|
+
shapeIds: SUPPORTED_CLI_LOOKUP_VERSIONS.map(version => `cli-lookup.document.v${version}`),
|
|
381
392
|
boundProfileIds: ['cli-lookup.document-bounds.v1'],
|
|
382
393
|
hookIds: ['cli-lookup.cli.parse-all', 'cli-lookup.cli.sharing-review'],
|
|
383
394
|
serialisationProfileId: null,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { defineSchemaCompatibility } from "./schema-compatibility.mjs";
|
|
2
|
+
import { LEGACY_REGISTRY_INSIGHTS_VERSION, REGISTRY_INSIGHTS_VERSION } from "./registry-insights.mjs";
|
|
2
3
|
export const LOOKUP_EVIDENCE_SCHEMA = 'whoisleuth.lookup-evidence';
|
|
3
4
|
export const V1_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION = 26;
|
|
4
5
|
export const PUBLISHED_V2_LOOKUP_EVIDENCE_SCHEMA_VERSION = 27;
|
|
@@ -6,14 +7,23 @@ export const REGISTRAR_STANDING_LOOKUP_EVIDENCE_SCHEMA_VERSION = 28;
|
|
|
6
7
|
export const LATEST_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION = REGISTRAR_STANDING_LOOKUP_EVIDENCE_SCHEMA_VERSION;
|
|
7
8
|
export const HOSTNAME_SCOPED_LOOKUP_EVIDENCE_SCHEMA_VERSION = 29;
|
|
8
9
|
export const PRIVACY_MINIMIZED_LOOKUP_EVIDENCE_SCHEMA_VERSION = PUBLISHED_V2_LOOKUP_EVIDENCE_SCHEMA_VERSION;
|
|
9
|
-
export const
|
|
10
|
+
export const FORM_ATTRIBUTED_LOOKUP_EVIDENCE_SCHEMA_VERSION = 30;
|
|
11
|
+
export const AUTHORITY_AWARE_LOOKUP_EVIDENCE_SCHEMA_VERSION = 31;
|
|
12
|
+
export const LOOKUP_EVIDENCE_SCHEMA_VERSION = AUTHORITY_AWARE_LOOKUP_EVIDENCE_SCHEMA_VERSION;
|
|
10
13
|
export const SUPPORTED_LOOKUP_EVIDENCE_SCHEMA_VERSIONS = Object.freeze([
|
|
11
14
|
V1_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION,
|
|
12
15
|
PUBLISHED_V2_LOOKUP_EVIDENCE_SCHEMA_VERSION,
|
|
13
16
|
LATEST_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION,
|
|
14
17
|
HOSTNAME_SCOPED_LOOKUP_EVIDENCE_SCHEMA_VERSION,
|
|
18
|
+
FORM_ATTRIBUTED_LOOKUP_EVIDENCE_SCHEMA_VERSION,
|
|
15
19
|
LOOKUP_EVIDENCE_SCHEMA_VERSION,
|
|
16
20
|
]);
|
|
21
|
+
export function lookupEvidenceRegistryInsightsVersion(schemaVersion) {
|
|
22
|
+
if (!SUPPORTED_LOOKUP_EVIDENCE_SCHEMA_VERSIONS.includes(schemaVersion))
|
|
23
|
+
throw new TypeError('Unsupported Lookup evidence version.');
|
|
24
|
+
return schemaVersion >= AUTHORITY_AWARE_LOOKUP_EVIDENCE_SCHEMA_VERSION
|
|
25
|
+
? REGISTRY_INSIGHTS_VERSION : LEGACY_REGISTRY_INSIGHTS_VERSION;
|
|
26
|
+
}
|
|
17
27
|
export const LOOKUP_EVIDENCE_PORTABLE_MAX_BYTES = 5 * 1024 * 1024;
|
|
18
28
|
export const LOOKUP_EVIDENCE_PORTABLE_MAX_ENTRIES = 20_000;
|
|
19
29
|
export const LOOKUP_EVIDENCE_PORTABLE_MAX_DEPTH = 24;
|
|
@@ -32,5 +42,5 @@ export const LOOKUP_EVIDENCE_COMPATIBILITY = defineSchemaCompatibility({
|
|
|
32
42
|
writeSemantics: 'read_only',
|
|
33
43
|
byteBudget: LOOKUP_EVIDENCE_PORTABLE_MAX_BYTES,
|
|
34
44
|
owner: 'packages/contracts/lookup-evidence.mts',
|
|
35
|
-
note: '
|
|
45
|
+
note: 'Versions 26–30 retain their registry insight derivation. Version 30 adds attributed form destinations. Version 31 retains WHOIS registration classification; collection completion is not positive registration evidence.',
|
|
36
46
|
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Version 1 is retained for exact historical evidence verification. */
|
|
2
|
+
export const LEGACY_REGISTRY_INSIGHTS_VERSION = 1;
|
|
3
|
+
export const REGISTRY_INSIGHTS_VERSION = 2;
|
|
4
|
+
export function isSupportedRegistryInsightsVersion(value) {
|
|
5
|
+
return value === LEGACY_REGISTRY_INSIGHTS_VERSION || value === REGISTRY_INSIGHTS_VERSION;
|
|
6
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { isValidAsciiHostname } from "../contracts/domain-name.mjs";
|
|
2
|
+
import { normalizeExplicitIsoTimestamp } from "./observation.mjs";
|
|
3
|
+
export const MAX_DMARC_DISCOVERY_OWNERS = 8;
|
|
4
|
+
function closedRecord(value, keys) {
|
|
5
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
6
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(value))
|
|
7
|
+
|| Reflect.ownKeys(value).length !== keys.length)
|
|
8
|
+
throw new TypeError('DMARC policy context has an invalid structure.');
|
|
9
|
+
const result = {};
|
|
10
|
+
for (const key of keys) {
|
|
11
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
12
|
+
if (!descriptor?.enumerable || !('value' in descriptor))
|
|
13
|
+
throw new TypeError('DMARC policy context requires ordinary data fields.');
|
|
14
|
+
result[key] = descriptor.value;
|
|
15
|
+
}
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
function owner(value) {
|
|
19
|
+
return typeof value === 'string' && value.length <= 253
|
|
20
|
+
&& isValidAsciiHostname(value, { requireDot: false, requireLowercase: true });
|
|
21
|
+
}
|
|
22
|
+
function time(value) {
|
|
23
|
+
if (value === null)
|
|
24
|
+
return null;
|
|
25
|
+
const normalized = typeof value === 'string' && value.length <= 64 ? normalizeExplicitIsoTimestamp(value) : null;
|
|
26
|
+
if (normalized === null)
|
|
27
|
+
throw new TypeError('DMARC policy context has an invalid observation time.');
|
|
28
|
+
return normalized;
|
|
29
|
+
}
|
|
30
|
+
/** Current opted-in review only; absent historical context is never reconstructed. */
|
|
31
|
+
export function normalizeDmarcPolicyContext(value, expectedDomain) {
|
|
32
|
+
if (value === undefined)
|
|
33
|
+
return undefined;
|
|
34
|
+
const input = closedRecord(value, ['version', 'source', 'relation', 'complete', 'owner', 'observedAt', 'policy', 'subdomainPolicy', 'nonexistentPolicy', 'psd', 'testMode', 'discovery']);
|
|
35
|
+
if (input.version !== 1 || input.source !== 'recursive_dns_txt' || typeof input.complete !== 'boolean'
|
|
36
|
+
|| typeof input.relation !== 'string' || !['exact', 'inherited', 'undetermined'].includes(input.relation)
|
|
37
|
+
|| !Array.isArray(input.discovery) || input.discovery.length < 1 || input.discovery.length > MAX_DMARC_DISCOVERY_OWNERS) {
|
|
38
|
+
throw new TypeError('DMARC policy context has an unsupported version or discovery state.');
|
|
39
|
+
}
|
|
40
|
+
const discovery = [];
|
|
41
|
+
for (let index = 0; index < input.discovery.length; index++) {
|
|
42
|
+
const entry = Object.getOwnPropertyDescriptor(input.discovery, String(index));
|
|
43
|
+
if (!entry?.enumerable || !('value' in entry))
|
|
44
|
+
throw new TypeError('DMARC discovery requires ordinary entries.');
|
|
45
|
+
const row = closedRecord(entry.value, ['owner', 'state', 'observedAt']);
|
|
46
|
+
if (!owner(row.owner) || typeof row.state !== 'string' || !['observed', 'not_found', 'invalid', 'unavailable'].includes(row.state)) {
|
|
47
|
+
throw new TypeError('DMARC discovery has an invalid owner or state.');
|
|
48
|
+
}
|
|
49
|
+
discovery.push(Object.freeze({ owner: row.owner, state: row.state, observedAt: time(row.observedAt) }));
|
|
50
|
+
}
|
|
51
|
+
if (new Set(discovery.map(row => row.owner)).size !== discovery.length
|
|
52
|
+
|| (expectedDomain !== undefined && discovery[0].owner !== expectedDomain)
|
|
53
|
+
|| discovery.some((row, index) => index > 0 && !discovery[index - 1].owner.endsWith(`.${row.owner}`))
|
|
54
|
+
|| (input.complete && discovery.some(row => row.state === 'unavailable'))) {
|
|
55
|
+
throw new TypeError('DMARC discovery does not match one bounded owner walk.');
|
|
56
|
+
}
|
|
57
|
+
const observedAt = time(input.observedAt);
|
|
58
|
+
if (input.relation === 'undetermined') {
|
|
59
|
+
if ([input.owner, observedAt, input.policy, input.subdomainPolicy, input.nonexistentPolicy, input.psd, input.testMode].some(field => field !== null)) {
|
|
60
|
+
throw new TypeError('Undetermined DMARC policy must not declare a selected policy.');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
const selected = discovery.find(row => row.owner === input.owner);
|
|
65
|
+
if (!input.complete || !owner(input.owner) || !selected || selected.state !== 'observed' || selected.observedAt !== observedAt
|
|
66
|
+
|| (input.relation === 'exact') !== (input.owner === discovery[0].owner)
|
|
67
|
+
|| [input.policy, input.subdomainPolicy, input.nonexistentPolicy].some(field => typeof field !== 'string' || !['none', 'quarantine', 'reject'].includes(field))
|
|
68
|
+
|| typeof input.psd !== 'string' || !['u', 'n', 'y'].includes(input.psd) || typeof input.testMode !== 'boolean') {
|
|
69
|
+
throw new TypeError('DMARC policy does not match a complete selected observation.');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return Object.freeze({
|
|
73
|
+
version: 1, source: 'recursive_dns_txt', relation: input.relation,
|
|
74
|
+
complete: input.complete, owner: input.owner, observedAt,
|
|
75
|
+
policy: input.policy, subdomainPolicy: input.subdomainPolicy,
|
|
76
|
+
nonexistentPolicy: input.nonexistentPolicy,
|
|
77
|
+
psd: input.psd, testMode: input.testMode,
|
|
78
|
+
discovery: Object.freeze(discovery),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// A bounded account of one completed Lookup operation. This records collector
|
|
2
|
+
// execution, not proof of network delivery, independent verification or history.
|
|
3
|
+
export const LOOKUP_SOURCE_IDS = Object.freeze([
|
|
4
|
+
'rdap', 'whois', 'domain_evidence', 'reverse_dns', 'registrar_rdap',
|
|
5
|
+
'network_context', 'security_txt', 'external_intelligence',
|
|
6
|
+
'malware_host_intelligence', 'malware_ioc_intelligence',
|
|
7
|
+
]);
|
|
8
|
+
export const LOOKUP_SOURCE_STATES = Object.freeze([
|
|
9
|
+
'success', 'partial', 'not_found', 'skipped', 'error', 'unsupported',
|
|
10
|
+
'not_applicable', 'unavailable', 'rate_limited', 'complete', 'disabled', 'stale',
|
|
11
|
+
]);
|
|
12
|
+
export const LOOKUP_COLLECTION_RECEIPT_VERSION = 1;
|
|
13
|
+
export const LOOKUP_CHOICE_KEYS = Object.freeze([
|
|
14
|
+
'securityTxt', 'externalIntelligence', 'malwareHostIntelligence', 'malwareIocIntelligence',
|
|
15
|
+
]);
|
|
16
|
+
export const LOOKUP_CHOICE_SOURCES = Object.freeze({
|
|
17
|
+
securityTxt: 'security_txt', externalIntelligence: 'external_intelligence',
|
|
18
|
+
malwareHostIntelligence: 'malware_host_intelligence', malwareIocIntelligence: 'malware_ioc_intelligence',
|
|
19
|
+
});
|
|
20
|
+
const ADMISSIONS = ['admitted', 'disabled', 'unsupported', 'not_applicable', 'prerequisite_unavailable', 'unknown'];
|
|
21
|
+
function fields(value, keys) {
|
|
22
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
23
|
+
throw new TypeError('Collection receipt must contain ordinary objects.');
|
|
24
|
+
const prototype = Object.getPrototypeOf(value);
|
|
25
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
26
|
+
throw new TypeError('Collection receipt must contain ordinary objects.');
|
|
27
|
+
const own = Reflect.ownKeys(value);
|
|
28
|
+
if (own.length !== keys.length || own.some(key => typeof key !== 'string' || !keys.includes(key))) {
|
|
29
|
+
throw new TypeError('Collection receipt contains an unsupported field set.');
|
|
30
|
+
}
|
|
31
|
+
const output = {};
|
|
32
|
+
for (const key of keys) {
|
|
33
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
34
|
+
if (!descriptor?.enumerable || !('value' in descriptor))
|
|
35
|
+
throw new TypeError('Collection receipt contains an invalid field.');
|
|
36
|
+
output[key] = descriptor.value;
|
|
37
|
+
}
|
|
38
|
+
return output;
|
|
39
|
+
}
|
|
40
|
+
function oneOf(value, options) {
|
|
41
|
+
if (typeof value !== 'string' || !options.includes(value))
|
|
42
|
+
throw new TypeError('Collection receipt contains an invalid state.');
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
function quality(value) {
|
|
46
|
+
if (value !== null && typeof value !== 'boolean')
|
|
47
|
+
throw new TypeError('Collection receipt quality must be explicit or unknown.');
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
export function lookupCollectionChoices(input) {
|
|
51
|
+
return Object.freeze({
|
|
52
|
+
securityTxt: input.securityTxt === true,
|
|
53
|
+
externalIntelligence: input.externalIntelligence === true,
|
|
54
|
+
malwareHostIntelligence: input.malwareHostIntelligence === true,
|
|
55
|
+
malwareIocIntelligence: input.malwareIocIntelligence === true,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/** Absence is historical unknown, never reconstructed as a negative choice. */
|
|
59
|
+
export function parseLookupCollectionReceipt(raw) {
|
|
60
|
+
if (raw === undefined)
|
|
61
|
+
return null;
|
|
62
|
+
const value = fields(raw, ['version', 'requestedDepth', 'targetClass', 'webSelection', 'originalChoices', 'sources']);
|
|
63
|
+
if (value.version !== LOOKUP_COLLECTION_RECEIPT_VERSION)
|
|
64
|
+
throw new TypeError('Collection receipt version is unsupported.');
|
|
65
|
+
const choices = fields(value.originalChoices, LOOKUP_CHOICE_KEYS);
|
|
66
|
+
if (Object.values(choices).some(choice => typeof choice !== 'boolean'))
|
|
67
|
+
throw new TypeError('Collection receipt choices must be explicit.');
|
|
68
|
+
const requestedDepth = oneOf(value.requestedDepth, ['fast', 'deep']);
|
|
69
|
+
const targetClass = oneOf(value.targetClass, ['domain', 'ipv4', 'ipv6', 'asn']);
|
|
70
|
+
const webSelection = oneOf(value.webSelection, ['hostname', 'selected_url', 'not_applicable']);
|
|
71
|
+
if ((targetClass !== 'domain' && webSelection !== 'not_applicable') || (webSelection === 'selected_url' && requestedDepth !== 'deep')) {
|
|
72
|
+
throw new TypeError('Collection receipt target and web selection disagree.');
|
|
73
|
+
}
|
|
74
|
+
if (!Array.isArray(value.sources) || Object.getPrototypeOf(value.sources) !== Array.prototype
|
|
75
|
+
|| value.sources.length !== LOOKUP_SOURCE_IDS.length
|
|
76
|
+
|| Reflect.ownKeys(value.sources).length !== LOOKUP_SOURCE_IDS.length + 1)
|
|
77
|
+
throw new TypeError('Collection receipt source inventory is invalid.');
|
|
78
|
+
const bySource = new Map();
|
|
79
|
+
for (let index = 0; index < LOOKUP_SOURCE_IDS.length; index++) {
|
|
80
|
+
const descriptor = Object.getOwnPropertyDescriptor(value.sources, String(index));
|
|
81
|
+
if (!descriptor?.enumerable || !('value' in descriptor))
|
|
82
|
+
throw new TypeError('Collection receipt source entry is invalid.');
|
|
83
|
+
const row = fields(descriptor.value, ['source', 'intent', 'admission', 'attempt', 'settlement', 'complete', 'truncated']);
|
|
84
|
+
const source = oneOf(row.source, LOOKUP_SOURCE_IDS);
|
|
85
|
+
if (bySource.has(source))
|
|
86
|
+
throw new TypeError('Collection receipt source is duplicated.');
|
|
87
|
+
const attempt = oneOf(row.attempt, ['started', 'not_started', 'unknown']);
|
|
88
|
+
const admission = oneOf(row.admission, ADMISSIONS);
|
|
89
|
+
const complete = quality(row.complete), truncated = quality(row.truncated);
|
|
90
|
+
const intent = oneOf(row.intent, ['requested', 'not_requested']);
|
|
91
|
+
const settlement = oneOf(row.settlement, [...LOOKUP_SOURCE_STATES, 'unknown']);
|
|
92
|
+
if (attempt === 'started' && admission !== 'admitted'
|
|
93
|
+
|| attempt === 'started' && intent !== 'requested'
|
|
94
|
+
|| attempt !== 'started' && (complete !== null || truncated !== null)
|
|
95
|
+
|| attempt === 'not_started' && settlement !== 'unknown'
|
|
96
|
+
|| complete === true && !['success', 'complete', 'not_found'].includes(settlement)
|
|
97
|
+
|| complete === true && truncated === true)
|
|
98
|
+
throw new TypeError('Collection receipt execution and quality disagree.');
|
|
99
|
+
bySource.set(source, Object.freeze({
|
|
100
|
+
source, intent, admission, attempt, settlement, complete, truncated,
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
for (const choice of LOOKUP_CHOICE_KEYS) {
|
|
104
|
+
if ((bySource.get(LOOKUP_CHOICE_SOURCES[choice]).intent === 'requested') !== choices[choice]) {
|
|
105
|
+
throw new TypeError('Collection receipt choice and source intent disagree.');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return Object.freeze({ version: LOOKUP_COLLECTION_RECEIPT_VERSION, requestedDepth, targetClass, webSelection,
|
|
109
|
+
originalChoices: lookupCollectionChoices(choices),
|
|
110
|
+
sources: Object.freeze(LOOKUP_SOURCE_IDS.map(source => bySource.get(source))),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/** Bind a validated execution account to this submission's pre-filter choices.
|
|
114
|
+
* A browser may omit an ineligible optional request; the server cannot know
|
|
115
|
+
* which unchecked/disabled control led to that omission. Do not invent why. */
|
|
116
|
+
export function bindLookupCollectionChoices(raw, originalChoices) {
|
|
117
|
+
const receipt = parseLookupCollectionReceipt(raw);
|
|
118
|
+
if (!receipt)
|
|
119
|
+
return null;
|
|
120
|
+
const choices = fields(originalChoices, LOOKUP_CHOICE_KEYS);
|
|
121
|
+
if (Object.values(choices).some(choice => typeof choice !== 'boolean'))
|
|
122
|
+
throw new TypeError('Collection receipt choices must be explicit.');
|
|
123
|
+
const sources = receipt.sources.map(row => {
|
|
124
|
+
const choice = LOOKUP_CHOICE_KEYS.find(key => LOOKUP_CHOICE_SOURCES[key] === row.source);
|
|
125
|
+
if (!choice || choices[choice] === receipt.originalChoices[choice])
|
|
126
|
+
return { ...row };
|
|
127
|
+
if (receipt.originalChoices[choice] || choices[choice] !== true || row.attempt !== 'not_started') {
|
|
128
|
+
throw new TypeError('Collection receipt does not match the submitted choices.');
|
|
129
|
+
}
|
|
130
|
+
return { ...row, intent: 'requested', admission: 'unknown' };
|
|
131
|
+
});
|
|
132
|
+
const bound = { ...receipt, originalChoices: lookupCollectionChoices(choices), sources };
|
|
133
|
+
parseLookupCollectionReceipt(bound);
|
|
134
|
+
return bound;
|
|
135
|
+
}
|
|
136
|
+
/** Stable, payload-free material comparison. Parsing also removes key order as a difference. */
|
|
137
|
+
export function lookupCollectionReceiptIdentity(raw) {
|
|
138
|
+
const receipt = parseLookupCollectionReceipt(raw);
|
|
139
|
+
return receipt ? JSON.stringify(receipt) : null;
|
|
140
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { LOOKUP_CHOICE_KEYS, LOOKUP_CHOICE_SOURCES, parseLookupCollectionReceipt } from "./lookup-collection-receipt.mjs";
|
|
2
|
+
export const LOOKUP_SOURCE_LABELS = Object.freeze({
|
|
3
|
+
rdap: 'Registry RDAP', whois: 'WHOIS chain', domain_evidence: 'Domain evidence',
|
|
4
|
+
reverse_dns: 'Reverse DNS', registrar_rdap: 'Registrar RDAP', network_context: 'Network context',
|
|
5
|
+
security_txt: 'security.txt', external_intelligence: 'Archived web intelligence',
|
|
6
|
+
malware_host_intelligence: 'Malware host intelligence', malware_ioc_intelligence: 'Malware infrastructure intelligence',
|
|
7
|
+
});
|
|
8
|
+
const ADMISSION_LABELS = Object.freeze({
|
|
9
|
+
admitted: 'Admitted', disabled: 'Disabled by policy', unsupported: 'Unsupported', not_applicable: 'Not applicable',
|
|
10
|
+
prerequisite_unavailable: 'Required earlier evidence unavailable', unknown: 'Admission not recorded',
|
|
11
|
+
});
|
|
12
|
+
const ATTEMPT_LABELS = Object.freeze({ started: 'Collector started', not_started: 'Collector not started', unknown: 'Collector start unknown' });
|
|
13
|
+
/** Inert guidance from recorded states, never an admission or retry decision. */
|
|
14
|
+
export function lookupSourceStateExplanation(state, source, receipt) {
|
|
15
|
+
if (receipt && receipt.source === source && receipt.attempt === 'not_started') {
|
|
16
|
+
if (receipt.intent === 'not_requested')
|
|
17
|
+
return 'Not requested in this Lookup. Select it explicitly in a new Lookup if needed.';
|
|
18
|
+
if (receipt.admission === 'disabled')
|
|
19
|
+
return 'Collection was disabled by policy. Retrying does not change that policy.';
|
|
20
|
+
if (receipt.admission === 'prerequisite_unavailable')
|
|
21
|
+
return 'Required earlier evidence was unavailable, so this collector did not start. Review that source first.';
|
|
22
|
+
if (receipt.admission === 'not_applicable')
|
|
23
|
+
return 'This collector did not apply to the submitted target or mode.';
|
|
24
|
+
if (receipt.admission === 'unsupported')
|
|
25
|
+
return 'Collection was not supported for this target; retrying unchanged inputs will not add evidence.';
|
|
26
|
+
}
|
|
27
|
+
switch (state) {
|
|
28
|
+
case 'success':
|
|
29
|
+
case 'complete':
|
|
30
|
+
return 'Collection completed within this source’s scope. Review its observations and time below.';
|
|
31
|
+
case 'not_found':
|
|
32
|
+
return 'This source returned no matching record. Its authority and search scope determine what that means.';
|
|
33
|
+
case 'partial':
|
|
34
|
+
case 'inconclusive':
|
|
35
|
+
return 'Evidence is incomplete or inconclusive. Missing fields remain unknown; a later retry may help if source conditions change.';
|
|
36
|
+
case 'error':
|
|
37
|
+
case 'unavailable':
|
|
38
|
+
return 'Usable evidence was not obtained. This is not an absence finding; retry may help after the reported problem changes.';
|
|
39
|
+
case 'rate_limited':
|
|
40
|
+
return 'The source reported a rate limit. Wait for its permitted retry window; no reset time is assumed.';
|
|
41
|
+
case 'unsupported':
|
|
42
|
+
return source === 'rdap' || source === 'whois'
|
|
43
|
+
? 'No applicable service was recorded by registry policy or discovery. Retrying unchanged discovery may not add evidence.'
|
|
44
|
+
: 'This source does not support the target or request. Use an applicable source rather than repeating the same request.';
|
|
45
|
+
case 'skipped':
|
|
46
|
+
return 'This source was not queried under the selected mode or access policy. Review its source details before requesting it again.';
|
|
47
|
+
case 'disabled':
|
|
48
|
+
return 'Collection was disabled by policy. Retrying does not change that policy.';
|
|
49
|
+
case 'not_applicable':
|
|
50
|
+
return 'This source does not apply to the submitted target or mode.';
|
|
51
|
+
case 'stale':
|
|
52
|
+
return 'Retained evidence is out of date for this review. A deliberate refresh may provide a newer observation.';
|
|
53
|
+
default:
|
|
54
|
+
return 'The source outcome was not retained. Its request history and retry usefulness are unknown.';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** One presentation of the closed receipt vocabulary for browser and terminal. */
|
|
58
|
+
export function lookupCollectionReceiptReview(raw) {
|
|
59
|
+
const receipt = parseLookupCollectionReceipt(raw);
|
|
60
|
+
if (!receipt)
|
|
61
|
+
return null;
|
|
62
|
+
return {
|
|
63
|
+
receipt,
|
|
64
|
+
selection: `${receipt.requestedDepth === 'deep' ? 'Deep' : 'Fast'} Lookup · ${receipt.webSelection === 'selected_url'
|
|
65
|
+
? 'selected URL' : receipt.webSelection === 'hostname' ? 'hostname' : 'no web target'}`,
|
|
66
|
+
choices: LOOKUP_CHOICE_KEYS.map(choice => ({ label: LOOKUP_SOURCE_LABELS[LOOKUP_CHOICE_SOURCES[choice]], selected: receipt.originalChoices[choice] })),
|
|
67
|
+
sources: receipt.sources.map(row => ({
|
|
68
|
+
id: row.source, label: LOOKUP_SOURCE_LABELS[row.source],
|
|
69
|
+
summary: `${row.intent === 'requested' ? 'Requested' : 'Not requested'} · ${ADMISSION_LABELS[row.admission]} · ${ATTEMPT_LABELS[row.attempt]}`,
|
|
70
|
+
outcome: `Source result: ${row.settlement.replaceAll('_', ' ')} · ${row.complete === null ? 'completeness unknown' : row.complete ? 'complete' : 'not complete'} · ${row.truncated === null ? 'truncation unknown' : row.truncated ? 'truncated' : 'not truncated'}`,
|
|
71
|
+
explanation: lookupSourceStateExplanation(row.settlement, row.source, row),
|
|
72
|
+
})),
|
|
73
|
+
};
|
|
74
|
+
}
|