@slicedearth/whoisleuth-cli 2.2.0 → 2.4.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/NOTICE +6 -0
- package/README.md +20 -15
- package/bin/whoisleuth.mjs +8 -1
- package/cli/archive-content-digest.mjs +14 -0
- package/cli/archive-inspect.mjs +20 -53
- package/cli/arguments.mjs +454 -1748
- package/cli/artifact-validation/case-response.mjs +66 -35
- package/cli/artifact-validation/investigation-capsule.mjs +52 -31
- package/cli/artifact-validation/lookup-evidence.mjs +12 -6
- package/cli/artifact-validation/signed-review.mjs +112 -72
- package/cli/artifact-validation/structure-primitives.mjs +1 -105
- package/cli/artifact-verify.mjs +90 -24
- package/cli/assurance-command-runner.mjs +39 -4
- package/cli/bagit-folder.mjs +57 -0
- package/cli/bagit-review.mjs +23 -0
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +34 -3
- package/cli/bulk-command-runner.mjs +2 -0
- package/cli/bulk-output.mjs +29 -3
- package/cli/case-command.mjs +195 -0
- package/cli/case-pack.mjs +66 -111
- package/cli/command-argument-grammar.mjs +215 -0
- package/cli/command-reference.mjs +342 -285
- package/cli/completion.mjs +303 -80
- package/cli/config-profile.mjs +7 -5
- package/cli/ct-event-intake.mjs +12 -12
- package/cli/discover.mjs +4 -3
- package/cli/discovery-observation-snapshot.mjs +59 -18
- package/cli/discovery-scan-command-runner.mjs +21 -0
- package/cli/discovery-scan.mjs +13 -4
- package/cli/discovery-workflow.mjs +1 -1
- package/cli/domain-control-monitor.mjs +50 -20
- package/cli/domain-control-observations.mjs +149 -102
- package/cli/errors.mjs +33 -1
- package/cli/evidence-command-runner.mjs +16 -5
- package/cli/evidence-signing.mjs +2 -2
- package/cli/external-observation-mapping.mjs +7 -8
- package/cli/formatters/terminal-lookup.mjs +18 -46
- package/cli/formatters/terminal-shared.mjs +5 -3
- package/cli/formatters/terminal-technology.mjs +114 -0
- package/cli/inline-command-families.mjs +2 -0
- package/cli/input.mjs +116 -0
- package/cli/interactive-launcher.mjs +2 -160
- package/cli/interchange-report.mjs +18 -8
- package/cli/investigation-artifacts.mjs +176 -0
- package/cli/investigation-checkpoint.mjs +10 -0
- package/cli/investigation-folder.mjs +114 -0
- package/cli/investigation-manifest.mjs +1 -112
- package/cli/investigation-package-review.mjs +103 -0
- package/cli/investigation-plan.mjs +32 -19
- package/cli/investigation-run.mjs +188 -33
- package/cli/local-document-checkpoint.mjs +170 -0
- package/cli/lookalike-calibration.mjs +7 -2
- package/cli/lookup-brief.mjs +4 -2
- package/cli/lookup-browser-view.mjs +1 -1
- package/cli/lookup-browser.mjs +5 -12
- package/cli/lookup-command-runner.mjs +17 -6
- package/cli/lookup-diff.mjs +37 -3
- package/cli/lookup-plan.mjs +11 -5
- package/cli/lookup-reconcile.mjs +10 -8
- package/cli/mail-header-review.mjs +467 -0
- package/cli/mail-review.mjs +66 -14
- package/cli/manual.mjs +1 -0
- package/cli/network-command-runner.mjs +2 -0
- package/cli/output-file.mjs +31 -5
- package/cli/registry-doctor.mjs +3 -1
- package/cli/registry-support.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +2 -2
- package/cli/review-command-runner.mjs +70 -0
- package/cli/risk-calibration.mjs +19 -17
- package/cli/runner.mjs +62 -7
- package/cli/sharing-review.mjs +21 -34
- package/cli/signer-trust.mjs +104 -0
- package/cli/source-reliability.mjs +74 -83
- package/cli/terminal-input.mjs +168 -0
- package/cli/workflow-command-runner.mjs +68 -13
- package/docs/cli-reference.md +77 -24
- package/docs/cli.md +300 -25
- package/lib/abort.mjs +4 -1
- package/lib/availability.mjs +161 -112
- package/lib/bounded-file.mjs +3 -1
- package/lib/bounded-html-document.mjs +125 -0
- package/lib/bounded-json.mjs +51 -8
- package/lib/browser-library-profile.mjs +42 -51
- package/lib/browser-library-worker.mjs +112 -0
- package/lib/client-behavior-profile.mjs +3 -3
- package/lib/ct-search.mjs +11 -4
- package/lib/dns-convergence-review.mjs +1 -0
- package/lib/dns-delegation-health.mjs +24 -14
- package/lib/dns-inheritance-review.mjs +217 -0
- package/lib/dns-intelligence.mjs +73 -26
- package/lib/dns-mx.mjs +1 -1
- package/lib/dnssec-chain-validation.mjs +6 -0
- package/lib/dnssec-evidence-validation.mjs +11 -3
- package/lib/domain-change-packet.mjs +49 -16
- package/lib/domain-change-review.mjs +14 -8
- package/lib/domain-control-flight-recorder.mjs +221 -117
- package/lib/domain-control-manifest.mjs +129 -69
- package/lib/domain-posture-analysis.mjs +20 -7
- package/lib/domain-posture-parsers.mjs +201 -8
- package/lib/domain-posture.mjs +147 -29
- package/lib/evidence-export-privacy.mjs +561 -0
- package/lib/evidence-export.mjs +11 -543
- package/lib/evidence-report.mjs +5 -3
- package/lib/favicon.mjs +117 -41
- package/lib/generated/cisa-kev-catalog.mjs +12 -4
- package/lib/generated/registrar-standing-catalogue.mjs +7 -7
- package/lib/generated/retire-browser-catalog.mjs +2335 -940
- package/lib/hostname.mjs +2 -38
- package/lib/html-signals.mjs +111 -108
- package/lib/http-evidence-bounds.mjs +2 -1
- package/lib/http-intelligence.mjs +39 -26
- package/lib/local-geoip-evidence.mjs +19 -3
- package/lib/lookup-cache.mjs +34 -11
- package/lib/lookup-child-profile-contract.mjs +24 -15
- package/lib/lookup-diagnostics.mjs +24 -1
- package/lib/lookup-progress.mjs +30 -13
- package/lib/lookup-response-contract.mjs +1048 -0
- package/lib/lookup-response.mjs +8 -10
- package/lib/lookup-source-progress.mjs +40 -14
- package/lib/lookup.mjs +48 -27
- package/lib/nameserver-preflight-review.mjs +1 -0
- package/lib/observed-network-context.mjs +43 -27
- package/lib/outbound-request-bounds.mjs +4 -1
- package/lib/page-fingerprints.mjs +69 -133
- package/lib/page-role-profile.mjs +12 -8
- package/lib/perceptual-hash.mjs +43 -14
- package/lib/rdap-bootstrap.mjs +72 -22
- package/lib/rdap-capabilities.mjs +1 -1
- package/lib/rdap-client.mjs +15 -7
- package/lib/rdap-normalization.mjs +33 -18
- package/lib/rdap-search-workbench.mjs +1 -1
- package/lib/rdap-transport.mjs +47 -20
- package/lib/rdap-types.mjs +2 -1
- package/lib/rdap.mjs +18 -3
- package/lib/registrable-domain.mjs +21 -1
- package/lib/registrar-standing-contract.mjs +1 -0
- package/lib/registry-capabilities.mjs +5 -1
- package/lib/registry-capability-catalogue.mjs +2 -1
- package/lib/registry-dates.mjs +2 -190
- package/lib/registry-insights.mjs +85 -34
- package/lib/response-policy.mjs +86 -48
- package/lib/risk-calibration-summary.mjs +18 -6
- package/lib/rpki-evidence.mjs +8 -5
- package/lib/safe-fetch.mjs +54 -47
- package/lib/security-txt.mjs +42 -7
- package/lib/service-binding-dns.mjs +8 -0
- package/lib/smtp-transport-review.mjs +27 -15
- package/lib/sslbl-certificates.generated.mjs +136 -135
- package/lib/static-html-analysis.mjs +178 -151
- package/lib/structured-data-identity.mjs +1 -0
- package/lib/technology-markup.mjs +100 -0
- package/lib/threat-intelligence-manifests.mjs +11 -139
- package/lib/threat-intelligence-runtime.mjs +24 -52
- package/lib/threat-intelligence-targets.mjs +6 -34
- package/lib/threat-intelligence-types.mjs +39 -0
- package/lib/typosquat-generator.mjs +19 -15
- package/lib/urlhaus-intelligence.mjs +8 -3
- package/lib/urlscan-intelligence.mjs +1 -1
- package/lib/website-security-posture.mjs +30 -6
- package/lib/website-technology.mjs +153 -45
- package/lib/whois-authority.mjs +14 -2
- package/lib/whois-chain.mjs +9 -2
- package/lib/whois-common-formats.mjs +5 -5
- package/lib/whois-contracts.mjs +2 -1
- package/lib/whois-dialects.mjs +2 -2
- package/lib/whois-parser-support.mjs +8 -1
- package/lib/whois-transport.mjs +23 -7
- package/lib/zone-intent-review.mjs +16 -6
- package/package.json +2 -1
- package/packages/cases/case-attachment-model.mjs +83 -0
- package/packages/cases/case-evidence-model.mjs +156 -62
- package/packages/cases/case-export-input.mjs +53 -0
- package/packages/cases/case-migration-model.mjs +127 -44
- package/packages/cases/case-model.mjs +1 -0
- package/packages/cases/case-recheck-model.mjs +91 -0
- package/packages/cases/case-record-contracts.mjs +5 -20
- package/packages/cases/case-record-core.mjs +12 -22
- package/packages/cases/case-record-decisions.mjs +72 -0
- package/packages/cases/case-record-model.mjs +1 -0
- package/packages/cases/case-record-operations.mjs +95 -31
- package/packages/cases/case-record-projection.mjs +221 -0
- package/packages/cases/case-report.mjs +70 -54
- package/packages/cases/case-response-actions.mjs +654 -0
- package/packages/cases/case-response-model.mjs +64 -1115
- package/packages/cases/case-response-outcomes.mjs +317 -0
- package/packages/cases/case-response-packet-render.mjs +153 -0
- package/packages/cases/case-response-packet-types.mjs +1 -0
- package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
- package/packages/cases/case-response-packet.mjs +74 -572
- package/packages/cases/case-response-records.mjs +94 -0
- package/packages/cases/case-response-review-inputs.mjs +371 -0
- package/packages/cases/case-response-values.mjs +85 -0
- package/packages/cases/case-review-package.mjs +45 -0
- package/packages/cases/case-selection.mjs +46 -0
- package/packages/cases/case-storage-model.mjs +31 -2
- package/packages/cases/response-route-freshness.mjs +16 -0
- package/packages/comparison/capture-context.mjs +61 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +13 -11
- package/packages/comparison/page-similarity.mjs +5 -2
- package/packages/comparison/relationship-evidence.mjs +85 -8
- package/packages/comparison/relationship-provenance.mjs +93 -0
- package/packages/contracts/analyst-interchange.mjs +216 -0
- package/packages/contracts/analyst-review-state-contract.mjs +11 -0
- package/packages/contracts/analyst-review-state.mjs +5 -4
- package/packages/contracts/case-portability.mjs +366 -83
- package/packages/contracts/case-views-contract.mjs +12 -0
- package/packages/contracts/case-views.mjs +26 -0
- package/packages/contracts/cli-command-semantics.mjs +94 -0
- package/packages/contracts/cli-lookup.mjs +0 -1
- package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
- package/packages/contracts/domain-control-manifest.mjs +127 -73
- package/packages/contracts/domain-control-monitor.mjs +48 -45
- package/packages/contracts/domain-control-review.mjs +179 -121
- package/packages/contracts/domain-name.mjs +21 -0
- package/packages/contracts/external-observation-interchange.mjs +2 -1
- package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
- package/packages/contracts/http-url.mjs +2 -0
- package/packages/contracts/investigation-package-limits.mjs +24 -0
- package/packages/contracts/investigation-portability.mjs +56 -27
- package/packages/contracts/investigation-run.mjs +16 -0
- package/packages/contracts/ip-address.mjs +34 -0
- package/packages/contracts/lookup-evidence.mjs +35 -0
- package/packages/contracts/offline-comparison.mjs +1 -2
- package/packages/contracts/page-fingerprints.mjs +15 -0
- package/packages/contracts/risk-calibration.mjs +15 -11
- package/packages/contracts/schema-compatibility.mjs +1 -1
- package/packages/contracts/schema-lifecycle.mjs +83 -153
- package/packages/contracts/selected-file-limits.mjs +6 -0
- package/packages/contracts/vulnerability-identifiers.mjs +5 -0
- package/packages/contracts/web-capture.mjs +10 -0
- package/packages/contracts/workspace-portability.mjs +183 -211
- package/packages/evidence/artifact-integrity.mjs +8 -2
- package/packages/evidence/artifact-structure.mjs +105 -0
- package/packages/evidence/decision-fact.mjs +4 -1
- package/packages/evidence/domain-control-runtime.mjs +201 -33
- package/packages/evidence/domain-posture-context.mjs +79 -0
- package/packages/evidence/image-regions.mjs +56 -0
- package/packages/evidence/latest-observations.mjs +28 -0
- package/packages/evidence/lookup-target.mjs +104 -0
- package/packages/evidence/observation.mjs +16 -1
- package/packages/evidence/passphrase-encryption.mjs +44 -0
- package/packages/evidence/record-identity.mjs +72 -0
- package/packages/evidence/registry-dates.mjs +190 -0
- package/packages/evidence/retained-file.mjs +69 -0
- package/packages/evidence/review-age.mjs +14 -0
- package/packages/interchange/bagit.mjs +342 -0
- package/packages/interchange/bounded-zip-extraction.mjs +278 -0
- package/packages/interchange/crc32.mjs +17 -0
- package/packages/interchange/external-findings-import.mjs +92 -51
- package/packages/interchange/web-capture-import.mjs +382 -0
- package/packages/investigation/bulk-mail-exposure.mjs +2 -1
- package/packages/investigation/investigation-bagit.mjs +14 -0
- package/packages/investigation/investigation-capsule.mjs +33 -5
- package/packages/investigation/investigation-manifest.mjs +200 -0
- package/packages/investigation/investigation-package-crypto.mjs +98 -0
- package/packages/investigation/investigation-package.mjs +200 -0
- package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
- package/packages/investigation/lookup-asset-graph.mjs +193 -165
- package/packages/investigation/lookup-claim-readiness.mjs +18 -0
- package/packages/investigation/lookup-investigation-brief.mjs +3 -3
- package/packages/investigation/lookup-task-guidance.mjs +58 -0
- package/packages/monitoring/analyst-review-state.mjs +38 -90
- package/packages/workspace/brand-profile-model.mjs +276 -22
- package/packages/workspace/bulk-columns.mjs +17 -0
- package/packages/workspace/bulk-review-model.mjs +23 -11
- package/packages/workspace/bulk-session-model.mjs +100 -24
- package/packages/workspace/bulk-sort.mjs +1 -0
- package/packages/workspace/case-views.mjs +102 -0
- package/packages/workspace/detection-rule-model.mjs +70 -22
- package/packages/workspace/hostile-input.mjs +29 -8
- package/packages/workspace/investigation-template-model.mjs +19 -4
- package/packages/workspace/page-baseline.mjs +18 -15
- package/packages/workspace/relationship-observation-model.mjs +41 -23
- package/packages/workspace/watchlist-history.mjs +2 -2
- package/packages/workspace/watchlist-store.mjs +8 -2
- package/packages/workspace/website-snapshot-model.mjs +164 -26
- package/packages/workspace/workspace-archive-crypto.mjs +41 -61
- package/packages/workspace/workspace-archive.mjs +87 -21
- package/third-party-notices.txt +29 -2
- package/cli/evidence-command-arguments.mjs +0 -156
- package/packages/investigation/bulk-review-export.mjs +0 -63
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { ACQUISITION_DECISIONS, ACQUISITION_MANUAL_CHECKS, ACQUISITION_DECISION_PACKET_SCHEMA, ACQUISITION_DECISION_PACKET_VERSION, } from "../../packages/investigation/acquisition-decision-packet.mjs";
|
|
2
2
|
import { BULK_DOMAIN_COMPARISON_EXPORT_VERSION, BULK_DOMAIN_COMPARISON_SCHEMA, } from "../../packages/investigation/bulk-domain-comparison.mjs";
|
|
3
3
|
import { BULK_MAIL_EXPOSURE_EXPORT_VERSION, BULK_MAIL_EXPOSURE_SCHEMA, MAX_BULK_MAIL_EXPOSURE_ROWS, } from "../../packages/investigation/bulk-mail-exposure.mjs";
|
|
4
|
-
import { BULK_REVIEW_MANIFEST_SCHEMA, BULK_REVIEW_MANIFEST_VERSION, } from "../../packages/investigation
|
|
4
|
+
import { BULK_REVIEW_MANIFEST_SCHEMA, BULK_REVIEW_MANIFEST_VERSION, SUPPORTED_BULK_REVIEW_MANIFEST_VERSIONS, } from "../../packages/contracts/investigation-portability.mjs";
|
|
5
|
+
import { MAX_BULK_SESSION_ROWS, MAX_BULK_SESSION_SOURCES } from "../../packages/contracts/workspace-portability.mjs";
|
|
6
|
+
import { BULK_SORT_KEYS } from "../../packages/workspace/bulk-sort.mjs";
|
|
7
|
+
import { normalizeExplicitIsoTimestamp } from "../../packages/evidence/observation.mjs";
|
|
5
8
|
import { LOOKUP_CLAIM_PASSPORT_SCHEMA, LOOKUP_CLAIM_PASSPORT_TARGET_TYPES, LOOKUP_CLAIM_PASSPORT_VERSION, MAX_LOOKUP_CLAIM_PASSPORT_LIMITATIONS, MAX_LOOKUP_CLAIM_PASSPORT_REQUIREMENTS, } from "../../packages/investigation/lookup-claim-passport.mjs";
|
|
6
9
|
import { LOOKUP_CLAIM_IDS, LOOKUP_CLAIM_READINESS_VERSION, LOOKUP_CLAIM_REQUIREMENT_IDS, } from "../../packages/investigation/lookup-claim-readiness.mjs";
|
|
7
10
|
import { normalizeDomainControlManifestDocument } from "../../packages/evidence/domain-control-runtime.mjs";
|
|
8
|
-
import { DOMAIN_CHANGE_PACKET_SCHEMA,
|
|
11
|
+
import { DOMAIN_CHANGE_PACKET_SCHEMA, DOMAIN_CHANGE_PACKET_REVIEW_VERSIONS, } from "../../lib/domain-change-packet.mjs";
|
|
12
|
+
import { DOMAIN_CHANGE_REVIEW_SCHEMA } from "../../lib/domain-change-review.mjs";
|
|
9
13
|
import { DOMAIN_CONTROL_MANIFEST_SCHEMA } from "../../packages/contracts/domain-control-manifest.mjs";
|
|
10
|
-
import { INVESTIGATION_MANIFEST_SCHEMA,
|
|
11
|
-
import { array, boolean, domain,
|
|
14
|
+
import { INVESTIGATION_MANIFEST_SCHEMA, validateInvestigationManifest, } from "../investigation-manifest.mjs";
|
|
15
|
+
import { array, boolean, domain, enumeration, exact, fail, integer, iso, optionalText, sameValues, strings, text, validateIntegrity, } from "./structure-primitives.mjs";
|
|
12
16
|
const CLAIM_PASSPORT_SOURCE_STATES = ['complete', 'not_found', 'partial', 'skipped', 'unavailable', 'unknown', 'unsupported'];
|
|
13
17
|
const COMPARISON_STATES = ['conflicting', 'different', 'equal', 'missing', 'not_recorded', 'unavailable'];
|
|
14
18
|
const SOURCE_STATES = ['complete', 'error', 'not_found', 'not_recorded', 'partial', 'skipped', 'unavailable', 'unsupported'];
|
|
@@ -209,11 +213,17 @@ function validateDomainComparison(value) {
|
|
|
209
213
|
strings(comparison.limitations, 'Domain comparison limitations', 12, 600);
|
|
210
214
|
validateIntegrity(root.integrity, 'Domain comparison integrity', root.version, BULK_DOMAIN_COMPARISON_EXPORT_VERSION);
|
|
211
215
|
}
|
|
212
|
-
function
|
|
213
|
-
|
|
216
|
+
function validateNullableObservationTime(value, label) {
|
|
217
|
+
if (value !== null && normalizeExplicitIsoTimestamp(value) !== value)
|
|
218
|
+
fail(label);
|
|
219
|
+
}
|
|
220
|
+
function validateSourceCoverage(value, label, observedTime = false) {
|
|
221
|
+
const source = exact(value, ['source', 'state', ...(observedTime ? ['observedAt'] : [])], label);
|
|
214
222
|
if (typeof source.source !== 'string' || !/^[a-z][a-z0-9_-]{0,39}$/u.test(source.source))
|
|
215
223
|
fail(label);
|
|
216
224
|
enumeration(source.state, SOURCE_STATES.filter((state) => state !== 'not_recorded'), label);
|
|
225
|
+
if (observedTime)
|
|
226
|
+
validateNullableObservationTime(source.observedAt, `${label} observedAt`);
|
|
217
227
|
}
|
|
218
228
|
function validateProfileContext(value, label) {
|
|
219
229
|
const context = exact(value, ['sourceState', 'activeProfileId', 'profileUpdatedAt', 'limitation'], label);
|
|
@@ -281,29 +291,40 @@ function validateBulkView(value, label) {
|
|
|
281
291
|
text(view.caseDispositionFilter, label, 60, true);
|
|
282
292
|
enumeration(view.reviewStateFilter, ['', 'unreviewed', 'reviewing', 'reviewed', 'deferred'], label);
|
|
283
293
|
text(view.groupBy, label, 60, true);
|
|
284
|
-
enumeration(view.sortKey,
|
|
294
|
+
enumeration(view.sortKey, BULK_SORT_KEYS, label);
|
|
285
295
|
if (view.sortDirection !== 1 && view.sortDirection !== -1)
|
|
286
296
|
fail(label);
|
|
287
297
|
}
|
|
288
298
|
function validateBulkReviewManifest(value) {
|
|
289
299
|
const root = exact(value, ['schema', 'version', 'generatedAt', 'observedAt', 'lookupProfile', 'selection', 'view', 'rows', 'limitations', 'integrity'], 'Bulk review manifest');
|
|
300
|
+
const version = SUPPORTED_BULK_REVIEW_MANIFEST_VERSIONS.find((candidate) => candidate === root.version);
|
|
301
|
+
if (version === undefined)
|
|
302
|
+
fail('Bulk review manifest version');
|
|
303
|
+
const sourceTimes = version === BULK_REVIEW_MANIFEST_VERSION;
|
|
304
|
+
const maximumRows = sourceTimes ? MAX_BULK_SESSION_ROWS : 2_000;
|
|
305
|
+
const maximumSources = sourceTimes ? MAX_BULK_SESSION_SOURCES : 12;
|
|
290
306
|
iso(root.generatedAt, 'Bulk review manifest generatedAt');
|
|
291
|
-
|
|
307
|
+
if (sourceTimes)
|
|
308
|
+
validateNullableObservationTime(root.observedAt, 'Bulk review manifest observedAt');
|
|
309
|
+
else
|
|
310
|
+
iso(root.observedAt, 'Bulk review manifest observedAt');
|
|
292
311
|
enumeration(root.lookupProfile, ['deep', 'fast'], 'Bulk review manifest lookup profile');
|
|
293
312
|
const selection = exact(root.selection, ['count', 'domains'], 'Bulk review manifest selection');
|
|
294
|
-
const domains = strings(selection.domains, 'Bulk review manifest selected domains',
|
|
313
|
+
const domains = strings(selection.domains, 'Bulk review manifest selected domains', maximumRows, 253);
|
|
295
314
|
domains.forEach((item) => domain(item, 'Bulk review manifest selected domain'));
|
|
296
|
-
if (integer(selection.count, 'Bulk review manifest selection count', 0,
|
|
315
|
+
if (integer(selection.count, 'Bulk review manifest selection count', 0, maximumRows) !== domains.length)
|
|
297
316
|
fail('Bulk review manifest selection');
|
|
298
317
|
validateBulkView(root.view, 'Bulk review manifest view');
|
|
299
|
-
const rows = array(root.rows, 'Bulk review manifest rows',
|
|
318
|
+
const rows = array(root.rows, 'Bulk review manifest rows', maximumRows);
|
|
300
319
|
for (const [index, candidate] of rows.entries()) {
|
|
301
|
-
const row = exact(candidate, ['domain', 'reviewState', 'resultState', 'scanDepth', 'sourceCoverage', 'profileContext'], `Bulk review manifest row ${index + 1}`);
|
|
320
|
+
const row = exact(candidate, ['domain', 'reviewState', 'resultState', 'scanDepth', ...(sourceTimes ? ['observedAt'] : []), 'sourceCoverage', 'profileContext'], `Bulk review manifest row ${index + 1}`);
|
|
302
321
|
domain(row.domain, 'Bulk review manifest row domain');
|
|
303
322
|
enumeration(row.reviewState, ['unreviewed', 'reviewing', 'reviewed', 'deferred'], 'Bulk review manifest review state');
|
|
304
323
|
enumeration(row.resultState, ['complete', 'error'], 'Bulk review manifest result state');
|
|
305
324
|
enumeration(row.scanDepth, ['deep', 'fast'], 'Bulk review manifest scan depth');
|
|
306
|
-
|
|
325
|
+
if (sourceTimes)
|
|
326
|
+
validateNullableObservationTime(row.observedAt, 'Bulk review manifest row observedAt');
|
|
327
|
+
array(row.sourceCoverage, 'Bulk review manifest source coverage', maximumSources).forEach((item, sourceIndex) => validateSourceCoverage(item, `Bulk review manifest source ${sourceIndex + 1}`, sourceTimes));
|
|
307
328
|
validateProfileContext(row.profileContext, 'Bulk review manifest profile context');
|
|
308
329
|
if (domains[index] !== row.domain)
|
|
309
330
|
fail('Bulk review manifest selection');
|
|
@@ -311,59 +332,49 @@ function validateBulkReviewManifest(value) {
|
|
|
311
332
|
if (rows.length !== domains.length)
|
|
312
333
|
fail('Bulk review manifest selection');
|
|
313
334
|
strings(root.limitations, 'Bulk review manifest limitations', 8, 600);
|
|
314
|
-
validateIntegrity(root.integrity, 'Bulk review manifest integrity', root.version,
|
|
335
|
+
validateIntegrity(root.integrity, 'Bulk review manifest integrity', root.version, version);
|
|
315
336
|
}
|
|
316
|
-
function
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
let totalBytes = 0;
|
|
330
|
-
for (const [index, candidate] of artifacts.entries()) {
|
|
331
|
-
const item = exact(candidate, ['sequence', 'id', 'schema', 'version', 'byteLength', 'contentDigestSha256', 'canonicalDigestSha256'], `Investigation manifest artifact ${index + 1}`);
|
|
332
|
-
if (integer(item.sequence, 'Investigation manifest artifact sequence', 1, artifacts.length) !== index + 1
|
|
333
|
-
|| item.id !== `artifact-${index + 1}`)
|
|
334
|
-
fail('Investigation manifest artifact order');
|
|
335
|
-
if (item.schema !== null)
|
|
336
|
-
text(item.schema, 'Investigation manifest artifact schema', 160);
|
|
337
|
-
if (item.version !== null)
|
|
338
|
-
integer(item.version, 'Investigation manifest artifact version', 1, 1_000);
|
|
339
|
-
totalBytes += integer(item.byteLength, 'Investigation manifest artifact bytes', 1, 15 * 1024 * 1024);
|
|
340
|
-
digest(item.contentDigestSha256, 'Investigation manifest content digest');
|
|
341
|
-
digest(item.canonicalDigestSha256, 'Investigation manifest canonical digest');
|
|
342
|
-
const step = exact(steps[index], ['sequence', 'artifactId', 'contentDigestSha256'], `Investigation manifest step ${index + 1}`);
|
|
343
|
-
if (step.sequence !== item.sequence || step.artifactId !== item.id || step.contentDigestSha256 !== item.contentDigestSha256)
|
|
344
|
-
fail('Investigation manifest step linkage');
|
|
345
|
-
}
|
|
346
|
-
const summary = exact(root.summary, ['artifactCount', 'totalBytes'], 'Investigation manifest summary');
|
|
347
|
-
if (integer(summary.artifactCount, 'Investigation manifest artifact count', 1, MAX_INVESTIGATION_MANIFEST_ARTIFACTS) !== artifacts.length
|
|
348
|
-
|| integer(summary.totalBytes, 'Investigation manifest total bytes', 1, MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES) !== totalBytes)
|
|
349
|
-
fail('Investigation manifest summary');
|
|
350
|
-
strings(root.limitations, 'Investigation manifest limitations', 8, 600);
|
|
351
|
-
validateIntegrity(root.integrity, 'Investigation manifest integrity', root.version, INVESTIGATION_MANIFEST_VERSION);
|
|
337
|
+
function validateSourceObservations(value, label) {
|
|
338
|
+
const names = new Set();
|
|
339
|
+
return array(value, label, 16).map((candidate) => {
|
|
340
|
+
const source = exact(candidate, ['label', 'source', 'state', 'observedAt'], label);
|
|
341
|
+
const name = text(source.label, `${label} label`, 120).toLowerCase();
|
|
342
|
+
if (names.has(name))
|
|
343
|
+
fail(`${label} duplicate label`);
|
|
344
|
+
names.add(name);
|
|
345
|
+
text(source.source, `${label} source`, 240);
|
|
346
|
+
enumeration(source.state, ['observed', 'partial', 'unavailable'], `${label} state`);
|
|
347
|
+
iso(source.observedAt, `${label} observedAt`);
|
|
348
|
+
return source;
|
|
349
|
+
});
|
|
352
350
|
}
|
|
353
|
-
function validateReviewMatrix(value, label) {
|
|
351
|
+
function validateReviewMatrix(value, label, sources) {
|
|
354
352
|
for (const [rowIndex, candidate] of array(value, label, 500).entries()) {
|
|
355
353
|
const row = exact(candidate, ['owner', 'type', 'state', 'observations'], `${label} row ${rowIndex + 1}`);
|
|
356
354
|
text(row.owner, `${label} owner`, 253);
|
|
357
355
|
enumeration(row.type, DNS_TYPES, `${label} type`);
|
|
358
356
|
const observations = array(row.observations, `${label} observations`, 16);
|
|
357
|
+
if (sources && observations.length !== sources.length)
|
|
358
|
+
fail(`${label} source coverage`);
|
|
359
359
|
let complete = 0;
|
|
360
360
|
const signatures = new Set();
|
|
361
|
+
const names = new Set();
|
|
361
362
|
for (const observationCandidate of observations) {
|
|
362
|
-
const observation = exact(observationCandidate, ['label', 'source', 'state', 'values', 'ttlRange'], `${label} observation`);
|
|
363
|
+
const observation = exact(observationCandidate, ['label', 'source', 'state', 'values', 'ttlRange', ...(sources ? ['observedAt'] : [])], `${label} observation`);
|
|
363
364
|
text(observation.label, `${label} observation label`, 120);
|
|
364
365
|
text(observation.source, `${label} observation source`, 240);
|
|
365
366
|
const state = enumeration(observation.state, ['observed', 'partial', 'unavailable'], `${label} observation state`);
|
|
366
367
|
const values = strings(observation.values, `${label} observation values`, 500, 16_384);
|
|
368
|
+
if (sources) {
|
|
369
|
+
const name = String(observation.label).toLowerCase();
|
|
370
|
+
if (names.has(name))
|
|
371
|
+
fail(`${label} duplicate observation`);
|
|
372
|
+
names.add(name);
|
|
373
|
+
const source = sources.find((item) => item.label === observation.label);
|
|
374
|
+
if (!source || ['label', 'source', 'state', 'observedAt'].some((key) => observation[key] !== source[key]))
|
|
375
|
+
fail(`${label} source provenance`);
|
|
376
|
+
iso(observation.observedAt, `${label} observation time`);
|
|
377
|
+
}
|
|
367
378
|
if (state === 'observed') {
|
|
368
379
|
complete += 1;
|
|
369
380
|
signatures.add(JSON.stringify(values));
|
|
@@ -381,15 +392,17 @@ function validateReviewMatrix(value, label) {
|
|
|
381
392
|
fail(`${label} state`);
|
|
382
393
|
}
|
|
383
394
|
}
|
|
384
|
-
function validateDomainChangeReview(value, label) {
|
|
385
|
-
const
|
|
386
|
-
|
|
395
|
+
function validateDomainChangeReview(value, label, version) {
|
|
396
|
+
const retainedTimes = version >= 2;
|
|
397
|
+
const review = exact(value, ['schema', 'version', 'generatedAt', 'domain', 'state', 'authoritativeRecordMatrix', 'resolverDivergenceMatrix', 'dnssecAutomation', 'acmeDependencies', 'certificate', 'services', 'hsts', 'gate', 'limitations', ...(retainedTimes ? ['sourceObservations'] : [])], label);
|
|
398
|
+
if (review.schema !== DOMAIN_CHANGE_REVIEW_SCHEMA || review.version !== version)
|
|
387
399
|
fail(label);
|
|
388
400
|
iso(review.generatedAt, `${label} generatedAt`);
|
|
389
401
|
domain(review.domain, `${label} domain`);
|
|
390
402
|
enumeration(review.state, ['ready', 'review'], `${label} state`);
|
|
391
|
-
|
|
392
|
-
validateReviewMatrix(review.
|
|
403
|
+
const sources = retainedTimes ? exact(review.sourceObservations, ['authorities', 'resolvers'], `${label} source observations`) : null;
|
|
404
|
+
validateReviewMatrix(review.authoritativeRecordMatrix, `${label} authority matrix`, sources ? validateSourceObservations(sources.authorities, `${label} authority sources`) : undefined);
|
|
405
|
+
validateReviewMatrix(review.resolverDivergenceMatrix, `${label} resolver matrix`, sources ? validateSourceObservations(sources.resolvers, `${label} resolver sources`) : undefined);
|
|
393
406
|
const automation = exact(review.dnssecAutomation, ['state', 'cdsObserved', 'cdnskeyObserved', 'csyncObserved', 'conflictingTypes', 'detail'], `${label} DNSSEC automation`);
|
|
394
407
|
enumeration(automation.state, ['not_observed', 'conflict', 'partial', 'review_ready'], `${label} DNSSEC automation`);
|
|
395
408
|
boolean(automation.cdsObserved, `${label} CDS observed`);
|
|
@@ -405,8 +418,13 @@ function validateDomainChangeReview(value, label) {
|
|
|
405
418
|
optionalText(dependency.provider, `${label} ACME provider`, 120);
|
|
406
419
|
enumeration(dependency.state, ['confirmed', 'partial', 'unknown'], `${label} ACME state`);
|
|
407
420
|
}
|
|
408
|
-
const certificate = exact(review.certificate, ['state', 'continuity', 'findings'], `${label} certificate`);
|
|
421
|
+
const certificate = exact(review.certificate, ['state', 'continuity', 'findings', ...(retainedTimes ? ['observedAt'] : [])], `${label} certificate`);
|
|
409
422
|
enumeration(certificate.state, ['not_supplied', 'observed', 'partial', 'unavailable'], `${label} certificate state`);
|
|
423
|
+
if (retainedTimes) {
|
|
424
|
+
iso(certificate.observedAt, `${label} certificate observedAt`, true);
|
|
425
|
+
if ((certificate.state === 'not_supplied') !== (certificate.observedAt === null))
|
|
426
|
+
fail(`${label} certificate observation time`);
|
|
427
|
+
}
|
|
410
428
|
enumeration(certificate.continuity, ['unknown', 'retained', 'changes'], `${label} certificate continuity`);
|
|
411
429
|
strings(certificate.findings, `${label} certificate findings`, 8, 600);
|
|
412
430
|
for (const candidate of array(review.services, `${label} services`, 1_000)) {
|
|
@@ -501,21 +519,43 @@ function expectedDomainChangeSummary(before, after) {
|
|
|
501
519
|
const beforeRows = rows(before);
|
|
502
520
|
const afterRows = rows(after);
|
|
503
521
|
const keys = [...new Set([...beforeRows.keys(), ...afterRows.keys()])].sort();
|
|
504
|
-
|
|
505
|
-
const
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
522
|
+
const collectionComplete = (review) => {
|
|
523
|
+
const row = (review.authoritativeRecordMatrix[0]);
|
|
524
|
+
const observations = row?.observations ?? [];
|
|
525
|
+
return observations.length >= 2 && observations.every((item) => item.state === 'observed');
|
|
526
|
+
};
|
|
527
|
+
const rowEvidence = (review, row) => {
|
|
528
|
+
if (!row)
|
|
529
|
+
return { state: collectionComplete(review) ? 'complete' : 'unavailable', values: [] };
|
|
530
|
+
const observations = row.observations;
|
|
531
|
+
if (observations.some((item) => item.state !== 'observed'))
|
|
532
|
+
return { state: 'partial', values: [] };
|
|
533
|
+
if (row.state === 'different')
|
|
534
|
+
return { state: 'inconsistent', values: [] };
|
|
535
|
+
if (row.state !== 'aligned')
|
|
536
|
+
return { state: 'insufficient', values: [] };
|
|
537
|
+
return { state: 'complete', values: [...new Set(observations.flatMap((item) => item.values))].sort() };
|
|
538
|
+
};
|
|
539
|
+
const changed = [];
|
|
540
|
+
for (const key of keys.slice(0, 500)) {
|
|
541
|
+
const left = rowEvidence(before, beforeRows.get(key));
|
|
542
|
+
const right = rowEvidence(after, afterRows.get(key));
|
|
513
543
|
const [owner, type] = key.split('\u0000');
|
|
514
|
-
|
|
515
|
-
|
|
544
|
+
if (left.state !== 'complete' || right.state !== 'complete') {
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
else if (!sameValues(left.values, right.values)) {
|
|
548
|
+
changed.push({ owner: owner ?? '', type: type ?? '', beforeValues: left.values, afterValues: right.values });
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return changed;
|
|
516
552
|
}
|
|
517
553
|
function validateDomainChangePacket(value) {
|
|
518
554
|
const root = exact(value, ['schema', 'version', 'generatedAt', 'domain', 'reference', 'state', 'gate', 'summary', 'evidence', 'limitations', 'integrity'], 'Domain change packet');
|
|
555
|
+
const version = integer(root.version, 'Domain change packet version', 1, 1_000);
|
|
556
|
+
const reviewVersion = DOMAIN_CHANGE_PACKET_REVIEW_VERSIONS[version];
|
|
557
|
+
if (reviewVersion === undefined)
|
|
558
|
+
fail('Domain change packet version');
|
|
519
559
|
iso(root.generatedAt, 'Domain change packet generatedAt');
|
|
520
560
|
domain(root.domain, 'Domain change packet domain');
|
|
521
561
|
text(root.reference, 'Domain change packet reference', 200);
|
|
@@ -538,8 +578,8 @@ function validateDomainChangePacket(value) {
|
|
|
538
578
|
enumeration(summary.postChangeState, ['ready', 'review'], 'Domain change packet post-change state');
|
|
539
579
|
enumeration(summary.assuranceState, ['incomplete', 'needs_review', 'ready'], 'Domain change packet assurance state');
|
|
540
580
|
const evidence = exact(root.evidence, ['preChange', 'postChange', 'assurance'], 'Domain change packet evidence');
|
|
541
|
-
const pre = validateDomainChangeReview(evidence.preChange, 'Domain change packet pre-change review');
|
|
542
|
-
const post = validateDomainChangeReview(evidence.postChange, 'Domain change packet post-change review');
|
|
581
|
+
const pre = validateDomainChangeReview(evidence.preChange, 'Domain change packet pre-change review', reviewVersion);
|
|
582
|
+
const post = validateDomainChangeReview(evidence.postChange, 'Domain change packet post-change review', reviewVersion);
|
|
543
583
|
const assurance = validatePlannedAssurance(evidence.assurance, 'Domain change packet assurance');
|
|
544
584
|
const assuranceResult = assurance.result;
|
|
545
585
|
if (pre.generatedAt !== root.generatedAt || post.generatedAt !== root.generatedAt || assurance.generatedAt !== root.generatedAt
|
|
@@ -566,7 +606,7 @@ function validateDomainChangePacket(value) {
|
|
|
566
606
|
}))
|
|
567
607
|
fail('Domain change packet summary');
|
|
568
608
|
strings(root.limitations, 'Domain change packet limitations', 8, 600);
|
|
569
|
-
validateIntegrity(root.integrity, 'Domain change packet integrity', root.version,
|
|
609
|
+
validateIntegrity(root.integrity, 'Domain change packet integrity', root.version, version);
|
|
570
610
|
}
|
|
571
611
|
export function validateSignedDigestArtifactStructure(schema, value) {
|
|
572
612
|
if (value.schema !== schema)
|
|
@@ -1,105 +1 @@
|
|
|
1
|
-
|
|
2
|
-
const DIGEST_RE = /^sha256:[a-f0-9]{64}$/u;
|
|
3
|
-
const DOMAIN_RE = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/u;
|
|
4
|
-
export const HEX_DIGEST_RE = /^[a-f0-9]{64}$/u;
|
|
5
|
-
export const SEMANTIC_VERSION_RE = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u;
|
|
6
|
-
export function fail(label) {
|
|
7
|
-
throw new TypeError(`${label} has an unsupported or malformed structure.`);
|
|
8
|
-
}
|
|
9
|
-
export function record(value, label) {
|
|
10
|
-
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
11
|
-
fail(label);
|
|
12
|
-
return value;
|
|
13
|
-
}
|
|
14
|
-
export function exact(value, keys, label) {
|
|
15
|
-
const source = record(value, label);
|
|
16
|
-
const actual = Object.keys(source);
|
|
17
|
-
if (actual.length !== keys.length || actual.some((key) => !keys.includes(key)))
|
|
18
|
-
fail(label);
|
|
19
|
-
return source;
|
|
20
|
-
}
|
|
21
|
-
export function exactOptional(value, required, optional, label) {
|
|
22
|
-
const source = record(value, label);
|
|
23
|
-
const actual = Object.keys(source);
|
|
24
|
-
if (required.some((key) => !actual.includes(key))
|
|
25
|
-
|| actual.some((key) => !required.includes(key) && !optional.includes(key)))
|
|
26
|
-
fail(label);
|
|
27
|
-
return source;
|
|
28
|
-
}
|
|
29
|
-
export function text(value, label, maximum = 2_000, allowEmpty = false) {
|
|
30
|
-
if (typeof value !== 'string' || value.length > maximum || CONTROL_RE.test(value) || (!allowEmpty && !value))
|
|
31
|
-
fail(label);
|
|
32
|
-
return value;
|
|
33
|
-
}
|
|
34
|
-
export function optionalText(value, label, maximum = 2_000) {
|
|
35
|
-
if (value !== null)
|
|
36
|
-
text(value, label, maximum);
|
|
37
|
-
}
|
|
38
|
-
export function iso(value, label, nullable = false) {
|
|
39
|
-
if (nullable && value === null)
|
|
40
|
-
return;
|
|
41
|
-
const candidate = text(value, label, 64);
|
|
42
|
-
const parsed = Date.parse(candidate);
|
|
43
|
-
if (!Number.isFinite(parsed) || new Date(parsed).toISOString() !== candidate)
|
|
44
|
-
fail(label);
|
|
45
|
-
}
|
|
46
|
-
export function integer(value, label, minimum = 0, maximum = Number.MAX_SAFE_INTEGER) {
|
|
47
|
-
if (!Number.isSafeInteger(value) || Number(value) < minimum || Number(value) > maximum)
|
|
48
|
-
fail(label);
|
|
49
|
-
return Number(value);
|
|
50
|
-
}
|
|
51
|
-
export function boolean(value, label) {
|
|
52
|
-
if (typeof value !== 'boolean')
|
|
53
|
-
fail(label);
|
|
54
|
-
}
|
|
55
|
-
export function enumeration(value, allowed, label) {
|
|
56
|
-
if (typeof value !== 'string' || !allowed.includes(value))
|
|
57
|
-
fail(label);
|
|
58
|
-
return value;
|
|
59
|
-
}
|
|
60
|
-
export function array(value, label, maximum, minimum = 0) {
|
|
61
|
-
if (!Array.isArray(value) || value.length < minimum || value.length > maximum)
|
|
62
|
-
fail(label);
|
|
63
|
-
return value;
|
|
64
|
-
}
|
|
65
|
-
export function strings(value, label, maximum, textMaximum = 2_000) {
|
|
66
|
-
const values = array(value, label, maximum);
|
|
67
|
-
for (const item of values)
|
|
68
|
-
text(item, label, textMaximum);
|
|
69
|
-
return values;
|
|
70
|
-
}
|
|
71
|
-
export function digest(value, label) {
|
|
72
|
-
if (typeof value !== 'string' || !DIGEST_RE.test(value))
|
|
73
|
-
fail(label);
|
|
74
|
-
}
|
|
75
|
-
export function sameValues(left, right) {
|
|
76
|
-
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
77
|
-
}
|
|
78
|
-
export function domain(value, label) {
|
|
79
|
-
if (typeof value !== 'string' || !DOMAIN_RE.test(value))
|
|
80
|
-
fail(label);
|
|
81
|
-
}
|
|
82
|
-
export function absoluteUrl(value, label, protocols) {
|
|
83
|
-
const candidate = text(value, label, 2_048);
|
|
84
|
-
try {
|
|
85
|
-
const parsed = new URL(candidate);
|
|
86
|
-
if (!protocols.includes(parsed.protocol) || parsed.username || parsed.password || parsed.search || parsed.hash)
|
|
87
|
-
fail(label);
|
|
88
|
-
return parsed;
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
return fail(label);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
export function nullableRecord(value, label) {
|
|
95
|
-
if (value !== null)
|
|
96
|
-
record(value, label);
|
|
97
|
-
}
|
|
98
|
-
export function validateIntegrity(value, label, version, currentVersion) {
|
|
99
|
-
if (version !== currentVersion)
|
|
100
|
-
fail(label);
|
|
101
|
-
const integrity = exact(value, ['algorithm', 'canonicalization', 'digestSha256'], label);
|
|
102
|
-
if (integrity.algorithm !== 'SHA-256' || integrity.canonicalization !== 'sorted-json-v2')
|
|
103
|
-
fail(label);
|
|
104
|
-
digest(integrity.digestSha256, label);
|
|
105
|
-
}
|
|
1
|
+
export * from "../../packages/evidence/artifact-structure.mjs";
|