@slicedearth/whoisleuth-cli 2.3.0 → 2.5.1
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 +8 -2
- package/README.md +19 -15
- package/bin/whoisleuth.mjs +8 -1
- package/cli/archive-content-digest.mjs +14 -0
- package/cli/archive-inspect.mjs +20 -53
- package/cli/argument-values.mjs +55 -0
- package/cli/arguments.mjs +24 -408
- package/cli/artifact-validation/case-response.mjs +65 -44
- package/cli/artifact-validation/investigation-capsule.mjs +52 -31
- package/cli/artifact-validation/lookup-evidence.mjs +15 -9
- package/cli/artifact-validation/signed-review.mjs +136 -68
- package/cli/artifact-validation/structure-primitives.mjs +1 -105
- package/cli/artifact-verify.mjs +92 -24
- package/cli/assurance-arguments.mjs +56 -0
- package/cli/assurance-command-definitions.mjs +179 -0
- package/cli/assurance-command-runner.mjs +40 -5
- 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 +1 -1
- package/cli/bulk-command-runner.mjs +4 -2
- package/cli/bulk-output.mjs +29 -3
- package/cli/bulk.mjs +2 -3
- package/cli/case-command.mjs +212 -0
- package/cli/case-pack.mjs +50 -32
- package/cli/ci-report.mjs +6 -3
- package/cli/collection-arguments.mjs +123 -0
- package/cli/collection-command-definitions.mjs +330 -0
- package/cli/collection-preflight.mjs +8 -1
- package/cli/command-argument-grammar.mjs +8 -206
- package/cli/command-definition.mjs +329 -0
- package/cli/command-reference.mjs +94 -1357
- package/cli/command-seeds.mjs +18 -0
- package/cli/compare.mjs +1 -1
- package/cli/completion.mjs +298 -79
- package/cli/config-profile.mjs +8 -6
- package/cli/ct-event-intake.mjs +16 -16
- package/cli/discover.mjs +5 -4
- package/cli/discovery-observation-snapshot.mjs +46 -18
- package/cli/discovery-scan-command-runner.mjs +5 -3
- package/cli/discovery-scan.mjs +7 -4
- package/cli/discovery-snapshot.mjs +2 -2
- package/cli/discovery-workflow.mjs +5 -5
- package/cli/domain-control-monitor.mjs +50 -20
- package/cli/domain-control-observations.mjs +151 -103
- package/cli/errors.mjs +37 -6
- package/cli/evidence-arguments.mjs +46 -0
- package/cli/evidence-command-definitions.mjs +69 -0
- package/cli/evidence-command-runner.mjs +18 -8
- package/cli/evidence-signing.mjs +5 -4
- package/cli/external-observation-mapping.mjs +9 -10
- package/cli/formatters/terminal-command-formats.mjs +6 -0
- package/cli/formatters/terminal-lookup.mjs +24 -46
- package/cli/formatters/terminal-shared.mjs +5 -3
- package/cli/formatters/terminal-technology.mjs +114 -0
- package/cli/history-arguments.mjs +39 -0
- package/cli/history-command-definitions.mjs +88 -0
- package/cli/indicator-set-command.mjs +33 -0
- package/cli/inline-command-families.mjs +6 -46
- package/cli/inline-command-runner.mjs +4 -31
- package/cli/input.mjs +2 -2
- package/cli/intake-command.mjs +107 -0
- package/cli/interactive-launcher.mjs +2 -160
- package/cli/interchange-report.mjs +8 -10
- 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 +38 -147
- package/cli/investigation-recipes.mjs +177 -0
- package/cli/investigation-run.mjs +256 -35
- package/cli/local-document-checkpoint.mjs +170 -0
- package/cli/local-mmdb-review.mjs +197 -17
- package/cli/lookalike-calibration.mjs +1 -1
- package/cli/lookup-brief.mjs +2 -2
- package/cli/lookup-browser.mjs +5 -12
- package/cli/lookup-command-runner.mjs +33 -39
- package/cli/lookup-diff.mjs +47 -12
- package/cli/lookup-plan.mjs +16 -14
- package/cli/lookup-reconcile.mjs +15 -12
- package/cli/lookup-timeline.mjs +7 -6
- package/cli/mail-header-review.mjs +24 -247
- package/cli/mail-review.mjs +75 -21
- package/cli/manual.mjs +1 -0
- package/cli/network-arguments.mjs +51 -0
- package/cli/network-command-definitions.mjs +130 -0
- package/cli/network-command-runner.mjs +45 -31
- package/cli/offline-evidence-review.mjs +39 -6
- package/cli/output-file.mjs +31 -5
- package/cli/posture.mjs +1 -1
- package/cli/registry-doctor.mjs +3 -1
- package/cli/registry-fixture-scaffold.mjs +1 -1
- package/cli/registry-support.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +10 -9
- package/cli/review-arguments.mjs +131 -0
- package/cli/review-command-definitions.mjs +273 -0
- package/cli/review-command-runner.mjs +104 -149
- package/cli/risk-calibration.mjs +8 -8
- package/cli/runner.mjs +74 -37
- package/cli/saved-lookup.mjs +2 -2
- package/cli/selected-input-worker.mjs +63 -0
- package/cli/sharing-review.mjs +8 -80
- package/cli/signer-trust.mjs +104 -0
- package/cli/source-reliability.mjs +75 -84
- package/cli/support-arguments.mjs +49 -0
- package/cli/support-command-definitions.mjs +209 -0
- package/cli/support-command-runner.mjs +2 -2
- package/cli/terminal-input.mjs +168 -0
- package/cli/workflow-arguments.mjs +86 -0
- package/cli/workflow-command-definitions.mjs +78 -0
- package/cli/workflow-command-runner.mjs +75 -13
- package/docs/cli-reference.md +103 -30
- package/docs/cli.md +510 -25
- package/docs/contextual-reviews.md +155 -0
- package/lib/abort.mjs +4 -1
- package/lib/analyst-taxonomy.mjs +2 -40
- package/lib/application-version.mjs +1 -1
- package/lib/availability.mjs +27 -754
- package/lib/bounded-contract-normalizers.mjs +4 -0
- package/lib/bounded-file.mjs +3 -1
- package/lib/bounded-html-document.mjs +125 -0
- package/lib/bounded-json.mjs +2 -290
- package/lib/browser-library-profile.mjs +99 -130
- package/lib/browser-library-worker.mjs +112 -0
- package/lib/candidate-provenance-bounds.mjs +2 -0
- package/lib/classify.mjs +1 -1
- package/lib/client-behavior-profile.mjs +3 -3
- package/lib/ct-query.mjs +2 -27
- package/lib/ct-response-bounds.mjs +2 -9
- package/lib/ct-search.mjs +89 -46
- package/lib/dmarc-discovery.mjs +62 -0
- package/lib/dns-convergence-review.mjs +21 -8
- package/lib/dns-delegation-health.mjs +36 -22
- package/lib/dns-inheritance-review.mjs +207 -0
- package/lib/dns-intelligence.mjs +44 -29
- package/lib/dns-mx.mjs +1 -1
- package/lib/dns-query-coverage.mjs +40 -0
- package/lib/dnssec-evidence-validation.mjs +11 -3
- package/lib/domain-assurance.mjs +1 -1
- package/lib/domain-change-packet.mjs +18 -14
- package/lib/domain-change-review.mjs +40 -16
- package/lib/domain-control-flight-recorder.mjs +301 -124
- package/lib/domain-control-manifest.mjs +126 -58
- package/lib/domain-posture-analysis.mjs +41 -11
- package/lib/domain-posture-parsers.mjs +88 -6
- package/lib/domain-posture.mjs +120 -32
- package/lib/domain-registration.mjs +392 -0
- package/lib/evidence-export-privacy.mjs +568 -0
- package/lib/evidence-export.mjs +12 -593
- package/lib/evidence-report.mjs +7 -2
- package/lib/external-intelligence-risk.mjs +2 -124
- package/lib/favicon.mjs +117 -41
- package/lib/generated/cisa-kev-catalog.mjs +50 -4
- package/lib/generated/registrar-standing-catalogue.mjs +18 -10
- package/lib/generated/retire-browser-catalog.mjs +2742 -1034
- package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
- package/lib/html-signals.mjs +87 -85
- package/lib/http-evidence-bounds.mjs +2 -7
- package/lib/http-intelligence.mjs +52 -27
- package/lib/idn-confusable-policy.mjs +6 -4
- package/lib/idn-confusables.mjs +1 -1
- package/lib/interchange-fidelity-registry.mjs +10 -0
- package/lib/lookup-cache.mjs +34 -11
- package/lib/lookup-child-profile-contract.mjs +24 -15
- package/lib/lookup-contract-primitives.mjs +1 -1
- package/lib/lookup-diagnostics.mjs +2 -1
- package/lib/lookup-network-evidence-bounds.mjs +1 -10
- package/lib/lookup-progress.mjs +79 -13
- package/lib/lookup-response-contract.mjs +1077 -0
- package/lib/lookup-response.mjs +11 -24
- package/lib/lookup-source-progress.mjs +24 -35
- package/lib/lookup.mjs +49 -28
- package/lib/observed-network-context.mjs +43 -27
- package/lib/opportunity-scoring.mjs +2 -107
- package/lib/outbound-identity.mjs +1 -1
- package/lib/outbound-request-bounds.mjs +4 -1
- package/lib/page-fingerprints.mjs +87 -134
- package/lib/page-role-profile.mjs +12 -8
- package/lib/perceptual-hash-comparison.mjs +2 -34
- package/lib/perceptual-hash.mjs +79 -24
- package/lib/portable-generator.mjs +2 -29
- package/lib/project-metadata.mjs +2 -8
- package/lib/rdap-bootstrap.mjs +72 -22
- package/lib/rdap-client.mjs +26 -14
- 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 +2 -36
- package/lib/registrar-standing-contract.mjs +2 -1
- package/lib/registrar-standing.mjs +9 -13
- package/lib/registry-capabilities.mjs +7 -3
- package/lib/registry-capability-catalogue.mjs +29 -20
- package/lib/registry-dates.mjs +2 -190
- package/lib/registry-insights.mjs +56 -18
- package/lib/response-policy.mjs +86 -48
- package/lib/risk-calibration-summary.mjs +2 -338
- package/lib/risk-scoring.mjs +2 -345
- package/lib/rpki-evidence.mjs +8 -5
- package/lib/safe-fetch.mjs +54 -47
- package/lib/scoring-evidence-quality.mjs +2 -107
- package/lib/security-txt.mjs +10 -7
- package/lib/semantic-version.mjs +2 -54
- package/lib/service-binding-dns.mjs +35 -0
- package/lib/smtp-transport-review.mjs +75 -34
- package/lib/sslbl-certificates.generated.mjs +141 -138
- package/lib/static-html-analysis.mjs +117 -146
- package/lib/structured-data-identity.mjs +2 -1
- 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 +2 -36
- package/lib/typosquat-generator.mjs +29 -16
- package/lib/urlhaus-intelligence.mjs +8 -3
- package/lib/urlscan-intelligence.mjs +1 -1
- package/lib/website-enrichment.mjs +388 -0
- package/lib/website-security-posture.mjs +30 -6
- package/lib/website-technology.mjs +111 -71
- package/lib/whois-authority.mjs +55 -5
- 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-normalization.mjs +3 -1
- package/lib/whois-parser-support.mjs +8 -1
- package/lib/whois-parser.mjs +5 -3
- package/lib/whois-transport.mjs +23 -7
- package/package.json +14 -14
- package/packages/analysis/analyst-taxonomy.mjs +40 -0
- package/packages/analysis/bounded-json.mjs +330 -0
- package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
- package/packages/analysis/cli-argument-grammar.mjs +216 -0
- package/packages/analysis/ct-query.mjs +27 -0
- package/packages/analysis/ct-response-bounds.mjs +9 -0
- package/packages/analysis/external-intelligence-risk.mjs +124 -0
- package/packages/analysis/http-evidence-bounds.mjs +8 -0
- package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
- package/packages/analysis/opportunity-scoring.mjs +107 -0
- package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
- package/packages/analysis/portable-generator.mjs +29 -0
- package/packages/analysis/project-metadata.mjs +8 -0
- package/packages/analysis/registrable-domain.mjs +56 -0
- package/packages/analysis/risk-calibration-summary.mjs +352 -0
- package/packages/analysis/risk-scoring.mjs +345 -0
- package/packages/analysis/scoring-evidence-quality.mjs +107 -0
- package/packages/analysis/semantic-version.mjs +54 -0
- package/packages/analysis/threat-intelligence-types.mjs +75 -0
- package/packages/cases/case-attachment-model.mjs +83 -0
- package/packages/cases/case-evidence-links.mjs +135 -0
- package/packages/cases/case-evidence-model.mjs +229 -69
- package/packages/cases/case-evidence-quality.mjs +5 -0
- package/packages/cases/case-export-input.mjs +55 -0
- package/packages/cases/case-incident-context.mjs +52 -0
- package/packages/cases/case-markdown.mjs +11 -0
- package/packages/cases/case-migration-model.mjs +98 -48
- package/packages/cases/case-model.mjs +1 -0
- package/packages/cases/case-recheck-model.mjs +149 -0
- package/packages/cases/case-record-contracts.mjs +1 -1
- package/packages/cases/case-record-core.mjs +4 -3
- package/packages/cases/case-record-operations.mjs +123 -133
- package/packages/cases/case-record-projection.mjs +30 -20
- package/packages/cases/case-record-version-input.mjs +19 -0
- package/packages/cases/case-report.mjs +114 -67
- package/packages/cases/case-requested-evidence.mjs +146 -0
- package/packages/cases/case-response-actions.mjs +756 -0
- package/packages/cases/case-response-model.mjs +49 -1129
- package/packages/cases/case-response-outcomes.mjs +338 -0
- package/packages/cases/case-response-packet-render.mjs +157 -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 +58 -593
- package/packages/cases/case-response-records.mjs +93 -0
- package/packages/cases/case-response-review-inputs.mjs +382 -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 +26 -1
- package/packages/cases/case-workflow-metadata.mjs +225 -0
- package/packages/cases/case-workflow-migration.mjs +43 -0
- package/packages/cases/http-summary.mjs +6 -1
- package/packages/cases/response-route-freshness.mjs +16 -0
- package/packages/comparison/capture-context.mjs +61 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
- package/packages/comparison/favicon-similarity.mjs +1 -1
- package/packages/comparison/page-similarity.mjs +3 -2
- package/packages/comparison/relationship-evidence.mjs +103 -14
- package/packages/comparison/relationship-provenance.mjs +99 -0
- package/packages/contracts/analyst-interchange.mjs +234 -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 +370 -114
- package/packages/contracts/case-views-contract.mjs +12 -0
- package/packages/contracts/case-views.mjs +26 -0
- package/packages/contracts/cli-command-semantics.mjs +3 -0
- package/packages/contracts/cli-grammar.mjs +6 -0
- package/packages/contracts/cli-lookup-brief.mjs +2 -0
- package/packages/contracts/cli-lookup.mjs +0 -1
- package/packages/contracts/context-review.mjs +24 -0
- package/packages/contracts/document-intake.mjs +11 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +98 -44
- package/packages/contracts/domain-control-manifest.mjs +128 -74
- package/packages/contracts/domain-control-monitor.mjs +59 -45
- package/packages/contracts/domain-control-review.mjs +200 -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/har-review.mjs +2 -0
- package/packages/contracts/http-url.mjs +2 -0
- package/packages/contracts/identity-events.mjs +4 -0
- package/packages/contracts/investigation-package-limits.mjs +24 -0
- package/packages/contracts/investigation-portability.mjs +56 -27
- package/packages/contracts/investigation-run.mjs +18 -0
- package/packages/contracts/ip-address.mjs +44 -0
- package/packages/contracts/lookup-evidence.mjs +35 -0
- package/packages/contracts/mail-authentication.mjs +5 -0
- package/packages/contracts/message-intake.mjs +31 -0
- package/packages/contracts/offline-comparison.mjs +4 -2
- package/packages/contracts/page-fingerprints.mjs +15 -0
- package/packages/contracts/passive-mail.mjs +10 -0
- package/packages/contracts/risk-calibration.mjs +0 -1
- 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 +219 -238
- package/packages/evidence/artifact-integrity.mjs +8 -2
- package/packages/evidence/artifact-structure.mjs +105 -0
- package/packages/evidence/collection-quality.mjs +67 -0
- package/packages/evidence/decision-fact.mjs +4 -1
- package/packages/evidence/dns-query-outcome.mjs +72 -0
- package/packages/evidence/domain-control-runtime.mjs +155 -27
- package/packages/evidence/domain-posture-context.mjs +79 -0
- package/packages/evidence/email-recipient.mjs +18 -0
- package/packages/evidence/image-regions.mjs +56 -0
- package/packages/evidence/latest-observations.mjs +28 -0
- package/packages/evidence/lookup-target.mjs +108 -0
- package/packages/evidence/observation.mjs +18 -1
- package/packages/evidence/passphrase-encryption.mjs +44 -0
- package/packages/evidence/png-pixels.mjs +117 -0
- package/packages/evidence/public-address-policy.mjs +83 -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/defensive-indicator-export.mjs +264 -0
- package/packages/interchange/external-findings-import.mjs +93 -61
- package/packages/interchange/managed-indicator-export.mjs +35 -0
- package/packages/interchange/managed-indicator-set.mjs +210 -0
- package/packages/interchange/misp-indicator-export.mjs +93 -0
- package/packages/interchange/sharing-policy.mjs +56 -0
- package/packages/interchange/stix-indicator-export.mjs +129 -0
- package/packages/interchange/web-capture-import.mjs +389 -0
- package/packages/investigation/acquisition-decision-packet.mjs +1 -1
- package/packages/investigation/bulk-mail-exposure.mjs +9 -14
- package/packages/investigation/capture-coverage.mjs +57 -0
- package/packages/investigation/connector-provenance-review.mjs +100 -0
- package/packages/investigation/context-review.mjs +35 -0
- package/packages/investigation/document-intake.mjs +81 -0
- package/packages/investigation/docx-intake.mjs +133 -0
- package/packages/investigation/domain-history-review.mjs +100 -0
- package/packages/investigation/domain-transition-review.mjs +47 -0
- package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
- package/packages/investigation/har-intake.mjs +68 -0
- package/packages/investigation/identity-event-intake.mjs +131 -0
- package/packages/investigation/identity-incident-review.mjs +39 -0
- package/packages/investigation/incident-sequence-review.mjs +58 -0
- package/packages/investigation/intake-report.mjs +19 -0
- 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/link-intake.mjs +179 -0
- package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
- package/packages/investigation/lookup-asset-graph.mjs +161 -141
- 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/investigation/mail-authentication-review.mjs +222 -0
- package/packages/investigation/mail-header-identity.mjs +209 -0
- package/packages/investigation/message-intake.mjs +195 -0
- package/packages/investigation/page-behaviour.mjs +113 -0
- package/packages/investigation/pdf-intake.mjs +155 -0
- package/packages/investigation/platform-continuity-review.mjs +60 -0
- package/packages/investigation/qr-intake.mjs +56 -0
- package/packages/investigation/requested-action-hints.mjs +11 -0
- package/packages/investigation/selected-input-review.mjs +17 -0
- package/packages/investigation/storefront-review.mjs +70 -0
- package/packages/monitoring/analyst-review-state.mjs +38 -90
- package/packages/workspace/brand-profile-model.mjs +160 -22
- package/packages/workspace/bulk-columns.mjs +17 -0
- package/packages/workspace/bulk-review-model.mjs +39 -31
- package/packages/workspace/bulk-session-model.mjs +112 -27
- package/packages/workspace/bulk-sort.mjs +1 -0
- package/packages/workspace/campaign-model.mjs +1 -1
- package/packages/workspace/case-views.mjs +102 -0
- package/packages/workspace/detection-rule-model.mjs +1 -1
- package/packages/workspace/hostile-input.mjs +29 -8
- package/packages/workspace/investigation-guide.mjs +2 -2
- package/packages/workspace/investigation-template-model.mjs +45 -6
- package/packages/workspace/page-baseline.mjs +20 -17
- package/packages/workspace/relationship-observation-model.mjs +54 -35
- package/packages/workspace/shortlist-model.mjs +14 -5
- package/packages/workspace/watchlist-history.mjs +30 -14
- package/packages/workspace/watchlist-store.mjs +24 -6
- package/packages/workspace/website-snapshot-model.mjs +98 -33
- package/packages/workspace/workspace-archive-crypto.mjs +41 -61
- package/packages/workspace/workspace-archive.mjs +90 -25
- package/third-party-notices.txt +577 -17
- package/lib/hostname.mjs +0 -38
- package/packages/investigation/bulk-review-export.mjs +0 -63
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { normalizeDomain } from "../evidence/domain-name.mjs";
|
|
2
|
+
import { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, MAX_EXTERNAL_FINDING_DOMAINS, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, MAX_EXTERNAL_FINDING_SUMMARY_LENGTH, parseExternalFindingsDocument, retainExternalFindingLimitations, } from "./external-findings-import.mjs";
|
|
3
|
+
import { MAX_WEB_CAPTURE_DOM_DIGEST_BYTES, MAX_WEB_CAPTURE_SCREENSHOT_BYTES, WEB_CAPTURE_MANIFEST_SCHEMA, WEB_CAPTURE_MANIFEST_VERSION, WEB_CAPTURE_MANIFEST_SUPPORTED_VERSIONS, WEB_CAPTURE_SUMMARY_SCHEMA, WEB_CAPTURE_SUMMARY_VERSION, } from "../contracts/web-capture.mjs";
|
|
4
|
+
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
5
|
+
import { readManifestPageBehaviour } from "../investigation/page-behaviour.mjs";
|
|
6
|
+
import { readCaptureConditions, readObservationLabel } from "../comparison/capture-context.mjs";
|
|
7
|
+
export { WEB_CAPTURE_MANIFEST_SCHEMA, WEB_CAPTURE_MANIFEST_VERSION, WEB_CAPTURE_SUMMARY_SCHEMA, WEB_CAPTURE_SUMMARY_VERSION, } from "../contracts/web-capture.mjs";
|
|
8
|
+
export const MAX_WEB_CAPTURE_SUMMARIES = 50;
|
|
9
|
+
export { MAX_WEB_CAPTURE_DOM_DIGEST_BYTES, MAX_WEB_CAPTURE_SCREENSHOT_BYTES } from "../contracts/web-capture.mjs";
|
|
10
|
+
const MAX_CAPTURE_TECHNOLOGIES = 20;
|
|
11
|
+
const MAX_CAPTURE_ORIGINS = 30;
|
|
12
|
+
const SHA256_RE = /^[a-f0-9]{64}$/i;
|
|
13
|
+
const PERCEPTUAL_HASH_RE = /^[a-f0-9]{16}$/i;
|
|
14
|
+
const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
|
|
15
|
+
const ROOT_KEYS = new Set(['schema', 'schemaVersion', 'source', 'captures']);
|
|
16
|
+
const SOURCE_KEYS = new Set(['name', 'reference', 'collectedAt']);
|
|
17
|
+
const CAPTURE_KEYS = new Set([
|
|
18
|
+
'domain',
|
|
19
|
+
'capturedAt',
|
|
20
|
+
'completeness',
|
|
21
|
+
'limitations',
|
|
22
|
+
'pageTitle',
|
|
23
|
+
'finalOrigin',
|
|
24
|
+
'screenshotSha256',
|
|
25
|
+
'technologies',
|
|
26
|
+
'networkOrigins',
|
|
27
|
+
]);
|
|
28
|
+
const MANIFEST_CAPTURE_KEYS = new Set([
|
|
29
|
+
'pageBehaviour',
|
|
30
|
+
'conditions', 'observerLabel', 'vantageLabel',
|
|
31
|
+
'domain',
|
|
32
|
+
'capturedAt',
|
|
33
|
+
'completeness',
|
|
34
|
+
'limitations',
|
|
35
|
+
'page',
|
|
36
|
+
'requestDomains',
|
|
37
|
+
'technologies',
|
|
38
|
+
'artifacts',
|
|
39
|
+
]);
|
|
40
|
+
const PAGE_KEYS = new Set(['title', 'finalOrigin']);
|
|
41
|
+
const ARTIFACT_KEYS = new Set(['kind', 'fileName', 'mimeType', 'sha256', 'perceptualHash', 'bytes', 'width', 'height']);
|
|
42
|
+
function record(value) {
|
|
43
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
44
|
+
? value
|
|
45
|
+
: null;
|
|
46
|
+
}
|
|
47
|
+
function onlyKeys(value, allowed) {
|
|
48
|
+
return Object.keys(value).every((key) => allowed.has(key));
|
|
49
|
+
}
|
|
50
|
+
function text(value, maximum, label, optional = false) {
|
|
51
|
+
if (optional && (value === undefined || value === null || value === ''))
|
|
52
|
+
return null;
|
|
53
|
+
if (typeof value !== 'string' || !value.trim() || value.length > maximum || CONTROL_RE.test(value)) {
|
|
54
|
+
throw new Error(`${label} must be bounded text without control characters.`);
|
|
55
|
+
}
|
|
56
|
+
return value.replace(/\s+/g, ' ').trim();
|
|
57
|
+
}
|
|
58
|
+
function timestamp(value, label, optional = false) {
|
|
59
|
+
const candidate = text(value, 64, label, optional);
|
|
60
|
+
if (candidate === null)
|
|
61
|
+
return null;
|
|
62
|
+
const normalized = normalizeExplicitIsoTimestamp(candidate);
|
|
63
|
+
if (!normalized)
|
|
64
|
+
throw new Error(`${label} must be a valid date and time with an explicit timezone.`);
|
|
65
|
+
return normalized;
|
|
66
|
+
}
|
|
67
|
+
function origin(value, label) {
|
|
68
|
+
if (value === undefined || value === null || value === '')
|
|
69
|
+
return null;
|
|
70
|
+
const candidate = text(value, 500, label);
|
|
71
|
+
try {
|
|
72
|
+
const parsed = new URL(candidate ?? '');
|
|
73
|
+
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password)
|
|
74
|
+
throw new Error();
|
|
75
|
+
if (parsed.pathname !== '/' || parsed.search || parsed.hash)
|
|
76
|
+
throw new Error();
|
|
77
|
+
return parsed.origin.toLowerCase();
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
throw new Error(`${label} must be an HTTP(S) origin without credentials, path, query, or fragment.`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function stringList(value, maximumItems, maximumLength, label) {
|
|
84
|
+
if (value === undefined)
|
|
85
|
+
return [];
|
|
86
|
+
if (!Array.isArray(value) || value.length > maximumItems)
|
|
87
|
+
throw new Error(`${label} exceeds its item bound.`);
|
|
88
|
+
const output = new Set();
|
|
89
|
+
for (const item of value) {
|
|
90
|
+
const normalized = text(item, maximumLength, label);
|
|
91
|
+
if (normalized)
|
|
92
|
+
output.add(normalized);
|
|
93
|
+
}
|
|
94
|
+
return [...output];
|
|
95
|
+
}
|
|
96
|
+
export function parseWebCaptureSummary(value) {
|
|
97
|
+
const root = record(value);
|
|
98
|
+
if (!root
|
|
99
|
+
|| !onlyKeys(root, ROOT_KEYS)
|
|
100
|
+
|| root.schema !== WEB_CAPTURE_SUMMARY_SCHEMA
|
|
101
|
+
|| root.schemaVersion !== WEB_CAPTURE_SUMMARY_VERSION) {
|
|
102
|
+
throw new Error(`Web captures must use ${WEB_CAPTURE_SUMMARY_SCHEMA} schema version ${WEB_CAPTURE_SUMMARY_VERSION}.`);
|
|
103
|
+
}
|
|
104
|
+
const source = record(root.source);
|
|
105
|
+
if (!source || !onlyKeys(source, SOURCE_KEYS))
|
|
106
|
+
throw new Error('Web captures require a bounded source object.');
|
|
107
|
+
const sourceName = text(source.name, 80, 'Capture source name');
|
|
108
|
+
const sourceReference = text(source.reference, 500, 'Capture source reference', true);
|
|
109
|
+
const sourceCollectedAt = timestamp(source.collectedAt, 'Capture source collection time', true);
|
|
110
|
+
if (!Array.isArray(root.captures) || !root.captures.length || root.captures.length > MAX_WEB_CAPTURE_SUMMARIES) {
|
|
111
|
+
throw new Error(`Web captures must contain between 1 and ${MAX_WEB_CAPTURE_SUMMARIES} summaries.`);
|
|
112
|
+
}
|
|
113
|
+
const findings = [];
|
|
114
|
+
const domainCounts = new Map();
|
|
115
|
+
for (const [index, raw] of root.captures.entries()) {
|
|
116
|
+
const capture = record(raw);
|
|
117
|
+
if (!capture || !onlyKeys(capture, CAPTURE_KEYS))
|
|
118
|
+
throw new Error(`Web capture ${index + 1} contains unsupported fields.`);
|
|
119
|
+
const domain = normalizeDomain(text(capture.domain, 253, `Web capture ${index + 1} domain`));
|
|
120
|
+
if (!domain)
|
|
121
|
+
throw new Error(`Web capture ${index + 1} domain is invalid.`);
|
|
122
|
+
const domainCount = (domainCounts.get(domain) ?? 0) + 1;
|
|
123
|
+
if (domainCount > MAX_EXTERNAL_FINDINGS_PER_DOMAIN) {
|
|
124
|
+
throw new Error(`Web captures exceed the ${MAX_EXTERNAL_FINDINGS_PER_DOMAIN}-summary per-domain limit.`);
|
|
125
|
+
}
|
|
126
|
+
domainCounts.set(domain, domainCount);
|
|
127
|
+
if (domainCounts.size > MAX_EXTERNAL_FINDING_DOMAINS) {
|
|
128
|
+
throw new Error(`Web captures exceed the ${MAX_EXTERNAL_FINDING_DOMAINS}-domain limit.`);
|
|
129
|
+
}
|
|
130
|
+
const observedAt = timestamp(capture.capturedAt, `Web capture ${index + 1} time`);
|
|
131
|
+
const completeness = ['complete', 'inconclusive', 'partial', 'unknown'].includes(String(capture.completeness))
|
|
132
|
+
? capture.completeness
|
|
133
|
+
: 'unknown';
|
|
134
|
+
const limitations = stringList(capture.limitations, 8, 240, `Web capture ${index + 1} limitations`);
|
|
135
|
+
const pageTitle = text(capture.pageTitle, 300, `Web capture ${index + 1} title`, true);
|
|
136
|
+
const finalOrigin = origin(capture.finalOrigin, `Web capture ${index + 1} final origin`);
|
|
137
|
+
const technologies = stringList(capture.technologies, MAX_CAPTURE_TECHNOLOGIES, 80, `Web capture ${index + 1} technologies`);
|
|
138
|
+
const networkOrigins = stringList(capture.networkOrigins, MAX_CAPTURE_ORIGINS, 500, `Web capture ${index + 1} network origins`)
|
|
139
|
+
.map((item, originIndex) => origin(item, `Web capture ${index + 1} network origin ${originIndex + 1}`))
|
|
140
|
+
.filter((item) => item !== null);
|
|
141
|
+
const screenshotSha256 = capture.screenshotSha256 === undefined || capture.screenshotSha256 === null || capture.screenshotSha256 === ''
|
|
142
|
+
? null
|
|
143
|
+
: typeof capture.screenshotSha256 === 'string' && SHA256_RE.test(capture.screenshotSha256)
|
|
144
|
+
? capture.screenshotSha256.toLowerCase()
|
|
145
|
+
: (() => { throw new Error(`Web capture ${index + 1} screenshot digest must be SHA-256.`); })();
|
|
146
|
+
const summaries = [
|
|
147
|
+
pageTitle || finalOrigin
|
|
148
|
+
? `Sanitised page capture${pageTitle ? ` titled "${pageTitle}"` : ''}${finalOrigin ? ` ended at origin ${finalOrigin}` : ''}.`
|
|
149
|
+
: '',
|
|
150
|
+
technologies.length ? `Observed technology labels: ${technologies.join(', ')}.` : '',
|
|
151
|
+
networkOrigins.length ? `Observed network origins: ${networkOrigins.join(', ')}.` : '',
|
|
152
|
+
screenshotSha256 ? `Screenshot SHA-256: ${screenshotSha256}.` : '',
|
|
153
|
+
].filter(Boolean);
|
|
154
|
+
if (!summaries.length)
|
|
155
|
+
throw new Error(`Web capture ${index + 1} contains no supported summary evidence.`);
|
|
156
|
+
const retainedLimitations = retainExternalFindingLimitations(limitations, [
|
|
157
|
+
'Imported sanitised capture summary; WHOISleuth did not collect or independently verify this observation.',
|
|
158
|
+
]);
|
|
159
|
+
findings.push({
|
|
160
|
+
domain,
|
|
161
|
+
category: technologies.length && summaries.length === 1 ? 'http' : 'page',
|
|
162
|
+
evidenceClass: 'deployment_observation',
|
|
163
|
+
summary: summaries.join(' '),
|
|
164
|
+
observedAt,
|
|
165
|
+
completeness: limitations.every((item) => retainedLimitations.includes(item)) ? completeness : 'partial',
|
|
166
|
+
limitations: retainedLimitations,
|
|
167
|
+
reference: sourceReference,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return parseExternalFindingsDocument({
|
|
171
|
+
schema: EXTERNAL_FINDINGS_SCHEMA,
|
|
172
|
+
schemaVersion: EXTERNAL_FINDINGS_VERSION,
|
|
173
|
+
source: { name: sourceName, reference: sourceReference, collectedAt: sourceCollectedAt },
|
|
174
|
+
findings,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
function artifactName(value, label) {
|
|
178
|
+
const candidate = text(value, 120, label) ?? '';
|
|
179
|
+
if (candidate === '.'
|
|
180
|
+
|| candidate === '..'
|
|
181
|
+
|| candidate.includes('/')
|
|
182
|
+
|| candidate.includes('\\')
|
|
183
|
+
|| candidate.includes('%2f')
|
|
184
|
+
|| candidate.includes('%5c')) {
|
|
185
|
+
throw new Error(`${label} must be a plain file name without a path.`);
|
|
186
|
+
}
|
|
187
|
+
return candidate;
|
|
188
|
+
}
|
|
189
|
+
function positiveInteger(value, maximum, label) {
|
|
190
|
+
if (!Number.isInteger(value) || Number(value) <= 0 || Number(value) > maximum) {
|
|
191
|
+
throw new Error(`${label} is outside the supported bound.`);
|
|
192
|
+
}
|
|
193
|
+
return Number(value);
|
|
194
|
+
}
|
|
195
|
+
function listSummaryFragments(label, values) {
|
|
196
|
+
const fragments = [];
|
|
197
|
+
let current = `${label}:`;
|
|
198
|
+
for (const value of values) {
|
|
199
|
+
const separator = current === `${label}:` ? ' ' : ', ';
|
|
200
|
+
if (`${current}${separator}${value}.`.length > MAX_EXTERNAL_FINDING_SUMMARY_LENGTH) {
|
|
201
|
+
fragments.push(`${current}.`);
|
|
202
|
+
current = `${label}: ${value}`;
|
|
203
|
+
}
|
|
204
|
+
else
|
|
205
|
+
current += `${separator}${value}`;
|
|
206
|
+
}
|
|
207
|
+
if (current !== `${label}:`)
|
|
208
|
+
fragments.push(`${current}.`);
|
|
209
|
+
return fragments;
|
|
210
|
+
}
|
|
211
|
+
function partitionSummary(fragments) {
|
|
212
|
+
const summaries = [];
|
|
213
|
+
let current = '';
|
|
214
|
+
for (const fragment of fragments) {
|
|
215
|
+
if (!fragment || fragment.length > MAX_EXTERNAL_FINDING_SUMMARY_LENGTH) {
|
|
216
|
+
throw new Error('Web capture metadata cannot be represented within the external-finding summary bound.');
|
|
217
|
+
}
|
|
218
|
+
const combined = current ? `${current} ${fragment}` : fragment;
|
|
219
|
+
if (combined.length > MAX_EXTERNAL_FINDING_SUMMARY_LENGTH) {
|
|
220
|
+
summaries.push(current);
|
|
221
|
+
current = fragment;
|
|
222
|
+
}
|
|
223
|
+
else
|
|
224
|
+
current = combined;
|
|
225
|
+
}
|
|
226
|
+
if (current)
|
|
227
|
+
summaries.push(current);
|
|
228
|
+
return summaries;
|
|
229
|
+
}
|
|
230
|
+
export function readWebCaptureManifest(value) {
|
|
231
|
+
const root = record(value);
|
|
232
|
+
if (!root
|
|
233
|
+
|| !onlyKeys(root, ROOT_KEYS)
|
|
234
|
+
|| root.schema !== WEB_CAPTURE_MANIFEST_SCHEMA
|
|
235
|
+
|| !WEB_CAPTURE_MANIFEST_SUPPORTED_VERSIONS.includes(root.schemaVersion)) {
|
|
236
|
+
throw new Error(`Web capture manifests must use ${WEB_CAPTURE_MANIFEST_SCHEMA} schema version ${WEB_CAPTURE_MANIFEST_VERSION}.`);
|
|
237
|
+
}
|
|
238
|
+
const source = record(root.source);
|
|
239
|
+
if (!source || !onlyKeys(source, SOURCE_KEYS))
|
|
240
|
+
throw new Error('Web capture manifests require a bounded source object.');
|
|
241
|
+
const sourceName = text(source.name, 80, 'Capture source name');
|
|
242
|
+
const sourceReference = text(source.reference, 500, 'Capture source reference', true);
|
|
243
|
+
const sourceCollectedAt = timestamp(source.collectedAt, 'Capture source collection time', true);
|
|
244
|
+
if (!Array.isArray(root.captures) || !root.captures.length || root.captures.length > MAX_WEB_CAPTURE_SUMMARIES) {
|
|
245
|
+
throw new Error(`Web capture manifests must contain between 1 and ${MAX_WEB_CAPTURE_SUMMARIES} captures.`);
|
|
246
|
+
}
|
|
247
|
+
const findings = [];
|
|
248
|
+
const artifacts = [];
|
|
249
|
+
const contexts = [];
|
|
250
|
+
const domainCounts = new Map();
|
|
251
|
+
const findingCounts = new Map();
|
|
252
|
+
for (const [index, raw] of root.captures.entries()) {
|
|
253
|
+
const capture = record(raw);
|
|
254
|
+
if (!capture || !onlyKeys(capture, MANIFEST_CAPTURE_KEYS)) {
|
|
255
|
+
throw new Error(`Web capture manifest ${index + 1} contains unsupported fields or archive content.`);
|
|
256
|
+
}
|
|
257
|
+
const domain = normalizeDomain(text(capture.domain, 253, `Web capture manifest ${index + 1} domain`));
|
|
258
|
+
if (!domain)
|
|
259
|
+
throw new Error(`Web capture manifest ${index + 1} domain is invalid.`);
|
|
260
|
+
const domainCount = (domainCounts.get(domain) ?? 0) + 1;
|
|
261
|
+
if (domainCount > MAX_EXTERNAL_FINDINGS_PER_DOMAIN) {
|
|
262
|
+
throw new Error(`Web capture manifests exceed the ${MAX_EXTERNAL_FINDINGS_PER_DOMAIN}-capture per-domain limit.`);
|
|
263
|
+
}
|
|
264
|
+
domainCounts.set(domain, domainCount);
|
|
265
|
+
if (domainCounts.size > MAX_EXTERNAL_FINDING_DOMAINS) {
|
|
266
|
+
throw new Error(`Web capture manifests exceed the ${MAX_EXTERNAL_FINDING_DOMAINS}-domain limit.`);
|
|
267
|
+
}
|
|
268
|
+
const observedAt = timestamp(capture.capturedAt, `Web capture manifest ${index + 1} time`);
|
|
269
|
+
const conditions = readCaptureConditions(capture.conditions);
|
|
270
|
+
const observerLabel = readObservationLabel(capture.observerLabel), vantageLabel = readObservationLabel(capture.vantageLabel);
|
|
271
|
+
const completeness = ['complete', 'inconclusive', 'partial', 'unknown'].includes(String(capture.completeness))
|
|
272
|
+
? capture.completeness
|
|
273
|
+
: 'unknown';
|
|
274
|
+
const limitations = stringList(capture.limitations, 8, 240, `Web capture manifest ${index + 1} limitations`);
|
|
275
|
+
const pageBehaviour = readManifestPageBehaviour(capture.pageBehaviour, root.schemaVersion);
|
|
276
|
+
if (pageBehaviour?.state === 'partial' && completeness === 'complete')
|
|
277
|
+
throw new Error('Partial page observations cannot declare a complete capture.');
|
|
278
|
+
contexts.push({ domain, observedAt, completeness: String(completeness), conditions, observerLabel, vantageLabel, pageBehaviour });
|
|
279
|
+
const page = record(capture.page);
|
|
280
|
+
if (page && !onlyKeys(page, PAGE_KEYS))
|
|
281
|
+
throw new Error(`Web capture manifest ${index + 1} page metadata contains unsupported fields.`);
|
|
282
|
+
const pageTitle = text(page?.title, 300, `Web capture manifest ${index + 1} title`, true);
|
|
283
|
+
const finalOrigin = origin(page?.finalOrigin, `Web capture manifest ${index + 1} final origin`);
|
|
284
|
+
const technologies = stringList(capture.technologies, MAX_CAPTURE_TECHNOLOGIES, 80, `Web capture manifest ${index + 1} technologies`);
|
|
285
|
+
const requestDomains = stringList(capture.requestDomains, MAX_CAPTURE_ORIGINS, 253, `Web capture manifest ${index + 1} request domains`)
|
|
286
|
+
.map((item) => normalizeDomain(item))
|
|
287
|
+
.filter((item) => Boolean(item));
|
|
288
|
+
if (!Array.isArray(capture.artifacts) || !capture.artifacts.length || capture.artifacts.length > 2) {
|
|
289
|
+
throw new Error(`Web capture manifest ${index + 1} must contain one or two artifact metadata records.`);
|
|
290
|
+
}
|
|
291
|
+
const artifactSummaries = [];
|
|
292
|
+
const kinds = new Set();
|
|
293
|
+
for (const [artifactIndex, rawArtifact] of capture.artifacts.entries()) {
|
|
294
|
+
const artifact = record(rawArtifact);
|
|
295
|
+
if (!artifact || !onlyKeys(artifact, ARTIFACT_KEYS)) {
|
|
296
|
+
throw new Error(`Web capture manifest ${index + 1} artifact ${artifactIndex + 1} contains unsupported fields.`);
|
|
297
|
+
}
|
|
298
|
+
const kind = artifact.kind === 'screenshot' || artifact.kind === 'dom_digest' ? artifact.kind : null;
|
|
299
|
+
if (!kind || kinds.has(kind))
|
|
300
|
+
throw new Error(`Web capture manifest ${index + 1} has an invalid or duplicate artifact kind.`);
|
|
301
|
+
kinds.add(kind);
|
|
302
|
+
const fileName = artifactName(artifact.fileName, `Web capture manifest ${index + 1} artifact name`);
|
|
303
|
+
const sha256 = typeof artifact.sha256 === 'string' && SHA256_RE.test(artifact.sha256)
|
|
304
|
+
? artifact.sha256.toLowerCase()
|
|
305
|
+
: null;
|
|
306
|
+
if (!sha256)
|
|
307
|
+
throw new Error(`Web capture manifest ${index + 1} artifact digest must be SHA-256.`);
|
|
308
|
+
const mimeType = text(artifact.mimeType, 80, `Web capture manifest ${index + 1} artifact MIME type`)?.toLowerCase();
|
|
309
|
+
const maximumBytes = kind === 'screenshot' ? MAX_WEB_CAPTURE_SCREENSHOT_BYTES : MAX_WEB_CAPTURE_DOM_DIGEST_BYTES;
|
|
310
|
+
const bytes = positiveInteger(artifact.bytes, maximumBytes, `Web capture manifest ${index + 1} artifact size`);
|
|
311
|
+
if (kind === 'screenshot') {
|
|
312
|
+
if (!['image/png', 'image/jpeg', 'image/webp'].includes(mimeType || '')) {
|
|
313
|
+
throw new Error(`Web capture manifest ${index + 1} screenshot MIME type is unsupported.`);
|
|
314
|
+
}
|
|
315
|
+
const width = positiveInteger(artifact.width, 10_000, `Web capture manifest ${index + 1} screenshot width`);
|
|
316
|
+
const height = positiveInteger(artifact.height, 10_000, `Web capture manifest ${index + 1} screenshot height`);
|
|
317
|
+
const perceptualHash = artifact.perceptualHash === undefined || artifact.perceptualHash === null || artifact.perceptualHash === ''
|
|
318
|
+
? null
|
|
319
|
+
: typeof artifact.perceptualHash === 'string' && PERCEPTUAL_HASH_RE.test(artifact.perceptualHash)
|
|
320
|
+
? artifact.perceptualHash.toLowerCase()
|
|
321
|
+
: (() => { throw new Error(`Web capture manifest ${index + 1} screenshot perceptual hash is unsupported.`); })();
|
|
322
|
+
artifactSummaries.push(`Screenshot ${fileName}: ${mimeType}, ${width}x${height}, ${bytes} bytes, SHA-256 ${sha256}${perceptualHash ? `, dHash ${perceptualHash}` : ''}.`);
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
if (mimeType !== 'application/json')
|
|
326
|
+
throw new Error(`Web capture manifest ${index + 1} DOM digest MIME type is unsupported.`);
|
|
327
|
+
if (artifact.width !== undefined || artifact.height !== undefined || artifact.perceptualHash !== undefined) {
|
|
328
|
+
throw new Error(`Web capture manifest ${index + 1} DOM digest cannot declare image-only fields.`);
|
|
329
|
+
}
|
|
330
|
+
artifactSummaries.push(`DOM digest ${fileName}: application/json, ${bytes} bytes, SHA-256 ${sha256}.`);
|
|
331
|
+
}
|
|
332
|
+
artifacts.push({ capture: index + 1, observedAt, kind, fileName, mimeType: mimeType, sha256, bytes });
|
|
333
|
+
}
|
|
334
|
+
const summaryFragments = [
|
|
335
|
+
...(conditions ? [`Declared capture conditions: ${conditions.browser} ${conditions.browserVersion}; viewport ${conditions.viewport.width}x${conditions.viewport.height}; scale ${conditions.deviceScaleFactor}; locale ${conditions.locale}; timezone ${conditions.timezone}; ${conditions.colourScheme}.`] : []),
|
|
336
|
+
...(observerLabel || vantageLabel ? [`Declared observer: ${observerLabel ?? 'unknown'}; vantage: ${vantageLabel ?? 'unknown'}. These labels do not verify collection independence.`] : []),
|
|
337
|
+
pageTitle || finalOrigin
|
|
338
|
+
? `Sanitised page capture${pageTitle ? ` titled "${pageTitle}"` : ''}${finalOrigin ? ` ended at origin ${finalOrigin}` : ''}.`
|
|
339
|
+
: '',
|
|
340
|
+
...listSummaryFragments('Observed technology labels', technologies),
|
|
341
|
+
...listSummaryFragments('Observed request domains', requestDomains),
|
|
342
|
+
...(pageBehaviour ? [`Page observations: ${pageBehaviour.requests.length} navigation/script/frame responses; ${pageBehaviour.elements.length} script/frame/form elements; ${pageBehaviour.clipboardWriteAttempts} blocked Clipboard API write attempts. State: ${pageBehaviour.state}. Full observations remain in the selected manifest.`,
|
|
343
|
+
...listSummaryFragments('Requested-action wording in body text', pageBehaviour.actionHints)] : []),
|
|
344
|
+
...artifactSummaries,
|
|
345
|
+
].filter(Boolean);
|
|
346
|
+
const summaries = partitionSummary(summaryFragments);
|
|
347
|
+
const priorFindingCount = findingCounts.get(domain) ?? 0;
|
|
348
|
+
if (priorFindingCount + summaries.length > MAX_EXTERNAL_FINDINGS_PER_DOMAIN) {
|
|
349
|
+
throw new Error(`Web capture manifests exceed the ${MAX_EXTERNAL_FINDINGS_PER_DOMAIN}-finding per-domain import limit after preserving bounded metadata.`);
|
|
350
|
+
}
|
|
351
|
+
findingCounts.set(domain, priorFindingCount + summaries.length);
|
|
352
|
+
const retainedLimitations = retainExternalFindingLimitations(limitations, [
|
|
353
|
+
'Imported capture metadata, not independently collected website evidence; artefact bytes and separate byte checks are not retained in these findings.',
|
|
354
|
+
]);
|
|
355
|
+
for (const summary of summaries) {
|
|
356
|
+
findings.push({
|
|
357
|
+
domain,
|
|
358
|
+
category: 'page',
|
|
359
|
+
evidenceClass: 'deployment_observation',
|
|
360
|
+
summary,
|
|
361
|
+
observedAt,
|
|
362
|
+
completeness: limitations.every((item) => retainedLimitations.includes(item)) ? completeness : 'partial',
|
|
363
|
+
limitations: retainedLimitations,
|
|
364
|
+
reference: sourceReference,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const document = parseExternalFindingsDocument({
|
|
369
|
+
schema: EXTERNAL_FINDINGS_SCHEMA,
|
|
370
|
+
schemaVersion: EXTERNAL_FINDINGS_VERSION,
|
|
371
|
+
source: { name: sourceName, reference: sourceReference, collectedAt: sourceCollectedAt },
|
|
372
|
+
findings,
|
|
373
|
+
});
|
|
374
|
+
return { document, artifacts, captures: contexts };
|
|
375
|
+
}
|
|
376
|
+
export function parseWebCaptureManifest(value) {
|
|
377
|
+
return readWebCaptureManifest(value).document;
|
|
378
|
+
}
|
|
379
|
+
// Candidates must describe bytes already read and hashed by the caller. File
|
|
380
|
+
// names and MIME declarations never establish identity; equal copies are all
|
|
381
|
+
// reported rather than choosing an arbitrary file.
|
|
382
|
+
export function matchCaptureArtifacts(artifacts, candidates) {
|
|
383
|
+
return artifacts.map(artifact => {
|
|
384
|
+
const matchingIds = candidates.filter(candidate => candidate.bytes === artifact.bytes
|
|
385
|
+
&& candidate.sha256.replace(/^sha256:/u, '') === artifact.sha256)
|
|
386
|
+
.map(candidate => candidate.id);
|
|
387
|
+
return { capture: artifact.capture, kind: artifact.kind, state: matchingIds.length ? 'matched' : 'not_found', matchingIds };
|
|
388
|
+
});
|
|
389
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SORTED_JSON_V2, sha256ArtifactDigestV2 } from "../evidence/artifact-integrity.mjs";
|
|
2
2
|
import { ACQUISITION_DECISION_PACKET_SCHEMA, ACQUISITION_DECISION_PACKET_VERSION, } from "../contracts/investigation-portability.mjs";
|
|
3
|
-
import { isValidAsciiDomainName } from "
|
|
3
|
+
import { isValidAsciiDomainName } from "../contracts/domain-name.mjs";
|
|
4
4
|
export { ACQUISITION_DECISION_PACKET_SCHEMA, ACQUISITION_DECISION_PACKET_VERSION };
|
|
5
5
|
export const ACQUISITION_DECISIONS = [
|
|
6
6
|
'unresolved',
|
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import { SORTED_JSON_V2, sha256ArtifactDigestV2 } from "../evidence/artifact-integrity.mjs";
|
|
2
|
+
import { PASSIVE_MAIL_INTERPRETATION, PASSIVE_MAIL_LABELS } from "../contracts/passive-mail.mjs";
|
|
2
3
|
import { BULK_PROFILE_CONTEXT_MISMATCH_LIMITATION, normalizeBulkProfileContext, normalizeBulkSessionResult, } from "../workspace/bulk-session-model.mjs";
|
|
3
4
|
import { BULK_MAIL_EXPOSURE_EXPORT_VERSION, BULK_MAIL_EXPOSURE_SCHEMA, BULK_MAIL_EXPOSURE_VERSION, MAX_BULK_MAIL_EXPOSURE_ROWS, } from "../contracts/investigation-portability.mjs";
|
|
4
5
|
export { BULK_MAIL_EXPOSURE_EXPORT_VERSION, BULK_MAIL_EXPOSURE_SCHEMA, BULK_MAIL_EXPOSURE_VERSION, MAX_BULK_MAIL_EXPOSURE_ROWS };
|
|
5
|
-
const STATE_LABELS = {
|
|
6
|
-
authenticated_mail: 'Receiving mail with SPF and DMARC',
|
|
7
|
-
evidence_incomplete: 'Mail evidence incomplete',
|
|
8
|
-
mail_auth_gap: 'Receiving mail with an authentication gap',
|
|
9
|
-
mail_auth_incomplete: 'Receiving mail with incomplete authentication evidence',
|
|
10
|
-
no_explicit_mx: 'No explicit MX observed',
|
|
11
|
-
null_mx: 'Null MX observed',
|
|
12
|
-
};
|
|
13
6
|
function timestamp(value) {
|
|
14
7
|
if (typeof value !== 'string' || !Number.isFinite(Date.parse(value)))
|
|
15
8
|
return null;
|
|
@@ -51,16 +44,16 @@ function exposureState(row) {
|
|
|
51
44
|
}
|
|
52
45
|
function stateDetail(state) {
|
|
53
46
|
if (state === 'authenticated_mail') {
|
|
54
|
-
return '
|
|
47
|
+
return 'Explicit MX, SPF and DMARC publications were observed.';
|
|
55
48
|
}
|
|
56
49
|
if (state === 'null_mx') {
|
|
57
50
|
return 'A null MX explicitly declaring no inbound mail was observed.';
|
|
58
51
|
}
|
|
59
52
|
if (state === 'mail_auth_gap') {
|
|
60
|
-
return '
|
|
53
|
+
return 'An MX publication was observed while SPF or DMARC was observed absent.';
|
|
61
54
|
}
|
|
62
55
|
if (state === 'mail_auth_incomplete') {
|
|
63
|
-
return '
|
|
56
|
+
return 'An MX publication was observed, but SPF or DMARC collection was incomplete.';
|
|
64
57
|
}
|
|
65
58
|
if (state === 'no_explicit_mx') {
|
|
66
59
|
return 'DNS collection completed without an explicit MX. This is not proof that SMTP delivery is impossible.';
|
|
@@ -173,12 +166,13 @@ export function buildBulkMailExposureReport(rowsRaw, options = {}) {
|
|
|
173
166
|
return {
|
|
174
167
|
domain: item.domain,
|
|
175
168
|
state,
|
|
176
|
-
label:
|
|
169
|
+
label: PASSIVE_MAIL_LABELS[state],
|
|
177
170
|
detail: stateDetail(state),
|
|
178
171
|
...relation(state, profile, profileSourceState, profileContextComparable, profileContextLimitation),
|
|
179
172
|
mutationTypes: item.mutationTypes.slice(0, 40),
|
|
180
173
|
registration: item.availability,
|
|
181
|
-
|
|
174
|
+
// The published review retains source states; full source clocks stay in the workspace.
|
|
175
|
+
sourceCoverage: item.sourceCoverage.map(({ source, state }) => ({ source, state })),
|
|
182
176
|
profileContextState: item.profileContext.sourceState,
|
|
183
177
|
profileContextLimitation,
|
|
184
178
|
limitations: rowLimitations(item, state, profileSourceState, profileContextComparable, profileContextLimitation),
|
|
@@ -216,8 +210,9 @@ export function buildBulkMailExposureReport(rowsRaw, options = {}) {
|
|
|
216
210
|
counts,
|
|
217
211
|
profileContextUnevaluatedCount,
|
|
218
212
|
limitations: [
|
|
213
|
+
PASSIVE_MAIL_INTERPRETATION,
|
|
219
214
|
'This review uses compact Bulk evidence already collected and makes no additional request.',
|
|
220
|
-
'Null MX, no explicit MX,
|
|
215
|
+
'Null MX, no explicit MX, published mail policies, policy gaps, and incomplete evidence remain separate states.',
|
|
221
216
|
'SMTP delivery, mailbox existence, catch-all behaviour, and message acceptance were not tested.',
|
|
222
217
|
],
|
|
223
218
|
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { array, boolean, enumeration, exact, integer, text } from "../evidence/artifact-structure.mjs";
|
|
2
|
+
export const MAX_CAPTURE_CHANNEL_OBSERVATIONS = 500;
|
|
3
|
+
export const CAPTURE_REQUEST_CHANNELS = ['navigation', 'document', 'frame', 'script', 'stylesheet', 'image', 'font', 'media', 'xhr', 'fetch', 'beacon', 'event_source', 'other'];
|
|
4
|
+
export const CAPTURE_DISABLED_SURFACES = ['service_workers', 'dedicated_workers', 'shared_workers', 'websockets', 'webrtc', 'webtransport', 'downloads'];
|
|
5
|
+
export function emptyCaptureCoverage() {
|
|
6
|
+
return { attempts: [], omittedAttempts: 0, disabledSurfaces: [...CAPTURE_DISABLED_SURFACES], interactions: 'not_exercised', websocketRefusals: 0, directConnectionRefusals: 0 };
|
|
7
|
+
}
|
|
8
|
+
export function captureCoverageIsPartial(value) {
|
|
9
|
+
return Boolean(value.omittedAttempts || value.websocketRefusals || value.directConnectionRefusals || value.attempts.some(row => row.state !== 'observed'));
|
|
10
|
+
}
|
|
11
|
+
export function readCaptureCoverage(raw) {
|
|
12
|
+
const value = exact(raw, ['attempts', 'omittedAttempts', 'disabledSurfaces', 'interactions', 'websocketRefusals', 'directConnectionRefusals'], 'Capture coverage');
|
|
13
|
+
const disabledSurfaces = array(value.disabledSurfaces, 'Disabled capture surfaces', CAPTURE_DISABLED_SURFACES.length).map(value => enumeration(value, CAPTURE_DISABLED_SURFACES, 'Disabled capture surface'));
|
|
14
|
+
if (new Set(disabledSurfaces).size !== CAPTURE_DISABLED_SURFACES.length)
|
|
15
|
+
throw new TypeError('Capture coverage must identify every required disabled surface.');
|
|
16
|
+
const attempts = array(value.attempts, 'Capture request attempts', MAX_CAPTURE_CHANNEL_OBSERVATIONS).map(raw => {
|
|
17
|
+
const row = exact(raw, ['position', 'channel', 'method', 'origin', 'state', 'reason', 'collectionStarted'], 'Capture request attempt');
|
|
18
|
+
const state = enumeration(row.state, ['observed', 'refused', 'unavailable'], 'Capture request state');
|
|
19
|
+
const method = enumeration(row.method, ['read', 'non_read', 'unknown'], 'Capture method category');
|
|
20
|
+
const reason = row.reason === null ? null : enumeration(row.reason, ['method', 'request_bound', 'host_bound', 'response_bound', 'shutdown', 'address_or_transport'], 'Capture request qualification');
|
|
21
|
+
boolean(row.collectionStarted, 'Collector-started flag');
|
|
22
|
+
let origin = null;
|
|
23
|
+
if (row.origin !== null) {
|
|
24
|
+
const candidate = text(row.origin, 'Capture request origin', 500), parsed = new URL(candidate);
|
|
25
|
+
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.origin !== candidate || parsed.username || parsed.password)
|
|
26
|
+
throw new TypeError('Capture request coverage retains origins only.');
|
|
27
|
+
origin = candidate;
|
|
28
|
+
}
|
|
29
|
+
if (state === 'observed' && (reason !== null || method !== 'read' || !origin || row.collectionStarted !== true)
|
|
30
|
+
|| state !== 'observed' && reason === null
|
|
31
|
+
|| reason === 'method' && (method === 'read' || row.collectionStarted)
|
|
32
|
+
|| ['request_bound', 'host_bound', 'shutdown'].includes(reason ?? '') && row.collectionStarted
|
|
33
|
+
|| state === 'unavailable' && reason !== 'address_or_transport')
|
|
34
|
+
throw new TypeError('Capture request coverage has contradictory collection states.');
|
|
35
|
+
return { position: integer(row.position, 'Capture request position', 1, 1_000_000), channel: enumeration(row.channel, CAPTURE_REQUEST_CHANNELS, 'Capture request channel'),
|
|
36
|
+
method, origin, state, reason, collectionStarted: row.collectionStarted };
|
|
37
|
+
});
|
|
38
|
+
if (attempts.some((row, index) => index > 0 && row.position <= attempts[index - 1].position))
|
|
39
|
+
throw new TypeError('Capture request positions must be unique and ordered.');
|
|
40
|
+
return { attempts, omittedAttempts: integer(value.omittedAttempts, 'Omitted capture attempts', 0, 1_000_000), disabledSurfaces: [...CAPTURE_DISABLED_SURFACES],
|
|
41
|
+
interactions: enumeration(value.interactions, ['not_exercised'], 'Capture interaction coverage'),
|
|
42
|
+
websocketRefusals: integer(value.websocketRefusals, 'WebSocket refusals', 0, 1_000_000), directConnectionRefusals: integer(value.directConnectionRefusals, 'Direct-connection refusals', 0, 1_000_000) };
|
|
43
|
+
}
|
|
44
|
+
export function captureAttemptDescription(value) {
|
|
45
|
+
const status = value.state === 'observed' ? 'Response supplied' : value.state === 'refused' ? 'Response refused' : 'Response unavailable';
|
|
46
|
+
const reason = { method: 'non-read method', request_bound: 'request limit', host_bound: 'host limit', response_bound: 'response byte limit', shutdown: 'capture closing', address_or_transport: 'address or transport check' };
|
|
47
|
+
return `${status}${value.reason ? ` · ${reason[value.reason]}` : ''} · collector ${value.collectionStarted ? 'started' : 'not started'}`;
|
|
48
|
+
}
|
|
49
|
+
/** Unseen in a bounded capture is not the same as disabled or absent. */
|
|
50
|
+
export function captureChannelSummary(value) {
|
|
51
|
+
return CAPTURE_REQUEST_CHANNELS.map(channel => {
|
|
52
|
+
const rows = value.attempts.filter(row => row.channel === channel);
|
|
53
|
+
return { channel, observed: rows.filter(row => row.state === 'observed').length, refused: rows.filter(row => row.state === 'refused').length,
|
|
54
|
+
unavailable: rows.filter(row => row.state === 'unavailable').length,
|
|
55
|
+
coverage: rows.length ? 'recorded' : value.omittedAttempts ? 'unknown' : 'not_observed' };
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { array, exact, iso, record, text } from "../evidence/artifact-structure.mjs";
|
|
2
|
+
import { pageObservationOrigin } from "./page-behaviour.mjs";
|
|
3
|
+
import { CONTEXT_REVIEW_SCHEMA, CONTEXT_REVIEW_VERSION, MAX_CONTEXT_RECORDS, MAX_CONTEXT_INPUT_BYTES } from "../contracts/context-review.mjs";
|
|
4
|
+
import { parseBoundedJson } from "../analysis/bounded-json.mjs";
|
|
5
|
+
export { CONNECTOR_INPUT_SCHEMA, CONNECTOR_INPUT_VERSION } from "../contracts/context-review.mjs";
|
|
6
|
+
const KNOWN_FIELDS = new Set(['url', 'serverUrl', 'command', 'args', 'env', 'headers', 'type', 'transport', 'disabled', 'enabled', 'capabilities', 'package', 'oauth', 'auth']);
|
|
7
|
+
const CAPABILITIES = ['tools', 'resources', 'prompts', 'logging', 'completions'];
|
|
8
|
+
const PACKAGE = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*(?:@[a-zA-Z0-9][a-zA-Z0-9.+_-]*)?$/u;
|
|
9
|
+
function countObject(value, label) {
|
|
10
|
+
if (value === undefined)
|
|
11
|
+
return 0;
|
|
12
|
+
const keys = Object.keys(record(value, label));
|
|
13
|
+
if (keys.length > MAX_CONTEXT_RECORDS)
|
|
14
|
+
throw new TypeError(`${label} exceeds the metadata bound.`);
|
|
15
|
+
return keys.length;
|
|
16
|
+
}
|
|
17
|
+
/** Configuration is data only. Values of arguments, environment, headers and auth are never returned. */
|
|
18
|
+
export function readConnectorConfiguration(raw) {
|
|
19
|
+
const root = record(raw, 'Connector configuration');
|
|
20
|
+
const roots = ['mcpServers', 'servers'].filter(key => Object.hasOwn(root, key));
|
|
21
|
+
if (roots.length !== 1)
|
|
22
|
+
throw new TypeError('Select a configuration with exactly one mcpServers or servers object.');
|
|
23
|
+
if (Object.keys(root).some(key => !roots.includes(key)))
|
|
24
|
+
throw new TypeError('Select only the connector configuration section; unrelated settings are not reviewed.');
|
|
25
|
+
const entries = Object.entries(record(root[roots[0]], 'Connector entries'));
|
|
26
|
+
if (entries.length > MAX_CONTEXT_RECORDS)
|
|
27
|
+
throw new TypeError('Connector count exceeds the review bound.');
|
|
28
|
+
return entries.map(([name, value]) => {
|
|
29
|
+
text(name, 'Connector name', 200);
|
|
30
|
+
const item = record(value, 'Connector definition');
|
|
31
|
+
if (Object.keys(item).length > MAX_CONTEXT_RECORDS)
|
|
32
|
+
throw new TypeError('Connector definition exceeds the field bound.');
|
|
33
|
+
if (item.enabled !== undefined && typeof item.enabled !== 'boolean' || item.disabled !== undefined && typeof item.disabled !== 'boolean'
|
|
34
|
+
|| item.enabled !== undefined && item.disabled !== undefined && item.enabled === item.disabled)
|
|
35
|
+
throw new TypeError('Connector enablement declarations are invalid or conflicting.');
|
|
36
|
+
const suppliedUrl = item.url ?? item.serverUrl;
|
|
37
|
+
if (item.url !== undefined && item.serverUrl !== undefined && item.url !== item.serverUrl)
|
|
38
|
+
throw new TypeError('Conflicting connector URL declarations require a deliberate selection.');
|
|
39
|
+
const origin = suppliedUrl === undefined ? null : pageObservationOrigin(suppliedUrl);
|
|
40
|
+
if (suppliedUrl !== undefined && !origin)
|
|
41
|
+
throw new TypeError('Remote connector URL must be bounded HTTP(S) without credentials.');
|
|
42
|
+
const command = item.command === undefined ? null : text(item.command, 'Local connector command', 2_000);
|
|
43
|
+
const basename = command?.split(/[\\/]/u).at(-1) ?? null;
|
|
44
|
+
const executable = basename && /^[A-Za-z0-9._-]{1,120}$/u.test(basename) ? basename : null;
|
|
45
|
+
const args = item.args === undefined ? [] : array(item.args, 'Connector arguments', MAX_CONTEXT_RECORDS).map(value => text(value, 'Connector argument', 8_192, true));
|
|
46
|
+
let packageIdentity = item.package === undefined ? null : text(item.package, 'Declared connector package', 240);
|
|
47
|
+
if (packageIdentity !== null && !PACKAGE.test(packageIdentity))
|
|
48
|
+
throw new TypeError('Declared package must be a package identity, not an executable expression or URL.');
|
|
49
|
+
if (packageIdentity === null && ['npx', 'npx.cmd', 'uvx'].includes(executable ?? '')) {
|
|
50
|
+
// Only a simple first package operand is interpreted. Complex invocations remain unknown.
|
|
51
|
+
const operands = args.filter(value => !['--yes', '-y', '--no-install'].includes(value));
|
|
52
|
+
if (operands[0] && PACKAGE.test(operands[0]))
|
|
53
|
+
packageIdentity = operands[0];
|
|
54
|
+
}
|
|
55
|
+
const capabilityObject = item.capabilities === undefined ? {} : record(item.capabilities, 'Declared capabilities');
|
|
56
|
+
if (Object.keys(capabilityObject).length > MAX_CONTEXT_RECORDS)
|
|
57
|
+
throw new TypeError('Declared capabilities exceed the bound.');
|
|
58
|
+
return { name, transport: origin && command ? 'ambiguous' : origin ? 'remote' : command ? 'local' : 'unknown', enabled: item.enabled === false || item.disabled === true ? 'disabled' : item.enabled === true || item.disabled === false ? 'enabled' : 'unknown', origin, executable, package: packageIdentity,
|
|
59
|
+
argumentCount: args.length, environmentCount: countObject(item.env, 'Environment metadata'), headerCount: countObject(item.headers, 'Header metadata'),
|
|
60
|
+
authenticationDeclared: item.auth !== undefined || item.oauth !== undefined,
|
|
61
|
+
declaredCapabilities: CAPABILITIES.filter(key => Object.hasOwn(capabilityObject, key) && capabilityObject[key] !== false && capabilityObject[key] !== null),
|
|
62
|
+
unsupportedFields: Object.keys(item).filter(key => !KNOWN_FIELDS.has(key)).length + Object.keys(capabilityObject).filter(key => !CAPABILITIES.includes(key)).length,
|
|
63
|
+
privateLocationOmitted: suppliedUrl !== undefined && new URL(suppliedUrl).href !== `${origin}/` || command !== null && command !== executable,
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export function connectorProvenancePresentation(raw, reviewedAt) {
|
|
68
|
+
iso(reviewedAt, 'Review time');
|
|
69
|
+
const input = exact(raw, ['current', 'previous'], 'Connector review');
|
|
70
|
+
const current = readConnectorConfiguration(input.current), previous = input.previous === null ? null : readConnectorConfiguration(input.previous);
|
|
71
|
+
const signature = (row) => JSON.stringify([row.transport, row.enabled, row.origin, row.executable, row.package, row.argumentCount, row.environmentCount, row.headerCount, row.authenticationDeclared, row.declaredCapabilities]);
|
|
72
|
+
const observations = current.map(row => {
|
|
73
|
+
const old = previous?.find(value => value.name === row.name);
|
|
74
|
+
const changed = old ? signature(old) !== signature(row) : false;
|
|
75
|
+
return { label: `${row.name} · ${row.transport} connector`, state: row.transport === 'unknown' || row.transport === 'ambiguous' || row.unsupportedFields ? 'partial' : changed ? 'changed' : 'reported',
|
|
76
|
+
detail: `Declared enablement: ${row.enabled}. Endpoint: ${row.origin ?? 'not supplied'}; executable: ${row.executable ?? 'not retained or unknown'}; package: ${row.package ?? 'not established'}. Arguments: ${row.argumentCount}; environment entries: ${row.environmentCount}; headers: ${row.headerCount}; auth declaration ${row.authenticationDeclared ? 'present' : 'not supplied'}. Capabilities: ${row.declaredCapabilities.join(', ') || 'not declared'}. ${previous ? old ? changed ? 'Retained metadata changed.' : 'Retained metadata matches; excluded values were not compared.' : 'Newly listed connector.' : 'No earlier configuration selected.'}${row.unsupportedFields ? ` Uninterpreted fields: ${row.unsupportedFields}.` : ''}${row.privateLocationOmitted ? ' Private location detail omitted.' : ''}`,
|
|
77
|
+
source: 'Analyst-selected local configuration; declarations were not negotiated with a server', observedAt: null, hostname: row.origin ? new URL(row.origin).hostname : null };
|
|
78
|
+
});
|
|
79
|
+
for (const row of previous ?? [])
|
|
80
|
+
if (!current.some(value => value.name === row.name))
|
|
81
|
+
observations.push({ label: `${row.name} · no longer listed`, state: 'changed', detail: 'Absent from the selected current configuration; this does not establish that a process or endpoint has stopped.', source: 'Selected configuration comparison', observedAt: null, hostname: row.origin ? new URL(row.origin).hostname : null });
|
|
82
|
+
const report = { schema: CONTEXT_REVIEW_SCHEMA, version: CONTEXT_REVIEW_VERSION, kind: 'connector', reviewedAt, title: 'Connector provenance', state: !current.length || observations.some(row => row.state === 'partial') ? 'partial' : 'reviewed',
|
|
83
|
+
summary: `Configured connectors: ${current.length}${previous ? `; earlier configuration entries: ${previous.length}` : ''}. Commands were not run and endpoints were not contacted.`, observations,
|
|
84
|
+
nextSteps: ['Verify package publisher, exact version, installation source and requested privileges through a trusted source before enabling a local connector.', 'For remote connectors, investigate the endpoint domain separately and review authentication and data-sharing scope before granting access.', 'Review tool, resource and prompt capabilities against the task. Configuration declarations are not evidence of negotiated capabilities or trustworthy behaviour.'],
|
|
85
|
+
limitations: ['Arguments, local paths, environment values, header values, auth material and URL paths or queries are excluded from this report. Names and package identities remain visible; review them before sharing.', 'Matching retained metadata does not mean excluded values are unchanged. This is not code execution, a server connection, a protocol conformance test or safety certification.'] };
|
|
86
|
+
return { report, connectors: current };
|
|
87
|
+
}
|
|
88
|
+
export function reviewConnectorProvenance(raw, reviewedAt) {
|
|
89
|
+
return connectorProvenancePresentation(raw, reviewedAt).report;
|
|
90
|
+
}
|
|
91
|
+
export function reviewConnectorConfigurationText(current, previous, reviewedAt) {
|
|
92
|
+
return connectorConfigurationPresentation(current, previous, reviewedAt).report;
|
|
93
|
+
}
|
|
94
|
+
/** Browser presentation receives only the same minimised metadata as the report. */
|
|
95
|
+
export function connectorConfigurationPresentation(current, previous, reviewedAt) {
|
|
96
|
+
if (typeof current !== 'string' || typeof previous !== 'string')
|
|
97
|
+
throw new TypeError('Select connector configuration text.');
|
|
98
|
+
const parse = (value) => parseBoundedJson(value, { label: 'Connector configuration', maximumBytes: MAX_CONTEXT_INPUT_BYTES });
|
|
99
|
+
return connectorProvenancePresentation({ current: parse(current), previous: previous.trim() ? parse(previous) : null }, reviewedAt);
|
|
100
|
+
}
|