@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
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import { LOOKUP_ASSET_GRAPH_SCHEMA, LOOKUP_ASSET_GRAPH_VERSION, } from "../contracts/investigation-portability.mjs";
|
|
1
|
+
import { LOOKUP_ASSET_GRAPH_SCHEMA, LOOKUP_ASSET_GRAPH_VERSION, MAX_LOOKUP_ASSET_LIMITATIONS, } from "../contracts/investigation-portability.mjs";
|
|
2
|
+
import { MAX_PROFILE_VALUES } from "../contracts/workspace-portability.mjs";
|
|
3
|
+
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
4
|
+
import { canonicalRegistrableDomain } from "../analysis/registrable-domain.mjs";
|
|
5
|
+
import { createGraphInputReader, graphInputIsIncomplete } from "./lookup-asset-graph-inputs.mjs";
|
|
2
6
|
export { LOOKUP_ASSET_GRAPH_SCHEMA, LOOKUP_ASSET_GRAPH_VERSION };
|
|
3
7
|
const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/gu;
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const MAX_LIMITATIONS = 5;
|
|
8
|
+
const MAX_VALUES = 64;
|
|
9
|
+
const MAX_NESTED_VALUES = 32;
|
|
10
|
+
const MAX_LIMITATIONS = MAX_LOOKUP_ASSET_LIMITATIONS;
|
|
8
11
|
const MAX_VISUAL_EDGES_PER_HUB = 10;
|
|
9
|
-
function projectedNodeGroup(node,
|
|
12
|
+
function projectedNodeGroup(node, incident) {
|
|
10
13
|
if (node.kind === 'target')
|
|
11
14
|
return { id: 'focus', label: 'Lookup target' };
|
|
12
15
|
if (node.kind === 'address' || node.kind === 'network' || node.kind === 'prefix') {
|
|
@@ -23,7 +26,6 @@ function projectedNodeGroup(node, edges) {
|
|
|
23
26
|
return { id: 'identity', label: 'Web identity' };
|
|
24
27
|
}
|
|
25
28
|
if (node.kind === 'hostname') {
|
|
26
|
-
const incident = edges.filter((edge) => edge.source === node.id || edge.target === node.id);
|
|
27
29
|
if (incident.some((edge) => edge.kind === 'authorizes-name')) {
|
|
28
30
|
return { id: 'certificate', label: 'Certificates' };
|
|
29
31
|
}
|
|
@@ -36,12 +38,22 @@ function projectedNodeGroup(node, edges) {
|
|
|
36
38
|
return { id: 'summary', label: 'Grouped evidence' };
|
|
37
39
|
return { id: 'evidence', label: 'Other evidence' };
|
|
38
40
|
}
|
|
39
|
-
function
|
|
41
|
+
function nodeGroups(nodes, edges) {
|
|
42
|
+
const incident = new Map();
|
|
43
|
+
for (const edge of edges) {
|
|
44
|
+
for (const id of [edge.source, edge.target]) {
|
|
45
|
+
const list = incident.get(id) ?? [];
|
|
46
|
+
list.push(edge);
|
|
47
|
+
incident.set(id, list);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return new Map(nodes.map((node) => [node.id, projectedNodeGroup(node, incident.get(node.id) ?? [])]));
|
|
51
|
+
}
|
|
52
|
+
function interleaveVisualEdgesByFamily(edges, groups) {
|
|
40
53
|
const buckets = new Map();
|
|
41
54
|
for (const edge of edges) {
|
|
42
|
-
const endpointGroups = [
|
|
43
|
-
.filter((
|
|
44
|
-
.map((node) => projectedNodeGroup(node, edges).id)
|
|
55
|
+
const endpointGroups = [groups.get(edge.source)?.id, groups.get(edge.target)?.id]
|
|
56
|
+
.filter((group) => Boolean(group))
|
|
45
57
|
.filter((group) => group !== 'focus')
|
|
46
58
|
.sort();
|
|
47
59
|
const family = [...new Set(endpointGroups)].join('+') || 'evidence';
|
|
@@ -75,37 +87,13 @@ function record(value) {
|
|
|
75
87
|
? value
|
|
76
88
|
: {};
|
|
77
89
|
}
|
|
78
|
-
function records(value, maximum = MAX_VALUES) {
|
|
79
|
-
return Array.isArray(value)
|
|
80
|
-
? value.slice(0, maximum).map(record).filter((item) => Object.keys(item).length > 0)
|
|
81
|
-
: [];
|
|
82
|
-
}
|
|
83
90
|
function text(value, maximum = 240) {
|
|
84
|
-
return String(value
|
|
91
|
+
return (typeof value === 'string' ? value.slice(0, maximum * 4) : typeof value === 'number' && Number.isFinite(value) ? String(value) : '')
|
|
85
92
|
.replace(CONTROL_CHARACTERS, ' ')
|
|
86
93
|
.replace(/\s+/gu, ' ')
|
|
87
94
|
.trim()
|
|
88
95
|
.slice(0, maximum);
|
|
89
96
|
}
|
|
90
|
-
function textList(value, maximum = MAX_VALUES) {
|
|
91
|
-
if (!Array.isArray(value))
|
|
92
|
-
return [];
|
|
93
|
-
const output = [];
|
|
94
|
-
const seen = new Set();
|
|
95
|
-
for (const item of value.slice(0, maximum * 2)) {
|
|
96
|
-
const normalized = text(item, 320);
|
|
97
|
-
if (!normalized || seen.has(normalized))
|
|
98
|
-
continue;
|
|
99
|
-
seen.add(normalized);
|
|
100
|
-
output.push(normalized);
|
|
101
|
-
if (output.length >= maximum)
|
|
102
|
-
break;
|
|
103
|
-
}
|
|
104
|
-
return output;
|
|
105
|
-
}
|
|
106
|
-
function limitations(value) {
|
|
107
|
-
return textList(value, MAX_LIMITATIONS).map((item) => text(item, 280));
|
|
108
|
-
}
|
|
109
97
|
function hash(value) {
|
|
110
98
|
let state = 2_166_136_261;
|
|
111
99
|
for (let index = 0; index < value.length; index += 1) {
|
|
@@ -119,10 +107,7 @@ function nodeId(kind, value) {
|
|
|
119
107
|
return `${kind}-${slug || 'value'}-${hash(value)}`;
|
|
120
108
|
}
|
|
121
109
|
function isoDate(value) {
|
|
122
|
-
|
|
123
|
-
return null;
|
|
124
|
-
const timestamp = Date.parse(value);
|
|
125
|
-
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
|
|
110
|
+
return normalizeExplicitIsoTimestamp(value);
|
|
126
111
|
}
|
|
127
112
|
function hostname(value) {
|
|
128
113
|
const candidate = text(value, 320).toLowerCase().replace(/\.$/u, '');
|
|
@@ -161,6 +146,10 @@ function sourceCompleteness(value) {
|
|
|
161
146
|
}
|
|
162
147
|
export function buildLookupAssetGraph(input) {
|
|
163
148
|
const target = hostname(input.target);
|
|
149
|
+
const suppliedRegistrationDomain = hostname(input.registrationDomain);
|
|
150
|
+
const registrationDomain = suppliedRegistrationDomain && target
|
|
151
|
+
&& canonicalRegistrableDomain(target) === suppliedRegistrationDomain
|
|
152
|
+
? suppliedRegistrationDomain : target;
|
|
164
153
|
if (!target) {
|
|
165
154
|
return {
|
|
166
155
|
version: LOOKUP_ASSET_GRAPH_VERSION,
|
|
@@ -168,15 +157,28 @@ export function buildLookupAssetGraph(input) {
|
|
|
168
157
|
nodes: [],
|
|
169
158
|
edges: [],
|
|
170
159
|
sources: [],
|
|
160
|
+
coverage: { inputs: [] },
|
|
171
161
|
truncated: false,
|
|
172
162
|
limitations: ['A normalised domain target is required before an asset graph can be projected.'],
|
|
173
163
|
};
|
|
174
164
|
}
|
|
175
|
-
const
|
|
165
|
+
const reader = createGraphInputReader();
|
|
166
|
+
const textList = (value, id, maximum = MAX_VALUES) => reader.values(id, value, maximum, (item) => {
|
|
167
|
+
if (typeof item !== 'string' || item.length > 320 || item.search(CONTROL_CHARACTERS) >= 0) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
return text(item, 320) || null;
|
|
171
|
+
}, (item) => item);
|
|
172
|
+
const records = (value, id, maximum = MAX_NESTED_VALUES) => reader.values(id, value, maximum, (item) => item && typeof item === 'object' && !Array.isArray(item) ? item : null);
|
|
173
|
+
const limitations = (value, id, mandatory = []) => {
|
|
174
|
+
const optional = textList(value, `${id}.limitations`, MAX_LIMITATIONS - mandatory.length - 1);
|
|
175
|
+
const row = reader.coverage.get(`${id}.limitations`);
|
|
176
|
+
const omitted = row.supplied - row.inspected + row.omitted + row.invalid;
|
|
177
|
+
return [...new Set([...mandatory, ...optional]), ...(omitted ? [`${omitted} additional source limitation entries were not retained; review the source evidence.`] : [])];
|
|
178
|
+
};
|
|
176
179
|
const nodes = new Map();
|
|
177
180
|
const edges = new Map();
|
|
178
181
|
const sources = new Map();
|
|
179
|
-
let truncated = false;
|
|
180
182
|
const targetId = nodeId('target', target);
|
|
181
183
|
nodes.set(targetId, { id: targetId, label: target, kind: 'target', detail: 'Lookup target' });
|
|
182
184
|
const addNode = (kind, rawLabel, detail = '') => {
|
|
@@ -185,11 +187,7 @@ export function buildLookupAssetGraph(input) {
|
|
|
185
187
|
return null;
|
|
186
188
|
const id = nodeId(kind, label);
|
|
187
189
|
if (!nodes.has(id)) {
|
|
188
|
-
|
|
189
|
-
truncated = true;
|
|
190
|
-
return null;
|
|
191
|
-
}
|
|
192
|
-
nodes.set(id, { id, label, kind, detail: text(detail, 180) });
|
|
190
|
+
nodes.set(id, { id, label, kind, detail: text(detail, 500) });
|
|
193
191
|
}
|
|
194
192
|
return id;
|
|
195
193
|
};
|
|
@@ -199,10 +197,6 @@ export function buildLookupAssetGraph(input) {
|
|
|
199
197
|
const id = `${edge.kind}-${hash(`${edge.source}|${edge.target}|${edge.sourceLabel}`)}`;
|
|
200
198
|
if (edges.has(id))
|
|
201
199
|
return;
|
|
202
|
-
if (edges.size >= MAX_EDGES) {
|
|
203
|
-
truncated = true;
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
200
|
const sourceId = `source-${hash(`${edge.sourceLabel}|${edge.href}`)}`;
|
|
207
201
|
const existingSource = sources.get(sourceId);
|
|
208
202
|
const sourceLimitations = [...new Set([
|
|
@@ -218,7 +212,7 @@ export function buildLookupAssetGraph(input) {
|
|
|
218
212
|
id: sourceId,
|
|
219
213
|
label: edge.sourceLabel,
|
|
220
214
|
href: edge.href,
|
|
221
|
-
observedAt: existingSource
|
|
215
|
+
observedAt: existingSource && existingSource.observedAt !== edge.observedAt ? null : edge.observedAt,
|
|
222
216
|
completeness: sourceCompleteness,
|
|
223
217
|
limitations: sourceLimitations,
|
|
224
218
|
});
|
|
@@ -231,51 +225,56 @@ export function buildLookupAssetGraph(input) {
|
|
|
231
225
|
addEdge({ ...edge, source, target: targetNode });
|
|
232
226
|
return targetNode;
|
|
233
227
|
};
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
228
|
+
const registrationId = registrationDomain && registrationDomain !== target
|
|
229
|
+
&& target.endsWith(`.${registrationDomain}`)
|
|
230
|
+
? connect('hostname', registrationDomain, {
|
|
231
|
+
kind: 'registration-namespace', label: 'within registration namespace', sourceLabel: 'Hostname classification',
|
|
232
|
+
observedAt: null, completeness: 'complete', limitations: ['Hostname structure does not establish common ownership or control.'],
|
|
233
|
+
lenses: ['all', 'delegation'], href: '#evidence-registry',
|
|
234
|
+
}, 'Registrable-domain authority') ?? targetId
|
|
235
|
+
: targetId;
|
|
236
|
+
const reviewedProfileDomains = new Set(['official', 'partner', 'allowlisted'].flatMap((key) => reader.values(`profile.${key}`, input.profileDomains?.[key], MAX_PROFILE_VALUES, (value) => typeof value === 'string' && value.length <= 253 ? hostname(value) : null, (value) => value)));
|
|
239
237
|
const trustBoundary = (candidate, sourceHost = target, candidateOrigin = null, sourceOrigin = null) => {
|
|
240
238
|
if (!candidate)
|
|
241
239
|
return 'unresolved';
|
|
242
240
|
if (candidateOrigin && sourceOrigin && candidateOrigin === sourceOrigin)
|
|
243
241
|
return 'same_origin';
|
|
244
|
-
if (candidate === sourceHost || candidate ===
|
|
242
|
+
if (candidate === sourceHost || candidate === registrationDomain || (registrationDomain && candidate.endsWith(`.${registrationDomain}`)))
|
|
245
243
|
return 'same_registrable_domain';
|
|
246
244
|
if (reviewedProfileDomains.has(candidate))
|
|
247
245
|
return 'reviewed_profile';
|
|
246
|
+
if ([...reader.coverage.values()].some((row) => row.id.startsWith('profile.') && graphInputIsIncomplete(row)))
|
|
247
|
+
return 'unresolved';
|
|
248
248
|
return 'external';
|
|
249
249
|
};
|
|
250
250
|
const dnsEvidence = record(input.dnsEvidence);
|
|
251
251
|
const dnsRecords = record(input.dnsRecords);
|
|
252
252
|
const dnsCompleteness = sourceCompleteness(dnsEvidence);
|
|
253
|
-
const dnsLimits = limitations(dnsEvidence.limitations);
|
|
253
|
+
const dnsLimits = limitations(dnsEvidence.limitations, 'dns');
|
|
254
254
|
const dnsEdge = (kind, label, lenses, href = '#evidence-dns') => ({
|
|
255
255
|
kind,
|
|
256
256
|
label,
|
|
257
257
|
sourceLabel: 'DNS',
|
|
258
|
-
observedAt: isoDate(dnsEvidence.observedAt)
|
|
258
|
+
observedAt: isoDate(dnsEvidence.observedAt),
|
|
259
259
|
completeness: dnsCompleteness,
|
|
260
260
|
limitations: dnsLimits,
|
|
261
261
|
lenses,
|
|
262
262
|
href,
|
|
263
263
|
});
|
|
264
|
-
for (const address of [...textList(dnsRecords.a), ...textList(dnsRecords.aaaa)]) {
|
|
264
|
+
for (const address of [...textList(dnsRecords.a, 'dns.a'), ...textList(dnsRecords.aaaa, 'dns.aaaa')]) {
|
|
265
265
|
connect('address', address, dnsEdge('resolves-to', 'resolves to', ['all']));
|
|
266
266
|
}
|
|
267
|
-
for (const alias of textList(dnsRecords.cname)) {
|
|
267
|
+
for (const alias of textList(dnsRecords.cname, 'dns.cname')) {
|
|
268
268
|
connect('hostname', alias, dnsEdge('aliases-to', 'aliases to', ['all']));
|
|
269
269
|
}
|
|
270
|
-
for (const nameserver of textList(dnsRecords.ns)) {
|
|
270
|
+
for (const nameserver of textList(dnsRecords.ns, 'dns.ns')) {
|
|
271
271
|
connect('hostname', nameserver, dnsEdge('uses-nameserver', 'uses nameserver', ['all', 'delegation']));
|
|
272
272
|
}
|
|
273
|
-
for (const
|
|
274
|
-
const host = mxHostname(mx);
|
|
273
|
+
for (const host of reader.values('dns.mx', dnsRecords.mx, MAX_VALUES, mxHostname, (item) => item)) {
|
|
275
274
|
if (host)
|
|
276
275
|
connect('hostname', host, dnsEdge('routes-mail-to', 'routes mail to', ['all']));
|
|
277
276
|
}
|
|
278
|
-
for (const binding of records(dnsRecords.https)) {
|
|
277
|
+
for (const binding of records(dnsRecords.https, 'dns.https', MAX_VALUES)) {
|
|
279
278
|
const serviceTarget = hostname(binding.target);
|
|
280
279
|
if (serviceTarget && serviceTarget !== '.') {
|
|
281
280
|
connect('hostname', serviceTarget, dnsEdge('service-binding', 'publishes HTTPS service target', ['all']), `Priority ${text(binding.priority, 12) || 'unknown'}`);
|
|
@@ -293,65 +292,69 @@ export function buildLookupAssetGraph(input) {
|
|
|
293
292
|
kind: 'registered-via',
|
|
294
293
|
label: 'registered via',
|
|
295
294
|
sourceLabel: 'Registry RDAP',
|
|
296
|
-
observedAt,
|
|
295
|
+
observedAt: isoDate(rdapEvidence.fetchedAt ?? rdapEvidence.observedAt),
|
|
297
296
|
completeness: sourceCompleteness(rdapEvidence),
|
|
298
|
-
limitations: limitations(rdapEvidence.limitations),
|
|
297
|
+
limitations: limitations(rdapEvidence.limitations, 'rdap'),
|
|
299
298
|
lenses: ['all'],
|
|
300
299
|
href: '#evidence-registry',
|
|
301
|
-
}, 'Published registrar of record');
|
|
300
|
+
}, 'Published registrar of record', registrationId);
|
|
302
301
|
}
|
|
303
302
|
const delegation = record(dnsEvidence.delegation);
|
|
304
|
-
const delegationLimits = limitations(delegation.limitations);
|
|
303
|
+
const delegationLimits = limitations(delegation.limitations, 'delegation');
|
|
305
304
|
const registryObservationId = addNode('observation', 'Registry publication', 'Registry nameserver and glue evidence');
|
|
306
|
-
const parentObservationId = addNode('observation', 'Recursive
|
|
307
|
-
for (const nameserver of textList(record(delegation.registry).nameservers)) {
|
|
305
|
+
const parentObservationId = addNode('observation', 'Recursive nameserver observation', 'Point-in-time recursive resolver observation, not a direct parent-server query');
|
|
306
|
+
for (const nameserver of textList(record(delegation.registry).nameservers, 'delegation.registry.nameservers')) {
|
|
308
307
|
const nameserverId = addNode('hostname', nameserver, 'Authoritative nameserver');
|
|
309
308
|
if (registryObservationId && nameserverId) {
|
|
310
309
|
addEdge({
|
|
311
310
|
source: registryObservationId,
|
|
312
311
|
target: nameserverId,
|
|
313
312
|
...dnsEdge('registry-publishes', 'registry publishes', ['delegation']),
|
|
313
|
+
observedAt: isoDate(rdapEvidence.fetchedAt ?? rdapEvidence.observedAt),
|
|
314
314
|
sourceLabel: 'Registry delegation',
|
|
315
315
|
completeness: sourceCompleteness(delegation),
|
|
316
316
|
limitations: delegationLimits,
|
|
317
317
|
});
|
|
318
318
|
}
|
|
319
319
|
}
|
|
320
|
-
for (const nameserver of textList(record(delegation.parent).nameservers)) {
|
|
320
|
+
for (const nameserver of textList(record(delegation.parent).nameservers, 'delegation.parent.nameservers')) {
|
|
321
321
|
const nameserverId = addNode('hostname', nameserver, 'Authoritative nameserver');
|
|
322
322
|
if (parentObservationId && nameserverId) {
|
|
323
323
|
addEdge({
|
|
324
324
|
source: parentObservationId,
|
|
325
325
|
target: nameserverId,
|
|
326
|
-
...dnsEdge('parent-observes', '
|
|
327
|
-
|
|
326
|
+
...dnsEdge('parent-observes', 'recursive resolver observes', ['delegation']),
|
|
327
|
+
observedAt: isoDate(delegation.observedAt),
|
|
328
|
+
sourceLabel: 'Recursive nameserver observation',
|
|
328
329
|
completeness: sourceCompleteness(delegation),
|
|
329
330
|
limitations: delegationLimits,
|
|
330
331
|
});
|
|
331
332
|
}
|
|
332
333
|
}
|
|
333
|
-
for (const authority of records(delegation.authorities)) {
|
|
334
|
+
for (const [authorityIndex, authority] of records(delegation.authorities, 'delegation.authorities').entries()) {
|
|
334
335
|
const nameserver = hostname(authority.nameserver);
|
|
335
336
|
if (!nameserver)
|
|
336
337
|
continue;
|
|
337
338
|
const nameserverId = addNode('hostname', nameserver, `Direct authority state: ${text(authority.state, 40) || 'unknown'}`);
|
|
338
339
|
if (nameserverId) {
|
|
339
340
|
addEdge({
|
|
340
|
-
source:
|
|
341
|
+
source: registrationId,
|
|
341
342
|
target: nameserverId,
|
|
342
343
|
...dnsEdge('direct-authority', 'queried directly', ['delegation']),
|
|
344
|
+
observedAt: isoDate(delegation.observedAt),
|
|
343
345
|
sourceLabel: 'Direct authoritative DNS',
|
|
344
346
|
completeness: authority.state === 'success' ? 'complete' : authority.state ? 'partial' : 'unknown',
|
|
345
347
|
limitations: delegationLimits,
|
|
346
348
|
});
|
|
347
349
|
}
|
|
348
|
-
for (const address of textList(authority.addresses,
|
|
350
|
+
for (const address of textList(authority.addresses, `delegation.authorities.${authorityIndex}.addresses`)) {
|
|
349
351
|
const addressId = addNode('address', address, text(authority.addressSource, 80));
|
|
350
352
|
if (nameserverId && addressId) {
|
|
351
353
|
addEdge({
|
|
352
354
|
source: nameserverId,
|
|
353
355
|
target: addressId,
|
|
354
356
|
...dnsEdge('nameserver-address', 'answered at', ['delegation']),
|
|
357
|
+
observedAt: isoDate(delegation.observedAt),
|
|
355
358
|
sourceLabel: text(authority.addressSource, 80) || 'Nameserver address',
|
|
356
359
|
completeness: authority.state === 'success' ? 'complete' : 'partial',
|
|
357
360
|
limitations: delegationLimits,
|
|
@@ -373,16 +376,16 @@ export function buildLookupAssetGraph(input) {
|
|
|
373
376
|
kind: 'observed-endpoint',
|
|
374
377
|
label: 'was observed at',
|
|
375
378
|
sourceLabel: 'Observed network context',
|
|
376
|
-
observedAt: isoDate(networkContext.observedAt)
|
|
379
|
+
observedAt: isoDate(networkContext.observedAt),
|
|
377
380
|
completeness: sourceCompleteness(networkContext),
|
|
378
|
-
limitations: limitations(networkContext.limitations),
|
|
381
|
+
limitations: limitations(networkContext.limitations, 'network'),
|
|
379
382
|
lenses: ['all'],
|
|
380
383
|
href: '#evidence-network',
|
|
381
384
|
});
|
|
382
385
|
}
|
|
383
386
|
const networkLabel = text(network.name || network.holder || network.handle, 180);
|
|
384
387
|
const networkId = networkLabel
|
|
385
|
-
? addNode('network', networkLabel, textList(network.cidrs,
|
|
388
|
+
? addNode('network', networkLabel, textList(network.cidrs, 'network.cidrs').join(', '))
|
|
386
389
|
: null;
|
|
387
390
|
if (endpointId && networkId) {
|
|
388
391
|
addEdge({
|
|
@@ -391,14 +394,14 @@ export function buildLookupAssetGraph(input) {
|
|
|
391
394
|
kind: 'registered-with',
|
|
392
395
|
label: 'registered within',
|
|
393
396
|
sourceLabel: 'IP RDAP',
|
|
394
|
-
observedAt: isoDate(networkContext.observedAt)
|
|
397
|
+
observedAt: isoDate(networkContext.observedAt),
|
|
395
398
|
completeness: sourceCompleteness(networkContext),
|
|
396
|
-
limitations: limitations(networkContext.limitations),
|
|
399
|
+
limitations: limitations(networkContext.limitations, 'network'),
|
|
397
400
|
lenses: ['all'],
|
|
398
401
|
href: '#evidence-network',
|
|
399
402
|
});
|
|
400
403
|
}
|
|
401
|
-
for (const cidr of textList(network.cidrs,
|
|
404
|
+
for (const cidr of textList(network.cidrs, 'network.cidrs')) {
|
|
402
405
|
const prefixId = addNode('prefix', cidr, 'Published IP RDAP network prefix');
|
|
403
406
|
const prefixSource = networkId || endpointId;
|
|
404
407
|
if (prefixId && prefixSource) {
|
|
@@ -408,9 +411,9 @@ export function buildLookupAssetGraph(input) {
|
|
|
408
411
|
kind: 'publishes-prefix',
|
|
409
412
|
label: 'publishes prefix',
|
|
410
413
|
sourceLabel: 'IP RDAP',
|
|
411
|
-
observedAt: isoDate(networkContext.observedAt)
|
|
414
|
+
observedAt: isoDate(networkContext.observedAt),
|
|
412
415
|
completeness: sourceCompleteness(networkContext),
|
|
413
|
-
limitations: limitations(networkContext.limitations),
|
|
416
|
+
limitations: limitations(networkContext.limitations, 'network'),
|
|
414
417
|
lenses: ['all'],
|
|
415
418
|
href: '#evidence-network',
|
|
416
419
|
});
|
|
@@ -424,24 +427,25 @@ export function buildLookupAssetGraph(input) {
|
|
|
424
427
|
kind: 'redirects-to',
|
|
425
428
|
label: finalHost === target ? 'served from' : 'redirects to',
|
|
426
429
|
sourceLabel: 'HTTP',
|
|
427
|
-
observedAt: isoDate(httpEvidence.observedAt)
|
|
430
|
+
observedAt: isoDate(httpEvidence.observedAt),
|
|
428
431
|
completeness: sourceCompleteness(httpEvidence),
|
|
429
|
-
limitations: limitations(httpEvidence.limitations),
|
|
432
|
+
limitations: limitations(httpEvidence.limitations, 'http'),
|
|
430
433
|
lenses: ['all', 'identity'],
|
|
431
434
|
href: '#evidence-http',
|
|
432
435
|
boundary: trustBoundary(finalHost),
|
|
433
436
|
}, 'Observed final website origin')
|
|
434
437
|
: null;
|
|
435
438
|
const identitySource = finalHostId || targetId;
|
|
439
|
+
const pageObservedAt = isoDate(record(input.pageIdentity).observedAt);
|
|
436
440
|
const canonicalOrigin = webOrigin(record(input.pageCanonical).url);
|
|
437
441
|
if (canonicalOrigin) {
|
|
438
442
|
connect('origin', canonicalOrigin.origin, {
|
|
439
443
|
kind: 'declares-canonical',
|
|
440
444
|
label: 'declares canonical origin',
|
|
441
445
|
sourceLabel: 'HTML canonical metadata',
|
|
442
|
-
observedAt,
|
|
446
|
+
observedAt: pageObservedAt,
|
|
443
447
|
completeness: sourceCompleteness(input.pageIdentity),
|
|
444
|
-
limitations: limitations(record(input.pageIdentity).limitations),
|
|
448
|
+
limitations: limitations(record(input.pageIdentity).limitations, 'page'),
|
|
445
449
|
lenses: ['identity'],
|
|
446
450
|
href: '#evidence-page',
|
|
447
451
|
boundary: trustBoundary(canonicalOrigin.hostname, finalHost || target, canonicalOrigin.origin, finalOrigin?.origin ?? null),
|
|
@@ -453,63 +457,68 @@ export function buildLookupAssetGraph(input) {
|
|
|
453
457
|
kind: 'declares-open-graph',
|
|
454
458
|
label: 'declares Open Graph origin',
|
|
455
459
|
sourceLabel: 'Open Graph metadata',
|
|
456
|
-
observedAt,
|
|
460
|
+
observedAt: pageObservedAt,
|
|
457
461
|
completeness: sourceCompleteness(input.pageIdentity),
|
|
458
|
-
limitations: limitations(record(input.pageIdentity).limitations),
|
|
462
|
+
limitations: limitations(record(input.pageIdentity).limitations, 'page'),
|
|
459
463
|
lenses: ['identity'],
|
|
460
464
|
href: '#evidence-page',
|
|
461
465
|
boundary: trustBoundary(openGraphOrigin.hostname, finalHost || target, openGraphOrigin.origin, finalOrigin?.origin ?? null),
|
|
462
466
|
}, 'Publisher-declared Open Graph origin', identitySource);
|
|
463
467
|
}
|
|
464
|
-
for (const origin of textList(record(input.pageForms).externalActionOrigins)) {
|
|
468
|
+
for (const origin of textList(record(input.pageForms).externalActionOrigins, 'page.formOrigins')) {
|
|
465
469
|
const destination = webOrigin(origin);
|
|
466
470
|
if (destination) {
|
|
467
471
|
connect('origin', destination.origin, {
|
|
468
472
|
kind: 'form-destination',
|
|
469
473
|
label: 'form may submit to',
|
|
470
474
|
sourceLabel: 'Static HTML form',
|
|
471
|
-
observedAt,
|
|
475
|
+
observedAt: pageObservedAt,
|
|
472
476
|
completeness: sourceCompleteness(input.pageIdentity),
|
|
473
|
-
limitations: ['A declared form action does not prove that a user submitted data or that the endpoint received it.'],
|
|
477
|
+
limitations: limitations(record(input.pageIdentity).limitations, 'page.forms', ['A declared form action does not prove that a user submitted data or that the endpoint received it.']),
|
|
474
478
|
lenses: ['identity'],
|
|
475
479
|
href: '#evidence-page',
|
|
476
480
|
boundary: trustBoundary(destination.hostname, finalHost || target, destination.origin, finalOrigin?.origin ?? null),
|
|
477
481
|
}, 'External form-action origin', identitySource);
|
|
478
482
|
}
|
|
479
483
|
}
|
|
480
|
-
for (const origin of textList(record(input.pageResources).externalOrigins)) {
|
|
484
|
+
for (const origin of textList(record(input.pageResources).externalOrigins, 'page.resourceOrigins')) {
|
|
481
485
|
const resource = webOrigin(origin);
|
|
482
486
|
if (resource) {
|
|
483
487
|
connect('origin', resource.origin, {
|
|
484
488
|
kind: 'loads-from',
|
|
485
489
|
label: 'references resources from',
|
|
486
490
|
sourceLabel: 'Static HTML resources',
|
|
487
|
-
observedAt,
|
|
491
|
+
observedAt: pageObservedAt,
|
|
488
492
|
completeness: sourceCompleteness(input.pageIdentity),
|
|
489
|
-
limitations: ['A static resource reference does not prove that a browser loaded the resource or disclosed data to it.'],
|
|
493
|
+
limitations: limitations(record(input.pageIdentity).limitations, 'page.resources', ['A static resource reference does not prove that a browser loaded the resource or disclosed data to it.']),
|
|
490
494
|
lenses: ['identity'],
|
|
491
495
|
href: '#evidence-page',
|
|
492
496
|
boundary: trustBoundary(resource.hostname, finalHost || target, resource.origin, finalOrigin?.origin ?? null),
|
|
493
497
|
}, 'External resource origin', identitySource);
|
|
494
498
|
}
|
|
495
499
|
}
|
|
496
|
-
for (const identifier of records(record(input.pageIdentity).trackingIdentifiers,
|
|
500
|
+
for (const identifier of records(record(input.pageIdentity).trackingIdentifiers, 'page.trackingIdentifiers', MAX_VALUES)) {
|
|
497
501
|
const value = text(identifier.value, 160);
|
|
498
502
|
if (value) {
|
|
499
503
|
connect('tracker', value, {
|
|
500
504
|
kind: 'declares-tracker',
|
|
501
505
|
label: 'declares tracker identifier',
|
|
502
506
|
sourceLabel: 'Static HTML identity',
|
|
503
|
-
observedAt,
|
|
507
|
+
observedAt: pageObservedAt,
|
|
504
508
|
completeness: sourceCompleteness(input.pageIdentity),
|
|
505
|
-
limitations: ['A shared identifier is a relationship lead and does not establish common ownership or control.'],
|
|
509
|
+
limitations: limitations(record(input.pageIdentity).limitations, 'page.trackers', ['A shared identifier is a relationship lead and does not establish common ownership or control.']),
|
|
506
510
|
lenses: ['identity'],
|
|
507
511
|
href: '#evidence-page',
|
|
508
512
|
}, text(identifier.type, 80), identitySource);
|
|
509
513
|
}
|
|
510
514
|
}
|
|
511
515
|
const structuredIdentity = record(input.structuredDataIdentity);
|
|
512
|
-
|
|
516
|
+
const structuredObservedAt = isoDate(structuredIdentity.observedAt);
|
|
517
|
+
const structuredLimits = limitations(structuredIdentity.limitations, 'structured', [
|
|
518
|
+
'Publisher-declared structured metadata is not independent identity verification.',
|
|
519
|
+
'A sameAs declaration is a site-authored claim and does not establish ownership or control.',
|
|
520
|
+
]);
|
|
521
|
+
for (const [entityIndex, entity] of records(structuredIdentity.entities, 'structured.entities').entries()) {
|
|
513
522
|
const entityLabel = text(entity.name, 180);
|
|
514
523
|
const declaredOrigin = webOrigin(entity.declaredOrigin);
|
|
515
524
|
const entityId = entityLabel
|
|
@@ -517,40 +526,34 @@ export function buildLookupAssetGraph(input) {
|
|
|
517
526
|
kind: 'declares-publisher',
|
|
518
527
|
label: 'declares publisher',
|
|
519
528
|
sourceLabel: 'Structured identity metadata',
|
|
520
|
-
observedAt,
|
|
529
|
+
observedAt: structuredObservedAt,
|
|
521
530
|
completeness: sourceCompleteness(structuredIdentity),
|
|
522
|
-
limitations:
|
|
523
|
-
...limitations(structuredIdentity.limitations),
|
|
524
|
-
'Publisher-declared structured metadata is not independent identity verification.',
|
|
525
|
-
].slice(0, MAX_LIMITATIONS),
|
|
531
|
+
limitations: structuredLimits,
|
|
526
532
|
lenses: ['identity'],
|
|
527
533
|
href: '#evidence-structured-identity',
|
|
528
|
-
}, textList(entity.types,
|
|
534
|
+
}, textList(entity.types, `structured.entities.${entityIndex}.types`, MAX_NESTED_VALUES).join(', ') || 'Publisher-declared entity', identitySource)
|
|
529
535
|
: identitySource;
|
|
530
536
|
if (declaredOrigin) {
|
|
531
537
|
connect('origin', declaredOrigin.origin, {
|
|
532
538
|
kind: 'declares-origin',
|
|
533
539
|
label: 'declares origin',
|
|
534
540
|
sourceLabel: 'Structured identity metadata',
|
|
535
|
-
observedAt,
|
|
541
|
+
observedAt: structuredObservedAt,
|
|
536
542
|
completeness: sourceCompleteness(structuredIdentity),
|
|
537
|
-
limitations:
|
|
543
|
+
limitations: structuredLimits,
|
|
538
544
|
lenses: ['identity'],
|
|
539
545
|
href: '#evidence-structured-identity',
|
|
540
546
|
boundary: trustBoundary(declaredOrigin.hostname, finalHost || target, declaredOrigin.origin, finalOrigin?.origin ?? null),
|
|
541
547
|
}, 'Publisher-declared structured-data origin', entityId || identitySource);
|
|
542
548
|
}
|
|
543
|
-
for (const sameAsHost of textList(entity.sameAsHosts,
|
|
549
|
+
for (const sameAsHost of textList(entity.sameAsHosts, `structured.entities.${entityIndex}.sameAsHosts`, MAX_NESTED_VALUES)) {
|
|
544
550
|
connect('origin', sameAsHost, {
|
|
545
551
|
kind: 'declares-same-as',
|
|
546
552
|
label: 'declares sameAs',
|
|
547
553
|
sourceLabel: 'Structured identity metadata',
|
|
548
|
-
observedAt,
|
|
554
|
+
observedAt: structuredObservedAt,
|
|
549
555
|
completeness: sourceCompleteness(structuredIdentity),
|
|
550
|
-
limitations:
|
|
551
|
-
...limitations(structuredIdentity.limitations),
|
|
552
|
-
'A sameAs declaration is a site-authored claim and does not establish ownership or control.',
|
|
553
|
-
].slice(0, MAX_LIMITATIONS),
|
|
556
|
+
limitations: structuredLimits,
|
|
554
557
|
lenses: ['identity'],
|
|
555
558
|
href: '#evidence-structured-identity',
|
|
556
559
|
boundary: trustBoundary(hostname(sameAsHost), finalHost || target),
|
|
@@ -572,9 +575,9 @@ export function buildLookupAssetGraph(input) {
|
|
|
572
575
|
kind: 'presents-certificate',
|
|
573
576
|
label: 'presented certificate',
|
|
574
577
|
sourceLabel: 'TLS',
|
|
575
|
-
observedAt: isoDate(tlsEvidence.observedAt)
|
|
578
|
+
observedAt: isoDate(tlsEvidence.observedAt),
|
|
576
579
|
completeness: sourceCompleteness(tlsEvidence),
|
|
577
|
-
limitations: limitations(tlsEvidence.limitations),
|
|
580
|
+
limitations: limitations(tlsEvidence.limitations, 'tls'),
|
|
578
581
|
lenses: ['all', 'certificate'],
|
|
579
582
|
href: '#evidence-tls',
|
|
580
583
|
}, certificateDetail)
|
|
@@ -592,9 +595,9 @@ export function buildLookupAssetGraph(input) {
|
|
|
592
595
|
kind: 'reviewed-hostname-match',
|
|
593
596
|
label: 'reviewed hostname match',
|
|
594
597
|
sourceLabel: 'TLS hostname verification',
|
|
595
|
-
observedAt: isoDate(tlsEvidence.observedAt)
|
|
598
|
+
observedAt: isoDate(tlsEvidence.observedAt),
|
|
596
599
|
completeness: sourceCompleteness(tlsEvidence),
|
|
597
|
-
limitations: limitations(tlsEvidence.limitations),
|
|
600
|
+
limitations: limitations(tlsEvidence.limitations, 'tls'),
|
|
598
601
|
lenses: ['certificate', 'identity'],
|
|
599
602
|
href: '#evidence-tls',
|
|
600
603
|
});
|
|
@@ -604,9 +607,9 @@ export function buildLookupAssetGraph(input) {
|
|
|
604
607
|
kind: 'evaluated-certificate',
|
|
605
608
|
label: 'evaluated certificate',
|
|
606
609
|
sourceLabel: 'TLS hostname verification',
|
|
607
|
-
observedAt: isoDate(tlsEvidence.observedAt)
|
|
610
|
+
observedAt: isoDate(tlsEvidence.observedAt),
|
|
608
611
|
completeness: sourceCompleteness(tlsEvidence),
|
|
609
|
-
limitations: limitations(tlsEvidence.limitations),
|
|
612
|
+
limitations: limitations(tlsEvidence.limitations, 'tls'),
|
|
610
613
|
lenses: ['certificate'],
|
|
611
614
|
href: '#evidence-tls',
|
|
612
615
|
});
|
|
@@ -624,15 +627,15 @@ export function buildLookupAssetGraph(input) {
|
|
|
624
627
|
kind: 'reviewed-runtime-trust',
|
|
625
628
|
label: 'reviewed runtime trust',
|
|
626
629
|
sourceLabel: 'TLS chain verification',
|
|
627
|
-
observedAt: isoDate(tlsEvidence.observedAt)
|
|
630
|
+
observedAt: isoDate(tlsEvidence.observedAt),
|
|
628
631
|
completeness: sourceCompleteness(tlsEvidence),
|
|
629
|
-
limitations: limitations(tlsEvidence.limitations),
|
|
632
|
+
limitations: limitations(tlsEvidence.limitations, 'tls'),
|
|
630
633
|
lenses: ['certificate'],
|
|
631
634
|
href: '#evidence-tls',
|
|
632
635
|
});
|
|
633
636
|
}
|
|
634
637
|
}
|
|
635
|
-
for (const name of textList(record(input.tlsAltNames).dnsNames,
|
|
638
|
+
for (const name of textList(record(input.tlsAltNames).dnsNames, 'tls.dnsNames')) {
|
|
636
639
|
const sanId = addNode('hostname', name, 'Certificate DNS subject alternative name');
|
|
637
640
|
if (sanId) {
|
|
638
641
|
addEdge({
|
|
@@ -641,9 +644,9 @@ export function buildLookupAssetGraph(input) {
|
|
|
641
644
|
kind: 'authorizes-name',
|
|
642
645
|
label: 'contains SAN',
|
|
643
646
|
sourceLabel: 'TLS certificate',
|
|
644
|
-
observedAt: isoDate(tlsEvidence.observedAt)
|
|
647
|
+
observedAt: isoDate(tlsEvidence.observedAt),
|
|
645
648
|
completeness: sourceCompleteness(tlsEvidence),
|
|
646
|
-
limitations: limitations(tlsEvidence.limitations),
|
|
649
|
+
limitations: limitations(tlsEvidence.limitations, 'tls'),
|
|
647
650
|
lenses: ['certificate', 'identity'],
|
|
648
651
|
href: '#evidence-tls',
|
|
649
652
|
boundary: trustBoundary(hostname(name)),
|
|
@@ -661,38 +664,48 @@ export function buildLookupAssetGraph(input) {
|
|
|
661
664
|
kind: 'uses-key',
|
|
662
665
|
label: 'uses public key',
|
|
663
666
|
sourceLabel: 'TLS certificate',
|
|
664
|
-
observedAt: isoDate(tlsEvidence.observedAt)
|
|
667
|
+
observedAt: isoDate(tlsEvidence.observedAt),
|
|
665
668
|
completeness: sourceCompleteness(tlsEvidence),
|
|
666
|
-
limitations: limitations(tlsEvidence.limitations),
|
|
669
|
+
limitations: limitations(tlsEvidence.limitations, 'tls'),
|
|
667
670
|
lenses: ['certificate'],
|
|
668
671
|
href: '#evidence-tls',
|
|
669
672
|
});
|
|
670
673
|
}
|
|
671
674
|
}
|
|
672
675
|
const issuer = record(input.tlsIssuer);
|
|
673
|
-
const issuerOrganizations = textList(issuer.organizations,
|
|
674
|
-
const issuerCommonNames = textList(issuer.commonNames,
|
|
676
|
+
const issuerOrganizations = textList(issuer.organizations, 'tls.issuer.organizations');
|
|
677
|
+
const issuerCommonNames = textList(issuer.commonNames, 'tls.issuer.commonNames');
|
|
675
678
|
const issuerLabel = issuerOrganizations[0]
|
|
676
679
|
|| issuerCommonNames[0]
|
|
677
680
|
|| text(issuer.organization || issuer.commonName || issuer.CN, 180);
|
|
678
681
|
if (issuerLabel) {
|
|
679
682
|
const issuerId = addNode('issuer', issuerLabel, 'Certificate issuer');
|
|
680
683
|
if (issuerId) {
|
|
684
|
+
for (const [attribute, values] of [['organization', issuerOrganizations], ['common-name', issuerCommonNames]]) {
|
|
685
|
+
for (const value of values) {
|
|
686
|
+
connect('identity', value, {
|
|
687
|
+
kind: `issuer-${attribute}`, label: `declares issuer ${attribute === 'organization' ? 'organisation' : 'common name'}`,
|
|
688
|
+
sourceLabel: 'TLS certificate', observedAt: isoDate(tlsEvidence.observedAt),
|
|
689
|
+
completeness: sourceCompleteness(tlsEvidence), limitations: limitations(tlsEvidence.limitations, 'tls'),
|
|
690
|
+
lenses: ['certificate'], href: '#evidence-tls',
|
|
691
|
+
}, 'Certificate issuer distinguished-name attribute', issuerId);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
681
694
|
addEdge({
|
|
682
695
|
source: certificateId,
|
|
683
696
|
target: issuerId,
|
|
684
697
|
kind: 'issued-by',
|
|
685
698
|
label: 'issued by',
|
|
686
699
|
sourceLabel: 'TLS certificate',
|
|
687
|
-
observedAt: isoDate(tlsEvidence.observedAt)
|
|
700
|
+
observedAt: isoDate(tlsEvidence.observedAt),
|
|
688
701
|
completeness: sourceCompleteness(tlsEvidence),
|
|
689
|
-
limitations: limitations(tlsEvidence.limitations),
|
|
702
|
+
limitations: limitations(tlsEvidence.limitations, 'tls'),
|
|
690
703
|
lenses: ['certificate'],
|
|
691
704
|
href: '#evidence-tls',
|
|
692
705
|
});
|
|
693
706
|
}
|
|
694
707
|
}
|
|
695
|
-
for (const finding of records(record(input.certificatePolicyReview).findings,
|
|
708
|
+
for (const [findingIndex, finding] of records(record(input.certificatePolicyReview).findings, 'certificatePolicy.findings').entries()) {
|
|
696
709
|
const findingId = text(finding.id, 80);
|
|
697
710
|
const state = text(finding.state, 80);
|
|
698
711
|
if (!findingId || state === 'not_configured')
|
|
@@ -705,21 +718,27 @@ export function buildLookupAssetGraph(input) {
|
|
|
705
718
|
kind: 'reviewed-against-policy',
|
|
706
719
|
label: 'reviewed against',
|
|
707
720
|
sourceLabel: 'DNS / TLS / Brand Profile',
|
|
708
|
-
observedAt: isoDate(record(input.certificatePolicyReview).observedAt)
|
|
721
|
+
observedAt: isoDate(record(input.certificatePolicyReview).observedAt),
|
|
709
722
|
completeness: state === 'indeterminate' || state === 'no_target_policy_observed' ? 'partial' : 'complete',
|
|
710
|
-
limitations: limitations(finding.limitations),
|
|
723
|
+
limitations: limitations(finding.limitations, `certificatePolicy.${findingIndex}`),
|
|
711
724
|
lenses: ['certificate'],
|
|
712
725
|
href: '#evidence-certificate-policy',
|
|
713
726
|
});
|
|
714
727
|
}
|
|
715
728
|
}
|
|
716
729
|
}
|
|
730
|
+
const retainedEdges = [...edges.values()];
|
|
731
|
+
const connectedNodeIds = new Set([targetId, ...[...edges.values()].flatMap((edge) => [edge.source, edge.target])]);
|
|
732
|
+
const retainedSourceIds = new Set(retainedEdges.map((edge) => edge.sourceId));
|
|
733
|
+
const inputCoverage = [...reader.coverage.values()];
|
|
734
|
+
const truncated = inputCoverage.some(graphInputIsIncomplete);
|
|
717
735
|
return {
|
|
718
736
|
version: LOOKUP_ASSET_GRAPH_VERSION,
|
|
719
737
|
targetId,
|
|
720
|
-
nodes: [...nodes.values()],
|
|
721
|
-
edges:
|
|
722
|
-
sources: [...sources.values()].sort((left, right) => left.label.localeCompare(right.label)),
|
|
738
|
+
nodes: [...nodes.values()].filter((node) => connectedNodeIds.has(node.id)),
|
|
739
|
+
edges: retainedEdges,
|
|
740
|
+
sources: [...sources.values()].filter((source) => retainedSourceIds.has(source.id)).sort((left, right) => left.label.localeCompare(right.label)),
|
|
741
|
+
coverage: { inputs: inputCoverage },
|
|
723
742
|
truncated,
|
|
724
743
|
limitations: [
|
|
725
744
|
'The graph contains only settled evidence from this Lookup and does not start additional requests.',
|
|
@@ -740,10 +759,11 @@ export function countLookupAssetGraphEdgesByLens(graph) {
|
|
|
740
759
|
export function projectLookupAssetGraph(graph, lens) {
|
|
741
760
|
const acceptedEdges = graph.edges.filter((edge) => edgeMatchesLens(edge, lens));
|
|
742
761
|
const graphNodeById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
762
|
+
const groups = nodeGroups(graph.nodes, acceptedEdges);
|
|
743
763
|
const visualEdges = [];
|
|
744
764
|
const visualDegree = new Map();
|
|
745
765
|
const omittedByHub = new Map();
|
|
746
|
-
for (const edge of interleaveVisualEdgesByFamily(acceptedEdges,
|
|
766
|
+
for (const edge of interleaveVisualEdgesByFamily(acceptedEdges, groups)) {
|
|
747
767
|
const sourceDegree = visualDegree.get(edge.source) ?? 0;
|
|
748
768
|
const targetDegree = visualDegree.get(edge.target) ?? 0;
|
|
749
769
|
const saturatedHub = sourceDegree >= MAX_VISUAL_EDGES_PER_HUB
|
|
@@ -772,7 +792,7 @@ export function projectLookupAssetGraph(graph, lens) {
|
|
|
772
792
|
const nodes = graph.nodes
|
|
773
793
|
.filter((node) => nodeIds.has(node.id))
|
|
774
794
|
.map((node) => {
|
|
775
|
-
const group =
|
|
795
|
+
const group = groups.get(node.id);
|
|
776
796
|
return {
|
|
777
797
|
id: node.id,
|
|
778
798
|
label: node.label,
|