@slicedearth/whoisleuth-cli 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +6 -0
- package/README.md +20 -15
- package/bin/whoisleuth.mjs +8 -1
- package/cli/archive-content-digest.mjs +14 -0
- package/cli/archive-inspect.mjs +20 -53
- package/cli/arguments.mjs +454 -1748
- package/cli/artifact-validation/case-response.mjs +66 -35
- package/cli/artifact-validation/investigation-capsule.mjs +52 -31
- package/cli/artifact-validation/lookup-evidence.mjs +12 -6
- package/cli/artifact-validation/signed-review.mjs +112 -72
- package/cli/artifact-validation/structure-primitives.mjs +1 -105
- package/cli/artifact-verify.mjs +90 -24
- package/cli/assurance-command-runner.mjs +39 -4
- package/cli/bagit-folder.mjs +57 -0
- package/cli/bagit-review.mjs +23 -0
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +34 -3
- package/cli/bulk-command-runner.mjs +2 -0
- package/cli/bulk-output.mjs +29 -3
- package/cli/case-command.mjs +195 -0
- package/cli/case-pack.mjs +66 -111
- package/cli/command-argument-grammar.mjs +215 -0
- package/cli/command-reference.mjs +342 -285
- package/cli/completion.mjs +303 -80
- package/cli/config-profile.mjs +7 -5
- package/cli/ct-event-intake.mjs +12 -12
- package/cli/discover.mjs +4 -3
- package/cli/discovery-observation-snapshot.mjs +59 -18
- package/cli/discovery-scan-command-runner.mjs +21 -0
- package/cli/discovery-scan.mjs +13 -4
- package/cli/discovery-workflow.mjs +1 -1
- package/cli/domain-control-monitor.mjs +50 -20
- package/cli/domain-control-observations.mjs +149 -102
- package/cli/errors.mjs +33 -1
- package/cli/evidence-command-runner.mjs +16 -5
- package/cli/evidence-signing.mjs +2 -2
- package/cli/external-observation-mapping.mjs +7 -8
- package/cli/formatters/terminal-lookup.mjs +18 -46
- package/cli/formatters/terminal-shared.mjs +5 -3
- package/cli/formatters/terminal-technology.mjs +114 -0
- package/cli/inline-command-families.mjs +2 -0
- package/cli/input.mjs +116 -0
- package/cli/interactive-launcher.mjs +2 -160
- package/cli/interchange-report.mjs +18 -8
- package/cli/investigation-artifacts.mjs +176 -0
- package/cli/investigation-checkpoint.mjs +10 -0
- package/cli/investigation-folder.mjs +114 -0
- package/cli/investigation-manifest.mjs +1 -112
- package/cli/investigation-package-review.mjs +103 -0
- package/cli/investigation-plan.mjs +32 -19
- package/cli/investigation-run.mjs +188 -33
- package/cli/local-document-checkpoint.mjs +170 -0
- package/cli/lookalike-calibration.mjs +7 -2
- package/cli/lookup-brief.mjs +4 -2
- package/cli/lookup-browser-view.mjs +1 -1
- package/cli/lookup-browser.mjs +5 -12
- package/cli/lookup-command-runner.mjs +17 -6
- package/cli/lookup-diff.mjs +37 -3
- package/cli/lookup-plan.mjs +11 -5
- package/cli/lookup-reconcile.mjs +10 -8
- package/cli/mail-header-review.mjs +467 -0
- package/cli/mail-review.mjs +66 -14
- package/cli/manual.mjs +1 -0
- package/cli/network-command-runner.mjs +2 -0
- package/cli/output-file.mjs +31 -5
- package/cli/registry-doctor.mjs +3 -1
- package/cli/registry-support.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +2 -2
- package/cli/review-command-runner.mjs +70 -0
- package/cli/risk-calibration.mjs +19 -17
- package/cli/runner.mjs +62 -7
- package/cli/sharing-review.mjs +21 -34
- package/cli/signer-trust.mjs +104 -0
- package/cli/source-reliability.mjs +74 -83
- package/cli/terminal-input.mjs +168 -0
- package/cli/workflow-command-runner.mjs +68 -13
- package/docs/cli-reference.md +77 -24
- package/docs/cli.md +300 -25
- package/lib/abort.mjs +4 -1
- package/lib/availability.mjs +161 -112
- package/lib/bounded-file.mjs +3 -1
- package/lib/bounded-html-document.mjs +125 -0
- package/lib/bounded-json.mjs +51 -8
- package/lib/browser-library-profile.mjs +42 -51
- package/lib/browser-library-worker.mjs +112 -0
- package/lib/client-behavior-profile.mjs +3 -3
- package/lib/ct-search.mjs +11 -4
- package/lib/dns-convergence-review.mjs +1 -0
- package/lib/dns-delegation-health.mjs +24 -14
- package/lib/dns-inheritance-review.mjs +217 -0
- package/lib/dns-intelligence.mjs +73 -26
- package/lib/dns-mx.mjs +1 -1
- package/lib/dnssec-chain-validation.mjs +6 -0
- package/lib/dnssec-evidence-validation.mjs +11 -3
- package/lib/domain-change-packet.mjs +49 -16
- package/lib/domain-change-review.mjs +14 -8
- package/lib/domain-control-flight-recorder.mjs +221 -117
- package/lib/domain-control-manifest.mjs +129 -69
- package/lib/domain-posture-analysis.mjs +20 -7
- package/lib/domain-posture-parsers.mjs +201 -8
- package/lib/domain-posture.mjs +147 -29
- package/lib/evidence-export-privacy.mjs +561 -0
- package/lib/evidence-export.mjs +11 -543
- package/lib/evidence-report.mjs +5 -3
- package/lib/favicon.mjs +117 -41
- package/lib/generated/cisa-kev-catalog.mjs +12 -4
- package/lib/generated/registrar-standing-catalogue.mjs +7 -7
- package/lib/generated/retire-browser-catalog.mjs +2335 -940
- package/lib/hostname.mjs +2 -38
- package/lib/html-signals.mjs +111 -108
- package/lib/http-evidence-bounds.mjs +2 -1
- package/lib/http-intelligence.mjs +39 -26
- package/lib/local-geoip-evidence.mjs +19 -3
- package/lib/lookup-cache.mjs +34 -11
- package/lib/lookup-child-profile-contract.mjs +24 -15
- package/lib/lookup-diagnostics.mjs +24 -1
- package/lib/lookup-progress.mjs +30 -13
- package/lib/lookup-response-contract.mjs +1048 -0
- package/lib/lookup-response.mjs +8 -10
- package/lib/lookup-source-progress.mjs +40 -14
- package/lib/lookup.mjs +48 -27
- package/lib/nameserver-preflight-review.mjs +1 -0
- package/lib/observed-network-context.mjs +43 -27
- package/lib/outbound-request-bounds.mjs +4 -1
- package/lib/page-fingerprints.mjs +69 -133
- package/lib/page-role-profile.mjs +12 -8
- package/lib/perceptual-hash.mjs +43 -14
- package/lib/rdap-bootstrap.mjs +72 -22
- package/lib/rdap-capabilities.mjs +1 -1
- package/lib/rdap-client.mjs +15 -7
- package/lib/rdap-normalization.mjs +33 -18
- package/lib/rdap-search-workbench.mjs +1 -1
- package/lib/rdap-transport.mjs +47 -20
- package/lib/rdap-types.mjs +2 -1
- package/lib/rdap.mjs +18 -3
- package/lib/registrable-domain.mjs +21 -1
- package/lib/registrar-standing-contract.mjs +1 -0
- package/lib/registry-capabilities.mjs +5 -1
- package/lib/registry-capability-catalogue.mjs +2 -1
- package/lib/registry-dates.mjs +2 -190
- package/lib/registry-insights.mjs +85 -34
- package/lib/response-policy.mjs +86 -48
- package/lib/risk-calibration-summary.mjs +18 -6
- package/lib/rpki-evidence.mjs +8 -5
- package/lib/safe-fetch.mjs +54 -47
- package/lib/security-txt.mjs +42 -7
- package/lib/service-binding-dns.mjs +8 -0
- package/lib/smtp-transport-review.mjs +27 -15
- package/lib/sslbl-certificates.generated.mjs +136 -135
- package/lib/static-html-analysis.mjs +178 -151
- package/lib/structured-data-identity.mjs +1 -0
- package/lib/technology-markup.mjs +100 -0
- package/lib/threat-intelligence-manifests.mjs +11 -139
- package/lib/threat-intelligence-runtime.mjs +24 -52
- package/lib/threat-intelligence-targets.mjs +6 -34
- package/lib/threat-intelligence-types.mjs +39 -0
- package/lib/typosquat-generator.mjs +19 -15
- package/lib/urlhaus-intelligence.mjs +8 -3
- package/lib/urlscan-intelligence.mjs +1 -1
- package/lib/website-security-posture.mjs +30 -6
- package/lib/website-technology.mjs +153 -45
- package/lib/whois-authority.mjs +14 -2
- package/lib/whois-chain.mjs +9 -2
- package/lib/whois-common-formats.mjs +5 -5
- package/lib/whois-contracts.mjs +2 -1
- package/lib/whois-dialects.mjs +2 -2
- package/lib/whois-parser-support.mjs +8 -1
- package/lib/whois-transport.mjs +23 -7
- package/lib/zone-intent-review.mjs +16 -6
- package/package.json +2 -1
- package/packages/cases/case-attachment-model.mjs +83 -0
- package/packages/cases/case-evidence-model.mjs +156 -62
- package/packages/cases/case-export-input.mjs +53 -0
- package/packages/cases/case-migration-model.mjs +127 -44
- package/packages/cases/case-model.mjs +1 -0
- package/packages/cases/case-recheck-model.mjs +91 -0
- package/packages/cases/case-record-contracts.mjs +5 -20
- package/packages/cases/case-record-core.mjs +12 -22
- package/packages/cases/case-record-decisions.mjs +72 -0
- package/packages/cases/case-record-model.mjs +1 -0
- package/packages/cases/case-record-operations.mjs +95 -31
- package/packages/cases/case-record-projection.mjs +221 -0
- package/packages/cases/case-report.mjs +70 -54
- package/packages/cases/case-response-actions.mjs +654 -0
- package/packages/cases/case-response-model.mjs +64 -1115
- package/packages/cases/case-response-outcomes.mjs +317 -0
- package/packages/cases/case-response-packet-render.mjs +153 -0
- package/packages/cases/case-response-packet-types.mjs +1 -0
- package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
- package/packages/cases/case-response-packet.mjs +74 -572
- package/packages/cases/case-response-records.mjs +94 -0
- package/packages/cases/case-response-review-inputs.mjs +371 -0
- package/packages/cases/case-response-values.mjs +85 -0
- package/packages/cases/case-review-package.mjs +45 -0
- package/packages/cases/case-selection.mjs +46 -0
- package/packages/cases/case-storage-model.mjs +31 -2
- package/packages/cases/response-route-freshness.mjs +16 -0
- package/packages/comparison/capture-context.mjs +61 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +13 -11
- package/packages/comparison/page-similarity.mjs +5 -2
- package/packages/comparison/relationship-evidence.mjs +85 -8
- package/packages/comparison/relationship-provenance.mjs +93 -0
- package/packages/contracts/analyst-interchange.mjs +216 -0
- package/packages/contracts/analyst-review-state-contract.mjs +11 -0
- package/packages/contracts/analyst-review-state.mjs +5 -4
- package/packages/contracts/case-portability.mjs +366 -83
- package/packages/contracts/case-views-contract.mjs +12 -0
- package/packages/contracts/case-views.mjs +26 -0
- package/packages/contracts/cli-command-semantics.mjs +94 -0
- package/packages/contracts/cli-lookup.mjs +0 -1
- package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
- package/packages/contracts/domain-control-manifest.mjs +127 -73
- package/packages/contracts/domain-control-monitor.mjs +48 -45
- package/packages/contracts/domain-control-review.mjs +179 -121
- package/packages/contracts/domain-name.mjs +21 -0
- package/packages/contracts/external-observation-interchange.mjs +2 -1
- package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
- package/packages/contracts/http-url.mjs +2 -0
- package/packages/contracts/investigation-package-limits.mjs +24 -0
- package/packages/contracts/investigation-portability.mjs +56 -27
- package/packages/contracts/investigation-run.mjs +16 -0
- package/packages/contracts/ip-address.mjs +34 -0
- package/packages/contracts/lookup-evidence.mjs +35 -0
- package/packages/contracts/offline-comparison.mjs +1 -2
- package/packages/contracts/page-fingerprints.mjs +15 -0
- package/packages/contracts/risk-calibration.mjs +15 -11
- package/packages/contracts/schema-compatibility.mjs +1 -1
- package/packages/contracts/schema-lifecycle.mjs +83 -153
- package/packages/contracts/selected-file-limits.mjs +6 -0
- package/packages/contracts/vulnerability-identifiers.mjs +5 -0
- package/packages/contracts/web-capture.mjs +10 -0
- package/packages/contracts/workspace-portability.mjs +183 -211
- package/packages/evidence/artifact-integrity.mjs +8 -2
- package/packages/evidence/artifact-structure.mjs +105 -0
- package/packages/evidence/decision-fact.mjs +4 -1
- package/packages/evidence/domain-control-runtime.mjs +201 -33
- package/packages/evidence/domain-posture-context.mjs +79 -0
- package/packages/evidence/image-regions.mjs +56 -0
- package/packages/evidence/latest-observations.mjs +28 -0
- package/packages/evidence/lookup-target.mjs +104 -0
- package/packages/evidence/observation.mjs +16 -1
- package/packages/evidence/passphrase-encryption.mjs +44 -0
- package/packages/evidence/record-identity.mjs +72 -0
- package/packages/evidence/registry-dates.mjs +190 -0
- package/packages/evidence/retained-file.mjs +69 -0
- package/packages/evidence/review-age.mjs +14 -0
- package/packages/interchange/bagit.mjs +342 -0
- package/packages/interchange/bounded-zip-extraction.mjs +278 -0
- package/packages/interchange/crc32.mjs +17 -0
- package/packages/interchange/external-findings-import.mjs +92 -51
- package/packages/interchange/web-capture-import.mjs +382 -0
- package/packages/investigation/bulk-mail-exposure.mjs +2 -1
- package/packages/investigation/investigation-bagit.mjs +14 -0
- package/packages/investigation/investigation-capsule.mjs +33 -5
- package/packages/investigation/investigation-manifest.mjs +200 -0
- package/packages/investigation/investigation-package-crypto.mjs +98 -0
- package/packages/investigation/investigation-package.mjs +200 -0
- package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
- package/packages/investigation/lookup-asset-graph.mjs +193 -165
- package/packages/investigation/lookup-claim-readiness.mjs +18 -0
- package/packages/investigation/lookup-investigation-brief.mjs +3 -3
- package/packages/investigation/lookup-task-guidance.mjs +58 -0
- package/packages/monitoring/analyst-review-state.mjs +38 -90
- package/packages/workspace/brand-profile-model.mjs +276 -22
- package/packages/workspace/bulk-columns.mjs +17 -0
- package/packages/workspace/bulk-review-model.mjs +23 -11
- package/packages/workspace/bulk-session-model.mjs +100 -24
- package/packages/workspace/bulk-sort.mjs +1 -0
- package/packages/workspace/case-views.mjs +102 -0
- package/packages/workspace/detection-rule-model.mjs +70 -22
- package/packages/workspace/hostile-input.mjs +29 -8
- package/packages/workspace/investigation-template-model.mjs +19 -4
- package/packages/workspace/page-baseline.mjs +18 -15
- package/packages/workspace/relationship-observation-model.mjs +41 -23
- package/packages/workspace/watchlist-history.mjs +2 -2
- package/packages/workspace/watchlist-store.mjs +8 -2
- package/packages/workspace/website-snapshot-model.mjs +164 -26
- package/packages/workspace/workspace-archive-crypto.mjs +41 -61
- package/packages/workspace/workspace-archive.mjs +87 -21
- package/third-party-notices.txt +29 -2
- package/cli/evidence-command-arguments.mjs +0 -156
- package/packages/investigation/bulk-review-export.mjs +0 -63
|
@@ -1,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 "../../lib/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, '');
|
|
@@ -130,13 +115,16 @@ function hostname(value) {
|
|
|
130
115
|
return null;
|
|
131
116
|
return candidate;
|
|
132
117
|
}
|
|
133
|
-
function
|
|
118
|
+
function webOrigin(value) {
|
|
134
119
|
const candidate = text(value, 2_048);
|
|
135
120
|
if (!candidate)
|
|
136
121
|
return null;
|
|
137
122
|
try {
|
|
138
|
-
|
|
139
|
-
|
|
123
|
+
const parsed = new URL(/^[a-z][a-z\d+.-]*:\/\//iu.test(candidate) ? candidate : `https://${candidate}`);
|
|
124
|
+
const parsedHostname = hostname(parsed.hostname);
|
|
125
|
+
return parsedHostname && parsed.origin !== 'null'
|
|
126
|
+
? Object.freeze({ origin: parsed.origin, hostname: parsedHostname })
|
|
127
|
+
: null;
|
|
140
128
|
}
|
|
141
129
|
catch {
|
|
142
130
|
return null;
|
|
@@ -158,6 +146,10 @@ function sourceCompleteness(value) {
|
|
|
158
146
|
}
|
|
159
147
|
export function buildLookupAssetGraph(input) {
|
|
160
148
|
const target = hostname(input.target);
|
|
149
|
+
const suppliedRegistrationDomain = hostname(input.registrationDomain);
|
|
150
|
+
const registrationDomain = suppliedRegistrationDomain && target
|
|
151
|
+
&& canonicalRegistrableDomain(target) === suppliedRegistrationDomain
|
|
152
|
+
? suppliedRegistrationDomain : target;
|
|
161
153
|
if (!target) {
|
|
162
154
|
return {
|
|
163
155
|
version: LOOKUP_ASSET_GRAPH_VERSION,
|
|
@@ -165,15 +157,28 @@ export function buildLookupAssetGraph(input) {
|
|
|
165
157
|
nodes: [],
|
|
166
158
|
edges: [],
|
|
167
159
|
sources: [],
|
|
160
|
+
coverage: { inputs: [] },
|
|
168
161
|
truncated: false,
|
|
169
162
|
limitations: ['A normalised domain target is required before an asset graph can be projected.'],
|
|
170
163
|
};
|
|
171
164
|
}
|
|
172
|
-
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
|
+
};
|
|
173
179
|
const nodes = new Map();
|
|
174
180
|
const edges = new Map();
|
|
175
181
|
const sources = new Map();
|
|
176
|
-
let truncated = false;
|
|
177
182
|
const targetId = nodeId('target', target);
|
|
178
183
|
nodes.set(targetId, { id: targetId, label: target, kind: 'target', detail: 'Lookup target' });
|
|
179
184
|
const addNode = (kind, rawLabel, detail = '') => {
|
|
@@ -182,11 +187,7 @@ export function buildLookupAssetGraph(input) {
|
|
|
182
187
|
return null;
|
|
183
188
|
const id = nodeId(kind, label);
|
|
184
189
|
if (!nodes.has(id)) {
|
|
185
|
-
|
|
186
|
-
truncated = true;
|
|
187
|
-
return null;
|
|
188
|
-
}
|
|
189
|
-
nodes.set(id, { id, label, kind, detail: text(detail, 180) });
|
|
190
|
+
nodes.set(id, { id, label, kind, detail: text(detail, 500) });
|
|
190
191
|
}
|
|
191
192
|
return id;
|
|
192
193
|
};
|
|
@@ -196,10 +197,6 @@ export function buildLookupAssetGraph(input) {
|
|
|
196
197
|
const id = `${edge.kind}-${hash(`${edge.source}|${edge.target}|${edge.sourceLabel}`)}`;
|
|
197
198
|
if (edges.has(id))
|
|
198
199
|
return;
|
|
199
|
-
if (edges.size >= MAX_EDGES) {
|
|
200
|
-
truncated = true;
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
200
|
const sourceId = `source-${hash(`${edge.sourceLabel}|${edge.href}`)}`;
|
|
204
201
|
const existingSource = sources.get(sourceId);
|
|
205
202
|
const sourceLimitations = [...new Set([
|
|
@@ -215,7 +212,7 @@ export function buildLookupAssetGraph(input) {
|
|
|
215
212
|
id: sourceId,
|
|
216
213
|
label: edge.sourceLabel,
|
|
217
214
|
href: edge.href,
|
|
218
|
-
observedAt: existingSource
|
|
215
|
+
observedAt: existingSource && existingSource.observedAt !== edge.observedAt ? null : edge.observedAt,
|
|
219
216
|
completeness: sourceCompleteness,
|
|
220
217
|
limitations: sourceLimitations,
|
|
221
218
|
});
|
|
@@ -228,51 +225,56 @@ export function buildLookupAssetGraph(input) {
|
|
|
228
225
|
addEdge({ ...edge, source, target: targetNode });
|
|
229
226
|
return targetNode;
|
|
230
227
|
};
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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)));
|
|
237
|
+
const trustBoundary = (candidate, sourceHost = target, candidateOrigin = null, sourceOrigin = null) => {
|
|
237
238
|
if (!candidate)
|
|
238
239
|
return 'unresolved';
|
|
239
|
-
if (
|
|
240
|
+
if (candidateOrigin && sourceOrigin && candidateOrigin === sourceOrigin)
|
|
240
241
|
return 'same_origin';
|
|
241
|
-
if (candidate ===
|
|
242
|
+
if (candidate === sourceHost || candidate === registrationDomain || (registrationDomain && candidate.endsWith(`.${registrationDomain}`)))
|
|
242
243
|
return 'same_registrable_domain';
|
|
243
244
|
if (reviewedProfileDomains.has(candidate))
|
|
244
245
|
return 'reviewed_profile';
|
|
246
|
+
if ([...reader.coverage.values()].some((row) => row.id.startsWith('profile.') && graphInputIsIncomplete(row)))
|
|
247
|
+
return 'unresolved';
|
|
245
248
|
return 'external';
|
|
246
249
|
};
|
|
247
250
|
const dnsEvidence = record(input.dnsEvidence);
|
|
248
251
|
const dnsRecords = record(input.dnsRecords);
|
|
249
252
|
const dnsCompleteness = sourceCompleteness(dnsEvidence);
|
|
250
|
-
const dnsLimits = limitations(dnsEvidence.limitations);
|
|
253
|
+
const dnsLimits = limitations(dnsEvidence.limitations, 'dns');
|
|
251
254
|
const dnsEdge = (kind, label, lenses, href = '#evidence-dns') => ({
|
|
252
255
|
kind,
|
|
253
256
|
label,
|
|
254
257
|
sourceLabel: 'DNS',
|
|
255
|
-
observedAt: isoDate(dnsEvidence.observedAt)
|
|
258
|
+
observedAt: isoDate(dnsEvidence.observedAt),
|
|
256
259
|
completeness: dnsCompleteness,
|
|
257
260
|
limitations: dnsLimits,
|
|
258
261
|
lenses,
|
|
259
262
|
href,
|
|
260
263
|
});
|
|
261
|
-
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')]) {
|
|
262
265
|
connect('address', address, dnsEdge('resolves-to', 'resolves to', ['all']));
|
|
263
266
|
}
|
|
264
|
-
for (const alias of textList(dnsRecords.cname)) {
|
|
267
|
+
for (const alias of textList(dnsRecords.cname, 'dns.cname')) {
|
|
265
268
|
connect('hostname', alias, dnsEdge('aliases-to', 'aliases to', ['all']));
|
|
266
269
|
}
|
|
267
|
-
for (const nameserver of textList(dnsRecords.ns)) {
|
|
270
|
+
for (const nameserver of textList(dnsRecords.ns, 'dns.ns')) {
|
|
268
271
|
connect('hostname', nameserver, dnsEdge('uses-nameserver', 'uses nameserver', ['all', 'delegation']));
|
|
269
272
|
}
|
|
270
|
-
for (const
|
|
271
|
-
const host = mxHostname(mx);
|
|
273
|
+
for (const host of reader.values('dns.mx', dnsRecords.mx, MAX_VALUES, mxHostname, (item) => item)) {
|
|
272
274
|
if (host)
|
|
273
275
|
connect('hostname', host, dnsEdge('routes-mail-to', 'routes mail to', ['all']));
|
|
274
276
|
}
|
|
275
|
-
for (const binding of records(dnsRecords.https)) {
|
|
277
|
+
for (const binding of records(dnsRecords.https, 'dns.https', MAX_VALUES)) {
|
|
276
278
|
const serviceTarget = hostname(binding.target);
|
|
277
279
|
if (serviceTarget && serviceTarget !== '.') {
|
|
278
280
|
connect('hostname', serviceTarget, dnsEdge('service-binding', 'publishes HTTPS service target', ['all']), `Priority ${text(binding.priority, 12) || 'unknown'}`);
|
|
@@ -290,65 +292,69 @@ export function buildLookupAssetGraph(input) {
|
|
|
290
292
|
kind: 'registered-via',
|
|
291
293
|
label: 'registered via',
|
|
292
294
|
sourceLabel: 'Registry RDAP',
|
|
293
|
-
observedAt,
|
|
295
|
+
observedAt: isoDate(rdapEvidence.fetchedAt ?? rdapEvidence.observedAt),
|
|
294
296
|
completeness: sourceCompleteness(rdapEvidence),
|
|
295
|
-
limitations: limitations(rdapEvidence.limitations),
|
|
297
|
+
limitations: limitations(rdapEvidence.limitations, 'rdap'),
|
|
296
298
|
lenses: ['all'],
|
|
297
299
|
href: '#evidence-registry',
|
|
298
|
-
}, 'Published registrar of record');
|
|
300
|
+
}, 'Published registrar of record', registrationId);
|
|
299
301
|
}
|
|
300
302
|
const delegation = record(dnsEvidence.delegation);
|
|
301
|
-
const delegationLimits = limitations(delegation.limitations);
|
|
303
|
+
const delegationLimits = limitations(delegation.limitations, 'delegation');
|
|
302
304
|
const registryObservationId = addNode('observation', 'Registry publication', 'Registry nameserver and glue evidence');
|
|
303
|
-
const parentObservationId = addNode('observation', 'Recursive
|
|
304
|
-
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')) {
|
|
305
307
|
const nameserverId = addNode('hostname', nameserver, 'Authoritative nameserver');
|
|
306
308
|
if (registryObservationId && nameserverId) {
|
|
307
309
|
addEdge({
|
|
308
310
|
source: registryObservationId,
|
|
309
311
|
target: nameserverId,
|
|
310
312
|
...dnsEdge('registry-publishes', 'registry publishes', ['delegation']),
|
|
313
|
+
observedAt: isoDate(rdapEvidence.fetchedAt ?? rdapEvidence.observedAt),
|
|
311
314
|
sourceLabel: 'Registry delegation',
|
|
312
315
|
completeness: sourceCompleteness(delegation),
|
|
313
316
|
limitations: delegationLimits,
|
|
314
317
|
});
|
|
315
318
|
}
|
|
316
319
|
}
|
|
317
|
-
for (const nameserver of textList(record(delegation.parent).nameservers)) {
|
|
320
|
+
for (const nameserver of textList(record(delegation.parent).nameservers, 'delegation.parent.nameservers')) {
|
|
318
321
|
const nameserverId = addNode('hostname', nameserver, 'Authoritative nameserver');
|
|
319
322
|
if (parentObservationId && nameserverId) {
|
|
320
323
|
addEdge({
|
|
321
324
|
source: parentObservationId,
|
|
322
325
|
target: nameserverId,
|
|
323
|
-
...dnsEdge('parent-observes', '
|
|
324
|
-
|
|
326
|
+
...dnsEdge('parent-observes', 'recursive resolver observes', ['delegation']),
|
|
327
|
+
observedAt: isoDate(delegation.observedAt),
|
|
328
|
+
sourceLabel: 'Recursive nameserver observation',
|
|
325
329
|
completeness: sourceCompleteness(delegation),
|
|
326
330
|
limitations: delegationLimits,
|
|
327
331
|
});
|
|
328
332
|
}
|
|
329
333
|
}
|
|
330
|
-
for (const authority of records(delegation.authorities)) {
|
|
334
|
+
for (const [authorityIndex, authority] of records(delegation.authorities, 'delegation.authorities').entries()) {
|
|
331
335
|
const nameserver = hostname(authority.nameserver);
|
|
332
336
|
if (!nameserver)
|
|
333
337
|
continue;
|
|
334
338
|
const nameserverId = addNode('hostname', nameserver, `Direct authority state: ${text(authority.state, 40) || 'unknown'}`);
|
|
335
339
|
if (nameserverId) {
|
|
336
340
|
addEdge({
|
|
337
|
-
source:
|
|
341
|
+
source: registrationId,
|
|
338
342
|
target: nameserverId,
|
|
339
343
|
...dnsEdge('direct-authority', 'queried directly', ['delegation']),
|
|
344
|
+
observedAt: isoDate(delegation.observedAt),
|
|
340
345
|
sourceLabel: 'Direct authoritative DNS',
|
|
341
346
|
completeness: authority.state === 'success' ? 'complete' : authority.state ? 'partial' : 'unknown',
|
|
342
347
|
limitations: delegationLimits,
|
|
343
348
|
});
|
|
344
349
|
}
|
|
345
|
-
for (const address of textList(authority.addresses,
|
|
350
|
+
for (const address of textList(authority.addresses, `delegation.authorities.${authorityIndex}.addresses`)) {
|
|
346
351
|
const addressId = addNode('address', address, text(authority.addressSource, 80));
|
|
347
352
|
if (nameserverId && addressId) {
|
|
348
353
|
addEdge({
|
|
349
354
|
source: nameserverId,
|
|
350
355
|
target: addressId,
|
|
351
356
|
...dnsEdge('nameserver-address', 'answered at', ['delegation']),
|
|
357
|
+
observedAt: isoDate(delegation.observedAt),
|
|
352
358
|
sourceLabel: text(authority.addressSource, 80) || 'Nameserver address',
|
|
353
359
|
completeness: authority.state === 'success' ? 'complete' : 'partial',
|
|
354
360
|
limitations: delegationLimits,
|
|
@@ -370,16 +376,16 @@ export function buildLookupAssetGraph(input) {
|
|
|
370
376
|
kind: 'observed-endpoint',
|
|
371
377
|
label: 'was observed at',
|
|
372
378
|
sourceLabel: 'Observed network context',
|
|
373
|
-
observedAt: isoDate(networkContext.observedAt)
|
|
379
|
+
observedAt: isoDate(networkContext.observedAt),
|
|
374
380
|
completeness: sourceCompleteness(networkContext),
|
|
375
|
-
limitations: limitations(networkContext.limitations),
|
|
381
|
+
limitations: limitations(networkContext.limitations, 'network'),
|
|
376
382
|
lenses: ['all'],
|
|
377
383
|
href: '#evidence-network',
|
|
378
384
|
});
|
|
379
385
|
}
|
|
380
386
|
const networkLabel = text(network.name || network.holder || network.handle, 180);
|
|
381
387
|
const networkId = networkLabel
|
|
382
|
-
? addNode('network', networkLabel, textList(network.cidrs,
|
|
388
|
+
? addNode('network', networkLabel, textList(network.cidrs, 'network.cidrs').join(', '))
|
|
383
389
|
: null;
|
|
384
390
|
if (endpointId && networkId) {
|
|
385
391
|
addEdge({
|
|
@@ -388,14 +394,14 @@ export function buildLookupAssetGraph(input) {
|
|
|
388
394
|
kind: 'registered-with',
|
|
389
395
|
label: 'registered within',
|
|
390
396
|
sourceLabel: 'IP RDAP',
|
|
391
|
-
observedAt: isoDate(networkContext.observedAt)
|
|
397
|
+
observedAt: isoDate(networkContext.observedAt),
|
|
392
398
|
completeness: sourceCompleteness(networkContext),
|
|
393
|
-
limitations: limitations(networkContext.limitations),
|
|
399
|
+
limitations: limitations(networkContext.limitations, 'network'),
|
|
394
400
|
lenses: ['all'],
|
|
395
401
|
href: '#evidence-network',
|
|
396
402
|
});
|
|
397
403
|
}
|
|
398
|
-
for (const cidr of textList(network.cidrs,
|
|
404
|
+
for (const cidr of textList(network.cidrs, 'network.cidrs')) {
|
|
399
405
|
const prefixId = addNode('prefix', cidr, 'Published IP RDAP network prefix');
|
|
400
406
|
const prefixSource = networkId || endpointId;
|
|
401
407
|
if (prefixId && prefixSource) {
|
|
@@ -405,148 +411,149 @@ export function buildLookupAssetGraph(input) {
|
|
|
405
411
|
kind: 'publishes-prefix',
|
|
406
412
|
label: 'publishes prefix',
|
|
407
413
|
sourceLabel: 'IP RDAP',
|
|
408
|
-
observedAt: isoDate(networkContext.observedAt)
|
|
414
|
+
observedAt: isoDate(networkContext.observedAt),
|
|
409
415
|
completeness: sourceCompleteness(networkContext),
|
|
410
|
-
limitations: limitations(networkContext.limitations),
|
|
416
|
+
limitations: limitations(networkContext.limitations, 'network'),
|
|
411
417
|
lenses: ['all'],
|
|
412
418
|
href: '#evidence-network',
|
|
413
419
|
});
|
|
414
420
|
}
|
|
415
421
|
}
|
|
416
422
|
const httpEvidence = record(input.httpEvidence);
|
|
417
|
-
const
|
|
423
|
+
const finalOrigin = webOrigin(httpEvidence.finalUrl);
|
|
424
|
+
const finalHost = finalOrigin?.hostname ?? null;
|
|
418
425
|
const finalHostId = finalHost
|
|
419
426
|
? connect('hostname', finalHost, {
|
|
420
427
|
kind: 'redirects-to',
|
|
421
428
|
label: finalHost === target ? 'served from' : 'redirects to',
|
|
422
429
|
sourceLabel: 'HTTP',
|
|
423
|
-
observedAt: isoDate(httpEvidence.observedAt)
|
|
430
|
+
observedAt: isoDate(httpEvidence.observedAt),
|
|
424
431
|
completeness: sourceCompleteness(httpEvidence),
|
|
425
|
-
limitations: limitations(httpEvidence.limitations),
|
|
432
|
+
limitations: limitations(httpEvidence.limitations, 'http'),
|
|
426
433
|
lenses: ['all', 'identity'],
|
|
427
434
|
href: '#evidence-http',
|
|
428
435
|
boundary: trustBoundary(finalHost),
|
|
429
436
|
}, 'Observed final website origin')
|
|
430
437
|
: null;
|
|
431
438
|
const identitySource = finalHostId || targetId;
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
439
|
+
const pageObservedAt = isoDate(record(input.pageIdentity).observedAt);
|
|
440
|
+
const canonicalOrigin = webOrigin(record(input.pageCanonical).url);
|
|
441
|
+
if (canonicalOrigin) {
|
|
442
|
+
connect('origin', canonicalOrigin.origin, {
|
|
435
443
|
kind: 'declares-canonical',
|
|
436
444
|
label: 'declares canonical origin',
|
|
437
445
|
sourceLabel: 'HTML canonical metadata',
|
|
438
|
-
observedAt,
|
|
446
|
+
observedAt: pageObservedAt,
|
|
439
447
|
completeness: sourceCompleteness(input.pageIdentity),
|
|
440
|
-
limitations: limitations(record(input.pageIdentity).limitations),
|
|
448
|
+
limitations: limitations(record(input.pageIdentity).limitations, 'page'),
|
|
441
449
|
lenses: ['identity'],
|
|
442
450
|
href: '#evidence-page',
|
|
443
|
-
boundary: trustBoundary(
|
|
451
|
+
boundary: trustBoundary(canonicalOrigin.hostname, finalHost || target, canonicalOrigin.origin, finalOrigin?.origin ?? null),
|
|
444
452
|
}, 'Publisher-declared canonical origin', identitySource);
|
|
445
453
|
}
|
|
446
|
-
const
|
|
447
|
-
if (
|
|
448
|
-
connect('origin',
|
|
454
|
+
const openGraphOrigin = webOrigin(record(input.pageOpenGraphUrl).url);
|
|
455
|
+
if (openGraphOrigin) {
|
|
456
|
+
connect('origin', openGraphOrigin.origin, {
|
|
449
457
|
kind: 'declares-open-graph',
|
|
450
458
|
label: 'declares Open Graph origin',
|
|
451
459
|
sourceLabel: 'Open Graph metadata',
|
|
452
|
-
observedAt,
|
|
460
|
+
observedAt: pageObservedAt,
|
|
453
461
|
completeness: sourceCompleteness(input.pageIdentity),
|
|
454
|
-
limitations: limitations(record(input.pageIdentity).limitations),
|
|
462
|
+
limitations: limitations(record(input.pageIdentity).limitations, 'page'),
|
|
455
463
|
lenses: ['identity'],
|
|
456
464
|
href: '#evidence-page',
|
|
457
|
-
boundary: trustBoundary(
|
|
465
|
+
boundary: trustBoundary(openGraphOrigin.hostname, finalHost || target, openGraphOrigin.origin, finalOrigin?.origin ?? null),
|
|
458
466
|
}, 'Publisher-declared Open Graph origin', identitySource);
|
|
459
467
|
}
|
|
460
|
-
for (const origin of textList(record(input.pageForms).externalActionOrigins)) {
|
|
461
|
-
const
|
|
462
|
-
if (
|
|
463
|
-
connect('origin',
|
|
468
|
+
for (const origin of textList(record(input.pageForms).externalActionOrigins, 'page.formOrigins')) {
|
|
469
|
+
const destination = webOrigin(origin);
|
|
470
|
+
if (destination) {
|
|
471
|
+
connect('origin', destination.origin, {
|
|
464
472
|
kind: 'form-destination',
|
|
465
473
|
label: 'form may submit to',
|
|
466
474
|
sourceLabel: 'Static HTML form',
|
|
467
|
-
observedAt,
|
|
475
|
+
observedAt: pageObservedAt,
|
|
468
476
|
completeness: sourceCompleteness(input.pageIdentity),
|
|
469
|
-
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.']),
|
|
470
478
|
lenses: ['identity'],
|
|
471
479
|
href: '#evidence-page',
|
|
472
|
-
boundary: trustBoundary(
|
|
480
|
+
boundary: trustBoundary(destination.hostname, finalHost || target, destination.origin, finalOrigin?.origin ?? null),
|
|
473
481
|
}, 'External form-action origin', identitySource);
|
|
474
482
|
}
|
|
475
483
|
}
|
|
476
|
-
for (const origin of textList(record(input.pageResources).externalOrigins)) {
|
|
477
|
-
const
|
|
478
|
-
if (
|
|
479
|
-
connect('
|
|
484
|
+
for (const origin of textList(record(input.pageResources).externalOrigins, 'page.resourceOrigins')) {
|
|
485
|
+
const resource = webOrigin(origin);
|
|
486
|
+
if (resource) {
|
|
487
|
+
connect('origin', resource.origin, {
|
|
480
488
|
kind: 'loads-from',
|
|
481
489
|
label: 'references resources from',
|
|
482
490
|
sourceLabel: 'Static HTML resources',
|
|
483
|
-
observedAt,
|
|
491
|
+
observedAt: pageObservedAt,
|
|
484
492
|
completeness: sourceCompleteness(input.pageIdentity),
|
|
485
|
-
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.']),
|
|
486
494
|
lenses: ['identity'],
|
|
487
495
|
href: '#evidence-page',
|
|
488
|
-
boundary: trustBoundary(
|
|
496
|
+
boundary: trustBoundary(resource.hostname, finalHost || target, resource.origin, finalOrigin?.origin ?? null),
|
|
489
497
|
}, 'External resource origin', identitySource);
|
|
490
498
|
}
|
|
491
499
|
}
|
|
492
|
-
for (const identifier of records(record(input.pageIdentity).trackingIdentifiers,
|
|
500
|
+
for (const identifier of records(record(input.pageIdentity).trackingIdentifiers, 'page.trackingIdentifiers', MAX_VALUES)) {
|
|
493
501
|
const value = text(identifier.value, 160);
|
|
494
502
|
if (value) {
|
|
495
503
|
connect('tracker', value, {
|
|
496
504
|
kind: 'declares-tracker',
|
|
497
505
|
label: 'declares tracker identifier',
|
|
498
506
|
sourceLabel: 'Static HTML identity',
|
|
499
|
-
observedAt,
|
|
507
|
+
observedAt: pageObservedAt,
|
|
500
508
|
completeness: sourceCompleteness(input.pageIdentity),
|
|
501
|
-
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.']),
|
|
502
510
|
lenses: ['identity'],
|
|
503
511
|
href: '#evidence-page',
|
|
504
512
|
}, text(identifier.type, 80), identitySource);
|
|
505
513
|
}
|
|
506
514
|
}
|
|
507
515
|
const structuredIdentity = record(input.structuredDataIdentity);
|
|
508
|
-
|
|
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()) {
|
|
509
522
|
const entityLabel = text(entity.name, 180);
|
|
510
|
-
const declaredOrigin =
|
|
523
|
+
const declaredOrigin = webOrigin(entity.declaredOrigin);
|
|
511
524
|
const entityId = entityLabel
|
|
512
525
|
? connect('identity', entityLabel, {
|
|
513
526
|
kind: 'declares-publisher',
|
|
514
527
|
label: 'declares publisher',
|
|
515
528
|
sourceLabel: 'Structured identity metadata',
|
|
516
|
-
observedAt,
|
|
529
|
+
observedAt: structuredObservedAt,
|
|
517
530
|
completeness: sourceCompleteness(structuredIdentity),
|
|
518
|
-
limitations:
|
|
519
|
-
...limitations(structuredIdentity.limitations),
|
|
520
|
-
'Publisher-declared structured metadata is not independent identity verification.',
|
|
521
|
-
].slice(0, MAX_LIMITATIONS),
|
|
531
|
+
limitations: structuredLimits,
|
|
522
532
|
lenses: ['identity'],
|
|
523
533
|
href: '#evidence-structured-identity',
|
|
524
|
-
}, textList(entity.types,
|
|
534
|
+
}, textList(entity.types, `structured.entities.${entityIndex}.types`, MAX_NESTED_VALUES).join(', ') || 'Publisher-declared entity', identitySource)
|
|
525
535
|
: identitySource;
|
|
526
536
|
if (declaredOrigin) {
|
|
527
|
-
connect('origin', declaredOrigin, {
|
|
537
|
+
connect('origin', declaredOrigin.origin, {
|
|
528
538
|
kind: 'declares-origin',
|
|
529
539
|
label: 'declares origin',
|
|
530
540
|
sourceLabel: 'Structured identity metadata',
|
|
531
|
-
observedAt,
|
|
541
|
+
observedAt: structuredObservedAt,
|
|
532
542
|
completeness: sourceCompleteness(structuredIdentity),
|
|
533
|
-
limitations:
|
|
543
|
+
limitations: structuredLimits,
|
|
534
544
|
lenses: ['identity'],
|
|
535
545
|
href: '#evidence-structured-identity',
|
|
536
|
-
boundary: trustBoundary(declaredOrigin, finalHost || target),
|
|
546
|
+
boundary: trustBoundary(declaredOrigin.hostname, finalHost || target, declaredOrigin.origin, finalOrigin?.origin ?? null),
|
|
537
547
|
}, 'Publisher-declared structured-data origin', entityId || identitySource);
|
|
538
548
|
}
|
|
539
|
-
for (const sameAsHost of textList(entity.sameAsHosts,
|
|
549
|
+
for (const sameAsHost of textList(entity.sameAsHosts, `structured.entities.${entityIndex}.sameAsHosts`, MAX_NESTED_VALUES)) {
|
|
540
550
|
connect('origin', sameAsHost, {
|
|
541
551
|
kind: 'declares-same-as',
|
|
542
552
|
label: 'declares sameAs',
|
|
543
553
|
sourceLabel: 'Structured identity metadata',
|
|
544
|
-
observedAt,
|
|
554
|
+
observedAt: structuredObservedAt,
|
|
545
555
|
completeness: sourceCompleteness(structuredIdentity),
|
|
546
|
-
limitations:
|
|
547
|
-
...limitations(structuredIdentity.limitations),
|
|
548
|
-
'A sameAs declaration is a site-authored claim and does not establish ownership or control.',
|
|
549
|
-
].slice(0, MAX_LIMITATIONS),
|
|
556
|
+
limitations: structuredLimits,
|
|
550
557
|
lenses: ['identity'],
|
|
551
558
|
href: '#evidence-structured-identity',
|
|
552
559
|
boundary: trustBoundary(hostname(sameAsHost), finalHost || target),
|
|
@@ -568,9 +575,9 @@ export function buildLookupAssetGraph(input) {
|
|
|
568
575
|
kind: 'presents-certificate',
|
|
569
576
|
label: 'presented certificate',
|
|
570
577
|
sourceLabel: 'TLS',
|
|
571
|
-
observedAt: isoDate(tlsEvidence.observedAt)
|
|
578
|
+
observedAt: isoDate(tlsEvidence.observedAt),
|
|
572
579
|
completeness: sourceCompleteness(tlsEvidence),
|
|
573
|
-
limitations: limitations(tlsEvidence.limitations),
|
|
580
|
+
limitations: limitations(tlsEvidence.limitations, 'tls'),
|
|
574
581
|
lenses: ['all', 'certificate'],
|
|
575
582
|
href: '#evidence-tls',
|
|
576
583
|
}, certificateDetail)
|
|
@@ -588,9 +595,9 @@ export function buildLookupAssetGraph(input) {
|
|
|
588
595
|
kind: 'reviewed-hostname-match',
|
|
589
596
|
label: 'reviewed hostname match',
|
|
590
597
|
sourceLabel: 'TLS hostname verification',
|
|
591
|
-
observedAt: isoDate(tlsEvidence.observedAt)
|
|
598
|
+
observedAt: isoDate(tlsEvidence.observedAt),
|
|
592
599
|
completeness: sourceCompleteness(tlsEvidence),
|
|
593
|
-
limitations: limitations(tlsEvidence.limitations),
|
|
600
|
+
limitations: limitations(tlsEvidence.limitations, 'tls'),
|
|
594
601
|
lenses: ['certificate', 'identity'],
|
|
595
602
|
href: '#evidence-tls',
|
|
596
603
|
});
|
|
@@ -600,9 +607,9 @@ export function buildLookupAssetGraph(input) {
|
|
|
600
607
|
kind: 'evaluated-certificate',
|
|
601
608
|
label: 'evaluated certificate',
|
|
602
609
|
sourceLabel: 'TLS hostname verification',
|
|
603
|
-
observedAt: isoDate(tlsEvidence.observedAt)
|
|
610
|
+
observedAt: isoDate(tlsEvidence.observedAt),
|
|
604
611
|
completeness: sourceCompleteness(tlsEvidence),
|
|
605
|
-
limitations: limitations(tlsEvidence.limitations),
|
|
612
|
+
limitations: limitations(tlsEvidence.limitations, 'tls'),
|
|
606
613
|
lenses: ['certificate'],
|
|
607
614
|
href: '#evidence-tls',
|
|
608
615
|
});
|
|
@@ -620,15 +627,15 @@ export function buildLookupAssetGraph(input) {
|
|
|
620
627
|
kind: 'reviewed-runtime-trust',
|
|
621
628
|
label: 'reviewed runtime trust',
|
|
622
629
|
sourceLabel: 'TLS chain verification',
|
|
623
|
-
observedAt: isoDate(tlsEvidence.observedAt)
|
|
630
|
+
observedAt: isoDate(tlsEvidence.observedAt),
|
|
624
631
|
completeness: sourceCompleteness(tlsEvidence),
|
|
625
|
-
limitations: limitations(tlsEvidence.limitations),
|
|
632
|
+
limitations: limitations(tlsEvidence.limitations, 'tls'),
|
|
626
633
|
lenses: ['certificate'],
|
|
627
634
|
href: '#evidence-tls',
|
|
628
635
|
});
|
|
629
636
|
}
|
|
630
637
|
}
|
|
631
|
-
for (const name of textList(record(input.tlsAltNames).dnsNames,
|
|
638
|
+
for (const name of textList(record(input.tlsAltNames).dnsNames, 'tls.dnsNames')) {
|
|
632
639
|
const sanId = addNode('hostname', name, 'Certificate DNS subject alternative name');
|
|
633
640
|
if (sanId) {
|
|
634
641
|
addEdge({
|
|
@@ -637,9 +644,9 @@ export function buildLookupAssetGraph(input) {
|
|
|
637
644
|
kind: 'authorizes-name',
|
|
638
645
|
label: 'contains SAN',
|
|
639
646
|
sourceLabel: 'TLS certificate',
|
|
640
|
-
observedAt: isoDate(tlsEvidence.observedAt)
|
|
647
|
+
observedAt: isoDate(tlsEvidence.observedAt),
|
|
641
648
|
completeness: sourceCompleteness(tlsEvidence),
|
|
642
|
-
limitations: limitations(tlsEvidence.limitations),
|
|
649
|
+
limitations: limitations(tlsEvidence.limitations, 'tls'),
|
|
643
650
|
lenses: ['certificate', 'identity'],
|
|
644
651
|
href: '#evidence-tls',
|
|
645
652
|
boundary: trustBoundary(hostname(name)),
|
|
@@ -657,34 +664,48 @@ export function buildLookupAssetGraph(input) {
|
|
|
657
664
|
kind: 'uses-key',
|
|
658
665
|
label: 'uses public key',
|
|
659
666
|
sourceLabel: 'TLS certificate',
|
|
660
|
-
observedAt: isoDate(tlsEvidence.observedAt)
|
|
667
|
+
observedAt: isoDate(tlsEvidence.observedAt),
|
|
661
668
|
completeness: sourceCompleteness(tlsEvidence),
|
|
662
|
-
limitations: limitations(tlsEvidence.limitations),
|
|
669
|
+
limitations: limitations(tlsEvidence.limitations, 'tls'),
|
|
663
670
|
lenses: ['certificate'],
|
|
664
671
|
href: '#evidence-tls',
|
|
665
672
|
});
|
|
666
673
|
}
|
|
667
674
|
}
|
|
668
675
|
const issuer = record(input.tlsIssuer);
|
|
669
|
-
const
|
|
676
|
+
const issuerOrganizations = textList(issuer.organizations, 'tls.issuer.organizations');
|
|
677
|
+
const issuerCommonNames = textList(issuer.commonNames, 'tls.issuer.commonNames');
|
|
678
|
+
const issuerLabel = issuerOrganizations[0]
|
|
679
|
+
|| issuerCommonNames[0]
|
|
680
|
+
|| text(issuer.organization || issuer.commonName || issuer.CN, 180);
|
|
670
681
|
if (issuerLabel) {
|
|
671
682
|
const issuerId = addNode('issuer', issuerLabel, 'Certificate issuer');
|
|
672
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
|
+
}
|
|
673
694
|
addEdge({
|
|
674
695
|
source: certificateId,
|
|
675
696
|
target: issuerId,
|
|
676
697
|
kind: 'issued-by',
|
|
677
698
|
label: 'issued by',
|
|
678
699
|
sourceLabel: 'TLS certificate',
|
|
679
|
-
observedAt: isoDate(tlsEvidence.observedAt)
|
|
700
|
+
observedAt: isoDate(tlsEvidence.observedAt),
|
|
680
701
|
completeness: sourceCompleteness(tlsEvidence),
|
|
681
|
-
limitations: limitations(tlsEvidence.limitations),
|
|
702
|
+
limitations: limitations(tlsEvidence.limitations, 'tls'),
|
|
682
703
|
lenses: ['certificate'],
|
|
683
704
|
href: '#evidence-tls',
|
|
684
705
|
});
|
|
685
706
|
}
|
|
686
707
|
}
|
|
687
|
-
for (const finding of records(record(input.certificatePolicyReview).findings,
|
|
708
|
+
for (const [findingIndex, finding] of records(record(input.certificatePolicyReview).findings, 'certificatePolicy.findings').entries()) {
|
|
688
709
|
const findingId = text(finding.id, 80);
|
|
689
710
|
const state = text(finding.state, 80);
|
|
690
711
|
if (!findingId || state === 'not_configured')
|
|
@@ -697,21 +718,27 @@ export function buildLookupAssetGraph(input) {
|
|
|
697
718
|
kind: 'reviewed-against-policy',
|
|
698
719
|
label: 'reviewed against',
|
|
699
720
|
sourceLabel: 'DNS / TLS / Brand Profile',
|
|
700
|
-
observedAt: isoDate(record(input.certificatePolicyReview).observedAt)
|
|
721
|
+
observedAt: isoDate(record(input.certificatePolicyReview).observedAt),
|
|
701
722
|
completeness: state === 'indeterminate' || state === 'no_target_policy_observed' ? 'partial' : 'complete',
|
|
702
|
-
limitations: limitations(finding.limitations),
|
|
723
|
+
limitations: limitations(finding.limitations, `certificatePolicy.${findingIndex}`),
|
|
703
724
|
lenses: ['certificate'],
|
|
704
725
|
href: '#evidence-certificate-policy',
|
|
705
726
|
});
|
|
706
727
|
}
|
|
707
728
|
}
|
|
708
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);
|
|
709
735
|
return {
|
|
710
736
|
version: LOOKUP_ASSET_GRAPH_VERSION,
|
|
711
737
|
targetId,
|
|
712
|
-
nodes: [...nodes.values()],
|
|
713
|
-
edges:
|
|
714
|
-
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 },
|
|
715
742
|
truncated,
|
|
716
743
|
limitations: [
|
|
717
744
|
'The graph contains only settled evidence from this Lookup and does not start additional requests.',
|
|
@@ -732,10 +759,11 @@ export function countLookupAssetGraphEdgesByLens(graph) {
|
|
|
732
759
|
export function projectLookupAssetGraph(graph, lens) {
|
|
733
760
|
const acceptedEdges = graph.edges.filter((edge) => edgeMatchesLens(edge, lens));
|
|
734
761
|
const graphNodeById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
762
|
+
const groups = nodeGroups(graph.nodes, acceptedEdges);
|
|
735
763
|
const visualEdges = [];
|
|
736
764
|
const visualDegree = new Map();
|
|
737
765
|
const omittedByHub = new Map();
|
|
738
|
-
for (const edge of interleaveVisualEdgesByFamily(acceptedEdges,
|
|
766
|
+
for (const edge of interleaveVisualEdgesByFamily(acceptedEdges, groups)) {
|
|
739
767
|
const sourceDegree = visualDegree.get(edge.source) ?? 0;
|
|
740
768
|
const targetDegree = visualDegree.get(edge.target) ?? 0;
|
|
741
769
|
const saturatedHub = sourceDegree >= MAX_VISUAL_EDGES_PER_HUB
|
|
@@ -764,7 +792,7 @@ export function projectLookupAssetGraph(graph, lens) {
|
|
|
764
792
|
const nodes = graph.nodes
|
|
765
793
|
.filter((node) => nodeIds.has(node.id))
|
|
766
794
|
.map((node) => {
|
|
767
|
-
const group =
|
|
795
|
+
const group = groups.get(node.id);
|
|
768
796
|
return {
|
|
769
797
|
id: node.id,
|
|
770
798
|
label: node.label,
|