@slicedearth/whoisleuth-cli 2.2.0 → 2.3.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/README.md +2 -1
- package/cli/arguments.mjs +389 -1745
- package/cli/artifact-validation/case-response.mjs +55 -30
- package/cli/artifact-validation/signed-review.mjs +30 -12
- package/cli/bulk-checkpoint.mjs +34 -3
- package/cli/case-pack.mjs +39 -92
- package/cli/command-argument-grammar.mjs +211 -0
- package/cli/command-reference.mjs +200 -213
- package/cli/completion.mjs +5 -1
- package/cli/discovery-observation-snapshot.mjs +16 -3
- package/cli/discovery-scan-command-runner.mjs +19 -0
- package/cli/discovery-scan.mjs +6 -0
- package/cli/domain-control-observations.mjs +1 -1
- package/cli/inline-command-families.mjs +1 -0
- package/cli/input.mjs +116 -0
- package/cli/interchange-report.mjs +14 -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/mail-header-review.mjs +467 -0
- package/cli/review-command-runner.mjs +25 -0
- package/cli/risk-calibration.mjs +19 -17
- package/cli/runner.mjs +7 -1
- package/docs/cli-reference.md +8 -1
- package/docs/cli.md +12 -1
- package/lib/bounded-json.mjs +9 -6
- package/lib/dns-convergence-review.mjs +1 -0
- package/lib/dns-intelligence.mjs +37 -3
- package/lib/dnssec-chain-validation.mjs +6 -0
- package/lib/domain-change-packet.mjs +42 -14
- package/lib/domain-control-flight-recorder.mjs +4 -1
- package/lib/domain-control-manifest.mjs +16 -16
- package/lib/domain-posture-parsers.mjs +114 -3
- package/lib/domain-posture.mjs +34 -3
- package/lib/evidence-export.mjs +60 -11
- package/lib/evidence-report.mjs +2 -2
- package/lib/html-signals.mjs +39 -33
- package/lib/local-geoip-evidence.mjs +19 -3
- package/lib/lookup-response.mjs +5 -4
- package/lib/lookup-source-progress.mjs +27 -4
- package/lib/nameserver-preflight-review.mjs +1 -0
- package/lib/rdap-capabilities.mjs +1 -1
- package/lib/registry-insights.mjs +35 -22
- package/lib/risk-calibration-summary.mjs +1 -3
- package/lib/security-txt.mjs +33 -1
- package/lib/static-html-analysis.mjs +69 -13
- package/lib/website-technology.mjs +79 -11
- package/lib/zone-intent-review.mjs +16 -6
- package/package.json +1 -1
- package/packages/cases/case-migration-model.mjs +71 -22
- package/packages/cases/case-record-contracts.mjs +5 -20
- package/packages/cases/case-record-core.mjs +10 -21
- 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 +6 -4
- package/packages/cases/case-record-projection.mjs +218 -0
- package/packages/cases/case-report.mjs +11 -6
- package/packages/cases/case-response-model.mjs +43 -6
- package/packages/cases/case-response-packet.mjs +78 -27
- package/packages/cases/case-storage-model.mjs +6 -2
- package/packages/comparison/comparison-ledger-bulk.mjs +9 -10
- package/packages/comparison/page-similarity.mjs +3 -1
- package/packages/contracts/case-portability.mjs +171 -51
- package/packages/contracts/cli-command-semantics.mjs +93 -0
- package/packages/contracts/risk-calibration.mjs +15 -10
- package/packages/contracts/workspace-portability.mjs +44 -7
- package/packages/evidence/domain-control-runtime.mjs +46 -6
- package/packages/interchange/external-findings-import.mjs +3 -2
- package/packages/investigation/lookup-asset-graph.mjs +35 -27
- package/packages/workspace/brand-profile-model.mjs +118 -2
- package/packages/workspace/bulk-review-model.mjs +13 -7
- package/packages/workspace/bulk-session-model.mjs +5 -4
- package/packages/workspace/detection-rule-model.mjs +70 -22
- package/packages/workspace/relationship-observation-model.mjs +4 -1
- package/packages/workspace/website-snapshot-model.mjs +106 -8
- package/packages/workspace/workspace-archive.mjs +2 -0
- package/cli/evidence-command-arguments.mjs +0 -156
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export const CAPABILITY_IDS = Object.freeze({
|
|
2
|
+
LOOKUP: 'lookup',
|
|
3
|
+
RDAP: 'rdap',
|
|
4
|
+
RDAP_NAMESERVER_SEARCH: 'rdap_nameserver_search',
|
|
5
|
+
WHOIS: 'whois',
|
|
6
|
+
AVAILABILITY: 'availability',
|
|
7
|
+
DOMAIN_EVIDENCE: 'domain_evidence',
|
|
8
|
+
DNS_INTELLIGENCE: 'dns_intelligence',
|
|
9
|
+
WEBSITE_PROBE: 'website_probe',
|
|
10
|
+
TLS_INTELLIGENCE: 'tls_intelligence',
|
|
11
|
+
CERTIFICATE_TRANSPARENCY: 'certificate_transparency',
|
|
12
|
+
SECURITY_TXT: 'security_txt',
|
|
13
|
+
EXTERNAL_INTELLIGENCE: 'external_intelligence',
|
|
14
|
+
URLSCAN_SEARCH: 'urlscan_search',
|
|
15
|
+
URLHAUS_HOST: 'urlhaus_host',
|
|
16
|
+
THREATFOX_DOMAIN_IOC: 'threatfox_domain_ioc',
|
|
17
|
+
REGISTRAR_RDAP: 'registrar_rdap',
|
|
18
|
+
NETWORK_CONTEXT: 'network_context',
|
|
19
|
+
REVERSE_DNS: 'reverse_dns',
|
|
20
|
+
DOMAIN_POSTURE: 'domain_posture',
|
|
21
|
+
DNSSEC_VALIDATION: 'dnssec_validation',
|
|
22
|
+
MAIL_TRANSPORT_REVIEW: 'mail_transport_review',
|
|
23
|
+
RENDERED_WEB_CAPTURE: 'rendered_web_capture',
|
|
24
|
+
RENDERED_CAPTURE_COMPARISON: 'rendered_capture_comparison',
|
|
25
|
+
IDN_CONFUSABLES: 'idn_confusables',
|
|
26
|
+
ANALYST_CASES: 'analyst_cases',
|
|
27
|
+
WATCHLISTS: 'watchlists',
|
|
28
|
+
OFFLINE_REVIEW: 'offline_review',
|
|
29
|
+
PORTABLE_EVIDENCE: 'portable_evidence',
|
|
30
|
+
RUNTIME_DIAGNOSTICS: 'runtime_diagnostics',
|
|
31
|
+
WORKFLOW_EXECUTION: 'workflow_execution',
|
|
32
|
+
SCHEDULED_MONITORING: 'scheduled_monitoring',
|
|
33
|
+
DISTRIBUTED_BUDGETS: 'distributed_budgets',
|
|
34
|
+
});
|
|
35
|
+
export const CLI_HELP_GROUP_ORDER = Object.freeze([
|
|
36
|
+
'investigate',
|
|
37
|
+
'respond',
|
|
38
|
+
'assure',
|
|
39
|
+
'utilities',
|
|
40
|
+
]);
|
|
41
|
+
function semantic(group, capabilityFamilyId) {
|
|
42
|
+
return Object.freeze({ group, capabilityFamilyId });
|
|
43
|
+
}
|
|
44
|
+
export const CLI_COMMAND_SEMANTICS = Object.freeze({
|
|
45
|
+
completion: semantic('utilities', CAPABILITY_IDS.OFFLINE_REVIEW),
|
|
46
|
+
doctor: semantic('utilities', CAPABILITY_IDS.RUNTIME_DIAGNOSTICS),
|
|
47
|
+
commands: semantic('utilities', CAPABILITY_IDS.OFFLINE_REVIEW),
|
|
48
|
+
manual: semantic('utilities', CAPABILITY_IDS.OFFLINE_REVIEW),
|
|
49
|
+
manifest: semantic('assure', CAPABILITY_IDS.PORTABLE_EVIDENCE),
|
|
50
|
+
'map-observations': semantic('respond', CAPABILITY_IDS.OFFLINE_REVIEW),
|
|
51
|
+
'oam-export': semantic('respond', CAPABILITY_IDS.PORTABLE_EVIDENCE),
|
|
52
|
+
lookup: semantic('investigate', CAPABILITY_IDS.LOOKUP),
|
|
53
|
+
bulk: semantic('investigate', CAPABILITY_IDS.LOOKUP),
|
|
54
|
+
'ct-search': semantic('investigate', CAPABILITY_IDS.CERTIFICATE_TRANSPARENCY),
|
|
55
|
+
'ct-intake': semantic('investigate', CAPABILITY_IDS.OFFLINE_REVIEW),
|
|
56
|
+
discover: semantic('investigate', CAPABILITY_IDS.OFFLINE_REVIEW),
|
|
57
|
+
'discover-scan': semantic('investigate', CAPABILITY_IDS.LOOKUP),
|
|
58
|
+
posture: semantic('investigate', CAPABILITY_IDS.DOMAIN_POSTURE),
|
|
59
|
+
http: semantic('investigate', CAPABILITY_IDS.WEBSITE_PROBE),
|
|
60
|
+
tls: semantic('investigate', CAPABILITY_IDS.TLS_INTELLIGENCE),
|
|
61
|
+
'dnssec-validate': semantic('assure', CAPABILITY_IDS.DNSSEC_VALIDATION),
|
|
62
|
+
'mail-transport': semantic('assure', CAPABILITY_IDS.MAIL_TRANSPORT_REVIEW),
|
|
63
|
+
'registry-support': semantic('investigate', CAPABILITY_IDS.OFFLINE_REVIEW),
|
|
64
|
+
'registry-doctor': semantic('investigate', CAPABILITY_IDS.OFFLINE_REVIEW),
|
|
65
|
+
'registry-cohort': semantic('investigate', CAPABILITY_IDS.OFFLINE_REVIEW),
|
|
66
|
+
'registry-scaffold': semantic('utilities', CAPABILITY_IDS.OFFLINE_REVIEW),
|
|
67
|
+
'risk-calibrate': semantic('assure', CAPABILITY_IDS.OFFLINE_REVIEW),
|
|
68
|
+
'lookalike-calibrate': semantic('assure', CAPABILITY_IDS.OFFLINE_REVIEW),
|
|
69
|
+
'verify-artifact': semantic('assure', CAPABILITY_IDS.PORTABLE_EVIDENCE),
|
|
70
|
+
'interchange-report': semantic('assure', CAPABILITY_IDS.PORTABLE_EVIDENCE),
|
|
71
|
+
'inspect-archive': semantic('assure', CAPABILITY_IDS.PORTABLE_EVIDENCE),
|
|
72
|
+
'sign-artifact': semantic('assure', CAPABILITY_IDS.PORTABLE_EVIDENCE),
|
|
73
|
+
'verify-signature': semantic('assure', CAPABILITY_IDS.PORTABLE_EVIDENCE),
|
|
74
|
+
'source-report': semantic('investigate', CAPABILITY_IDS.OFFLINE_REVIEW),
|
|
75
|
+
compare: semantic('investigate', CAPABILITY_IDS.OFFLINE_REVIEW),
|
|
76
|
+
'page-compare': semantic('investigate', CAPABILITY_IDS.OFFLINE_REVIEW),
|
|
77
|
+
'mail-review': semantic('investigate', CAPABILITY_IDS.OFFLINE_REVIEW),
|
|
78
|
+
'mail-headers': semantic('investigate', CAPABILITY_IDS.OFFLINE_REVIEW),
|
|
79
|
+
'review-evidence': semantic('investigate', CAPABILITY_IDS.PORTABLE_EVIDENCE),
|
|
80
|
+
brief: semantic('investigate', CAPABILITY_IDS.OFFLINE_REVIEW),
|
|
81
|
+
'case-pack': semantic('respond', CAPABILITY_IDS.PORTABLE_EVIDENCE),
|
|
82
|
+
'domain-control': semantic('assure', CAPABILITY_IDS.PORTABLE_EVIDENCE),
|
|
83
|
+
'monitor-once': semantic('assure', CAPABILITY_IDS.LOOKUP),
|
|
84
|
+
assurance: semantic('assure', CAPABILITY_IDS.PORTABLE_EVIDENCE),
|
|
85
|
+
'change-packet': semantic('respond', CAPABILITY_IDS.PORTABLE_EVIDENCE),
|
|
86
|
+
'sharing-review': semantic('respond', CAPABILITY_IDS.PORTABLE_EVIDENCE),
|
|
87
|
+
'workflow-plan': semantic('assure', CAPABILITY_IDS.OFFLINE_REVIEW),
|
|
88
|
+
'workflow-run': semantic('assure', CAPABILITY_IDS.WORKFLOW_EXECUTION),
|
|
89
|
+
diff: semantic('assure', CAPABILITY_IDS.OFFLINE_REVIEW),
|
|
90
|
+
reconcile: semantic('assure', CAPABILITY_IDS.OFFLINE_REVIEW),
|
|
91
|
+
timeline: semantic('assure', CAPABILITY_IDS.OFFLINE_REVIEW),
|
|
92
|
+
export: semantic('respond', CAPABILITY_IDS.PORTABLE_EVIDENCE),
|
|
93
|
+
});
|
|
@@ -158,6 +158,15 @@ export const RISK_CALIBRATION_REPORT_PRIVACY_KEYS = Object.freeze([
|
|
|
158
158
|
export const RISK_CALIBRATION_REPORT_INTERPRETATION_KEYS = Object.freeze([
|
|
159
159
|
'authority', 'statement', 'automaticTuning', 'networkRequests', 'persisted',
|
|
160
160
|
]);
|
|
161
|
+
/** Stable disposition identities admitted by the versioned calibration schema. */
|
|
162
|
+
export const RISK_CALIBRATION_DISPOSITIONS = Object.freeze([
|
|
163
|
+
'unreviewed',
|
|
164
|
+
'suspicious',
|
|
165
|
+
'confirmed_abuse',
|
|
166
|
+
'false_positive',
|
|
167
|
+
'expected',
|
|
168
|
+
'closed_no_action',
|
|
169
|
+
]);
|
|
161
170
|
export const RISK_CALIBRATION_DATASET_COMPATIBILITY = defineSchemaCompatibility({
|
|
162
171
|
id: 'cli.risk-calibration-dataset',
|
|
163
172
|
kind: 'cli_document',
|
|
@@ -337,8 +346,8 @@ export const RISK_CALIBRATION_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
|
|
|
337
346
|
{
|
|
338
347
|
id: 'risk-calibration-report-v3-detailed',
|
|
339
348
|
path: 'test/fixtures/risk-calibration-report-v3-detailed.json',
|
|
340
|
-
bytes:
|
|
341
|
-
sha256: '
|
|
349
|
+
bytes: 7_020,
|
|
350
|
+
sha256: 'a5cba1681c170c79f8891037f47efc93e0ae6ce5d247418ff3f1c9a81a352922',
|
|
342
351
|
contentDigestSha256: null,
|
|
343
352
|
schema: RISK_CALIBRATION_REPORT_SCHEMA,
|
|
344
353
|
version: RISK_CALIBRATION_REPORT_VERSION,
|
|
@@ -351,8 +360,8 @@ export const RISK_CALIBRATION_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
|
|
|
351
360
|
{
|
|
352
361
|
id: 'risk-calibration-report-v3-summary',
|
|
353
362
|
path: 'test/fixtures/risk-calibration-report-v3-summary.json',
|
|
354
|
-
bytes:
|
|
355
|
-
sha256: '
|
|
363
|
+
bytes: 5_480,
|
|
364
|
+
sha256: '71b7fe1ee613b2ab32bd1f5646ee765985022c904224e1a08f4577c1d1d06240',
|
|
356
365
|
contentDigestSha256: null,
|
|
357
366
|
schema: RISK_CALIBRATION_REPORT_SCHEMA,
|
|
358
367
|
version: RISK_CALIBRATION_REPORT_VERSION,
|
|
@@ -1110,9 +1119,7 @@ const FACTOR_SPEC = objectOf([
|
|
|
1110
1119
|
const REPORT_RECORD_SPEC = objectOf([
|
|
1111
1120
|
{ key: 'id', value: boundedText(MAX_RISK_CALIBRATION_RECORD_ID_LENGTH) },
|
|
1112
1121
|
{ key: 'domain', value: boundedText(MAX_RISK_CALIBRATION_DOMAIN_LENGTH) },
|
|
1113
|
-
{ key: 'analystDisposition', value: boundedText(MAX_RISK_CALIBRATION_DISPOSITION_LENGTH,
|
|
1114
|
-
'unreviewed', 'suspicious', 'confirmed_abuse', 'false_positive', 'expected', 'closed_no_action',
|
|
1115
|
-
]) },
|
|
1122
|
+
{ key: 'analystDisposition', value: boundedText(MAX_RISK_CALIBRATION_DISPOSITION_LENGTH, RISK_CALIBRATION_DISPOSITIONS) },
|
|
1116
1123
|
{ key: 'reviewReasonCode', value: nullable(boundedText(MAX_RISK_CALIBRATION_REVIEW_REASON_LENGTH)) },
|
|
1117
1124
|
{ key: 'interoperabilityTags', value: arrayOf(boundedText(MAX_RISK_CALIBRATION_STRING_LENGTH), 0, 32) },
|
|
1118
1125
|
{ key: 'metricClass', value: boundedText(16, ['positive', 'negative', 'excluded']) },
|
|
@@ -1179,9 +1186,7 @@ const DATASET_EVIDENCE_SPEC = objectOf([
|
|
|
1179
1186
|
const DATASET_RECORD_SPEC = objectOf([
|
|
1180
1187
|
{ key: 'id', value: boundedText(MAX_RISK_CALIBRATION_RECORD_ID_LENGTH, null, 1, canonicalRiskCalibrationIdentifier) },
|
|
1181
1188
|
{ key: 'domain', value: boundedText(MAX_RISK_CALIBRATION_DOMAIN_LENGTH, null, 1, validRiskCalibrationDomain) },
|
|
1182
|
-
{ key: 'analystDisposition', value: boundedText(MAX_RISK_CALIBRATION_DISPOSITION_LENGTH,
|
|
1183
|
-
'unreviewed', 'suspicious', 'confirmed_abuse', 'false_positive', 'expected', 'closed_no_action',
|
|
1184
|
-
]) },
|
|
1189
|
+
{ key: 'analystDisposition', value: boundedText(MAX_RISK_CALIBRATION_DISPOSITION_LENGTH, RISK_CALIBRATION_DISPOSITIONS) },
|
|
1185
1190
|
{ key: 'reviewReasonCode', value: boundedText(MAX_RISK_CALIBRATION_REVIEW_REASON_LENGTH, RISK_CALIBRATION_REVIEW_REASON_VALUES), optional: true },
|
|
1186
1191
|
{ key: 'evidence', value: DATASET_EVIDENCE_SPEC },
|
|
1187
1192
|
]);
|
|
@@ -4,8 +4,13 @@ import { defineSchemaLifecycleFamily, } from "./schema-lifecycle.mjs";
|
|
|
4
4
|
export const WORKSPACE_CONTRACT_OWNER = 'packages/contracts/workspace-portability.mts';
|
|
5
5
|
export const BRAND_PROFILE_SCHEMA = 'whoisleuth.brand-profiles';
|
|
6
6
|
export const PUBLIC_BRAND_PROFILE_SCHEMA_VERSION = 6;
|
|
7
|
-
export const
|
|
8
|
-
export const
|
|
7
|
+
export const LATEST_PUBLIC_BRAND_PROFILE_SCHEMA_VERSION = 7;
|
|
8
|
+
export const BRAND_PROFILE_SCHEMA_VERSION = 8;
|
|
9
|
+
export const BRAND_PROFILE_BROWSER_SUPPORTED_VERSIONS = Object.freeze([
|
|
10
|
+
PUBLIC_BRAND_PROFILE_SCHEMA_VERSION,
|
|
11
|
+
LATEST_PUBLIC_BRAND_PROFILE_SCHEMA_VERSION,
|
|
12
|
+
BRAND_PROFILE_SCHEMA_VERSION,
|
|
13
|
+
]);
|
|
9
14
|
export const SUPPORTED_BRAND_PROFILE_SCHEMA_VERSIONS = BRAND_PROFILE_BROWSER_SUPPORTED_VERSIONS;
|
|
10
15
|
export const MAX_PROFILES = 100;
|
|
11
16
|
export const MAX_PROFILE_VALUES = 200;
|
|
@@ -15,7 +20,10 @@ export const MAX_PROFILE_IMPORT_BYTES = 2 * 1024 * 1024;
|
|
|
15
20
|
export const MAX_PROFILE_NAME_LENGTH = 100;
|
|
16
21
|
export const MAX_PROFILE_TEXT_LENGTH = 200;
|
|
17
22
|
export const MAX_PROFILE_DOMAIN_LENGTH = 253;
|
|
23
|
+
export const MAX_PROFILE_URL_LENGTH = 2_048;
|
|
18
24
|
export const MAX_PROFILE_TLD_LENGTH = 63;
|
|
25
|
+
export const MAX_OFFICIAL_CHANNELS = 30;
|
|
26
|
+
export const MAX_RIGHTS_REFERENCES = 30;
|
|
19
27
|
export const MAX_DKIM_SELECTOR_LENGTH = 253;
|
|
20
28
|
export const MAX_DKIM_SELECTORS = 10;
|
|
21
29
|
export const MAX_PROTECTION_ATTESTATIONS = 6;
|
|
@@ -151,7 +159,7 @@ export const BULK_SESSION_BROWSER_STORE_LIFECYCLE_SCHEMA = 'whoisleuth.browser.b
|
|
|
151
159
|
export const INVESTIGATION_TEMPLATE_BROWSER_STORE_LIFECYCLE_SCHEMA = 'whoisleuth.browser.investigation-template-store';
|
|
152
160
|
export const BULK_REVIEW_BROWSER_STORE_LIFECYCLE_SCHEMA = 'whoisleuth.browser.bulk-review-store';
|
|
153
161
|
export const WORKSPACE_PORTABILITY_IDENTITY_CONSTANTS = Object.freeze([
|
|
154
|
-
'BRAND_PROFILE_SCHEMA', 'BRAND_PROFILE_SCHEMA_VERSION', 'BRAND_PROFILE_BROWSER_SUPPORTED_VERSIONS', 'SUPPORTED_BRAND_PROFILE_SCHEMA_VERSIONS',
|
|
162
|
+
'BRAND_PROFILE_SCHEMA', 'PUBLIC_BRAND_PROFILE_SCHEMA_VERSION', 'LATEST_PUBLIC_BRAND_PROFILE_SCHEMA_VERSION', 'BRAND_PROFILE_SCHEMA_VERSION', 'BRAND_PROFILE_BROWSER_SUPPORTED_VERSIONS', 'SUPPORTED_BRAND_PROFILE_SCHEMA_VERSIONS',
|
|
155
163
|
'CAMPAIGN_SCHEMA', 'CAMPAIGN_SCHEMA_VERSION', 'CAMPAIGN_BROWSER_SUPPORTED_VERSIONS', 'CAMPAIGN_EXPORT_SUPPORTED_VERSIONS',
|
|
156
164
|
'WATCHLIST_SCHEMA', 'WATCHLIST_SCHEMA_VERSION', 'WATCHLIST_BROWSER_SUPPORTED_VERSIONS', 'WATCHLIST_EXPORT_SUPPORTED_VERSIONS',
|
|
157
165
|
'SHORTLIST_SCHEMA', 'SHORTLIST_SCHEMA_VERSION', 'SHORTLIST_BROWSER_SUPPORTED_VERSIONS', 'SUPPORTED_SHORTLIST_SCHEMA_VERSIONS',
|
|
@@ -169,7 +177,8 @@ export const WORKSPACE_PORTABILITY_IDENTITY_CONSTANTS = Object.freeze([
|
|
|
169
177
|
]);
|
|
170
178
|
export const WORKSPACE_PORTABILITY_BOUND_CONSTANTS = Object.freeze([
|
|
171
179
|
'MAX_PROFILES', 'MAX_PROFILE_VALUES', 'MAX_PROFILE_VALUE_INPUTS', 'MAX_PROFILE_STORE_BYTES', 'MAX_PROFILE_IMPORT_BYTES',
|
|
172
|
-
'MAX_PROFILE_NAME_LENGTH', 'MAX_PROFILE_TEXT_LENGTH', 'MAX_PROFILE_DOMAIN_LENGTH', '
|
|
180
|
+
'MAX_PROFILE_NAME_LENGTH', 'MAX_PROFILE_TEXT_LENGTH', 'MAX_PROFILE_DOMAIN_LENGTH', 'MAX_PROFILE_URL_LENGTH', 'MAX_PROFILE_TLD_LENGTH',
|
|
181
|
+
'MAX_OFFICIAL_CHANNELS', 'MAX_RIGHTS_REFERENCES', 'MAX_DKIM_SELECTOR_LENGTH',
|
|
173
182
|
'MAX_DKIM_SELECTORS', 'MAX_PROTECTION_ATTESTATIONS', 'MAX_DESIRED_POSTURE_BASELINES', 'MAX_DESIRED_POSTURE_RECORDS',
|
|
174
183
|
'MAX_DESIRED_POSTURE_SUPPRESSIONS', 'MAX_DESIRED_POSTURE_OBSERVATIONS', 'MAX_DESIRED_POSTURE_CHANGE_WINDOWS',
|
|
175
184
|
'MAX_CAMPAIGNS', 'MAX_CAMPAIGN_DOMAINS', 'MAX_CAMPAIGN_NAME_LENGTH', 'MAX_CAMPAIGN_DESCRIPTION_LENGTH',
|
|
@@ -218,7 +227,7 @@ export const BRAND_PROFILE_BROWSER_COMPATIBILITY = defineSchemaCompatibility({
|
|
|
218
227
|
supportedVersions: BRAND_PROFILE_BROWSER_SUPPORTED_VERSIONS, acceptsUnversionedLegacy: false,
|
|
219
228
|
futureVersionBehavior: 'preserve_without_write', migration: 'normalize_to_current', writeSemantics: 'normalized_rewrite',
|
|
220
229
|
byteBudget: MAX_PROFILE_STORE_BYTES, owner: WORKSPACE_CONTRACT_OWNER,
|
|
221
|
-
note: '
|
|
230
|
+
note: 'Supported version 6 and 7 profiles migrate directly to version 8. Version 7 gives each approved change window a durable opaque identity; version 8 adds bounded official-channel and rights-reference records.',
|
|
222
231
|
});
|
|
223
232
|
export const CAMPAIGN_BROWSER_COMPATIBILITY = defineSchemaCompatibility({
|
|
224
233
|
id: 'browser.campaigns', kind: 'browser_store', schema: null, currentVersion: CAMPAIGN_SCHEMA_VERSION,
|
|
@@ -648,7 +657,7 @@ const WORKSPACE_LIFECYCLE_FIXTURE_SOURCE = Object.freeze([
|
|
|
648
657
|
"version": 6,
|
|
649
658
|
"role": "historical",
|
|
650
659
|
"expectation": "normalises_to_current_output",
|
|
651
|
-
"expectedOutputFixtureId": "workspace.browser.brand.
|
|
660
|
+
"expectedOutputFixtureId": "workspace.browser.brand.v8",
|
|
652
661
|
"scope": "repository",
|
|
653
662
|
"shapeId": "workspace.browser.brand.shape"
|
|
654
663
|
},
|
|
@@ -660,6 +669,20 @@ const WORKSPACE_LIFECYCLE_FIXTURE_SOURCE = Object.freeze([
|
|
|
660
669
|
"contentDigestSha256": null,
|
|
661
670
|
"schema": "whoisleuth.browser.brand-profile-store",
|
|
662
671
|
"version": 7,
|
|
672
|
+
"role": "historical",
|
|
673
|
+
"expectation": "normalises_to_current_output",
|
|
674
|
+
"expectedOutputFixtureId": "workspace.browser.brand.v8",
|
|
675
|
+
"scope": "repository",
|
|
676
|
+
"shapeId": "workspace.browser.brand.shape"
|
|
677
|
+
},
|
|
678
|
+
{
|
|
679
|
+
"id": "workspace.browser.brand.v8",
|
|
680
|
+
"path": "test/fixtures/workspace-lifecycle/browser-brand-v8.json",
|
|
681
|
+
"bytes": 37,
|
|
682
|
+
"sha256": "6c2310d9da626cb8acd88a47ccb60fa46051a63b23675cdc4801794ec5cc52e3",
|
|
683
|
+
"contentDigestSha256": null,
|
|
684
|
+
"schema": "whoisleuth.browser.brand-profile-store",
|
|
685
|
+
"version": 8,
|
|
663
686
|
"role": "current",
|
|
664
687
|
"expectation": "accepted_exact",
|
|
665
688
|
"expectedOutputFixtureId": null,
|
|
@@ -830,7 +853,7 @@ const WORKSPACE_LIFECYCLE_FIXTURE_SOURCE = Object.freeze([
|
|
|
830
853
|
"version": 6,
|
|
831
854
|
"role": "historical",
|
|
832
855
|
"expectation": "normalises_to_current_output",
|
|
833
|
-
"expectedOutputFixtureId": "workspace.portable.brand.
|
|
856
|
+
"expectedOutputFixtureId": "workspace.portable.brand.v8",
|
|
834
857
|
"scope": "repository",
|
|
835
858
|
"shapeId": "workspace.portable.brand.shape"
|
|
836
859
|
},
|
|
@@ -842,6 +865,20 @@ const WORKSPACE_LIFECYCLE_FIXTURE_SOURCE = Object.freeze([
|
|
|
842
865
|
"contentDigestSha256": null,
|
|
843
866
|
"schema": "whoisleuth.brand-profiles",
|
|
844
867
|
"version": 7,
|
|
868
|
+
"role": "historical",
|
|
869
|
+
"expectation": "normalises_to_current_output",
|
|
870
|
+
"expectedOutputFixtureId": "workspace.portable.brand.v8",
|
|
871
|
+
"scope": "repository",
|
|
872
|
+
"shapeId": "workspace.portable.brand.shape"
|
|
873
|
+
},
|
|
874
|
+
{
|
|
875
|
+
"id": "workspace.portable.brand.v8",
|
|
876
|
+
"path": "test/fixtures/workspace-lifecycle/portable-brand-v8.json",
|
|
877
|
+
"bytes": 122,
|
|
878
|
+
"sha256": "4e4af17a0e19cb841ea50fe150caa3de79b978311b51a334ed7f6f3d2e1c6a1c",
|
|
879
|
+
"contentDigestSha256": null,
|
|
880
|
+
"schema": "whoisleuth.brand-profiles",
|
|
881
|
+
"version": 8,
|
|
845
882
|
"role": "current",
|
|
846
883
|
"expectation": "accepted_exact",
|
|
847
884
|
"expectedOutputFixtureId": null,
|
|
@@ -171,15 +171,47 @@ export function canonicalMxRecord(value) {
|
|
|
171
171
|
}
|
|
172
172
|
return exchange(candidate);
|
|
173
173
|
}
|
|
174
|
-
function caaValue(value) {
|
|
175
|
-
|
|
174
|
+
function caaValue(value, tag) {
|
|
175
|
+
const candidate = recordText(value, MAX_DOMAIN_CONTROL_CAA_VALUE_LENGTH)
|
|
176
|
+
.replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/u, '$1$2').trim();
|
|
177
|
+
if (!candidate)
|
|
178
|
+
return '';
|
|
179
|
+
if (tag === 'issue' || tag === 'issuewild') {
|
|
180
|
+
const [issuer = '', ...parameters] = candidate.split(';');
|
|
181
|
+
const normalizedIssuer = issuer.trim().toLowerCase().replace(/\.$/u, '');
|
|
182
|
+
return [normalizedIssuer, ...parameters.map((parameter) => parameter.trim())].join('; ');
|
|
183
|
+
}
|
|
184
|
+
const scheme = /^([a-z][a-z0-9+.-]*):(.*)$/isu.exec(candidate);
|
|
185
|
+
if (!scheme)
|
|
186
|
+
return candidate;
|
|
187
|
+
const normalizedScheme = scheme[1].toLowerCase();
|
|
188
|
+
if (normalizedScheme === 'http' || normalizedScheme === 'https') {
|
|
189
|
+
try {
|
|
190
|
+
const url = new URL(candidate);
|
|
191
|
+
url.protocol = normalizedScheme;
|
|
192
|
+
url.hostname = url.hostname.toLowerCase();
|
|
193
|
+
return url.toString();
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return candidate;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (normalizedScheme === 'mailto') {
|
|
200
|
+
const [address = '', ...query] = scheme[2].split('?');
|
|
201
|
+
const at = address.lastIndexOf('@');
|
|
202
|
+
const normalizedAddress = at > 0
|
|
203
|
+
? `${address.slice(0, at)}@${address.slice(at + 1).toLowerCase()}`
|
|
204
|
+
: address;
|
|
205
|
+
return `${normalizedScheme}:${normalizedAddress}${query.length ? `?${query.join('?')}` : ''}`;
|
|
206
|
+
}
|
|
207
|
+
return `${normalizedScheme}:${scheme[2]}`;
|
|
176
208
|
}
|
|
177
209
|
export function canonicalCaaRecord(value) {
|
|
178
210
|
const item = structuredRecord(value, CAA_RECORD_KEYS, 'Domain control CAA record');
|
|
179
211
|
if (item) {
|
|
180
212
|
const flags = canonicalIntegerAlias(item, ['critical', 'flags'], MAX_DOMAIN_CONTROL_CAA_FLAGS, 'Domain control CAA flags', 0);
|
|
181
213
|
const tag = recordText(item.tag, MAX_DOMAIN_CONTROL_CAA_TAG_LENGTH).toLowerCase();
|
|
182
|
-
const payload = caaValue(item.value);
|
|
214
|
+
const payload = caaValue(item.value, tag);
|
|
183
215
|
return flags !== null && /^(?:issue|issuewild|iodef)$/u.test(tag) && payload
|
|
184
216
|
? `${flags} ${tag} ${payload}`
|
|
185
217
|
: '';
|
|
@@ -190,7 +222,7 @@ export function canonicalCaaRecord(value) {
|
|
|
190
222
|
return '';
|
|
191
223
|
const flags = recordInteger(match[1], MAX_DOMAIN_CONTROL_CAA_FLAGS);
|
|
192
224
|
const tag = match[2]?.toLowerCase() ?? '';
|
|
193
|
-
const payload = caaValue(match[3]);
|
|
225
|
+
const payload = caaValue(match[3], tag);
|
|
194
226
|
return flags !== null && payload ? `${flags} ${tag} ${payload}` : '';
|
|
195
227
|
}
|
|
196
228
|
export function canonicalDsRecord(value) {
|
|
@@ -405,7 +437,9 @@ function hostnames(value) {
|
|
|
405
437
|
return [...new Set(names)].sort(compareCodeUnits).slice(0, MAX_CANONICAL_DOMAIN_CONTROL_RECORDS);
|
|
406
438
|
}
|
|
407
439
|
function digest(value) {
|
|
408
|
-
|
|
440
|
+
if (typeof value !== 'string' || CONTROL_RE.test(value))
|
|
441
|
+
return null;
|
|
442
|
+
const candidate = value.trim().toLowerCase();
|
|
409
443
|
return SPKI_DIGEST_PATTERN.test(candidate) ? candidate : null;
|
|
410
444
|
}
|
|
411
445
|
function normalizeEntry(value) {
|
|
@@ -415,6 +449,12 @@ function normalizeEntry(value) {
|
|
|
415
449
|
const domain = domainControlName(source.domain);
|
|
416
450
|
if (!domain)
|
|
417
451
|
return null;
|
|
452
|
+
const tlsSpkiSha256 = digest(source.tlsSpkiSha256);
|
|
453
|
+
if (source.tlsSpkiSha256 !== undefined && source.tlsSpkiSha256 !== null
|
|
454
|
+
&& (typeof source.tlsSpkiSha256 !== 'string' || source.tlsSpkiSha256.trim())
|
|
455
|
+
&& !tlsSpkiSha256) {
|
|
456
|
+
throw new TypeError('Domain control TLS public-key fingerprint must be exactly 64 hexadecimal characters.');
|
|
457
|
+
}
|
|
418
458
|
return Object.freeze({
|
|
419
459
|
domain,
|
|
420
460
|
nameservers: Object.freeze(hostnames(source.nameservers)),
|
|
@@ -422,7 +462,7 @@ function normalizeEntry(value) {
|
|
|
422
462
|
mx: Object.freeze(canonicalDomainControlRecordList(source.mx, 'mx')),
|
|
423
463
|
caa: Object.freeze(canonicalDomainControlRecordList(source.caa, 'caa')),
|
|
424
464
|
tlsIssuer: text(source.tlsIssuer, MAX_DOMAIN_CONTROL_TEXT_LENGTH)?.toLowerCase() ?? null,
|
|
425
|
-
tlsSpkiSha256
|
|
465
|
+
tlsSpkiSha256,
|
|
426
466
|
registrarLock: source.registrarLock === 'required' || source.registrarLock === 'not_required'
|
|
427
467
|
? source.registrarLock
|
|
428
468
|
: null,
|
|
@@ -14,6 +14,7 @@ export const EXTERNAL_FINDING_CATEGORIES = [
|
|
|
14
14
|
'registration',
|
|
15
15
|
'reputation',
|
|
16
16
|
];
|
|
17
|
+
export const MAX_EXTERNAL_FINDING_SUMMARY_LENGTH = 900;
|
|
17
18
|
const CATEGORIES = new Set(EXTERNAL_FINDING_CATEGORIES);
|
|
18
19
|
const COMPLETENESS = new Set(['complete', 'inconclusive', 'partial', 'unknown']);
|
|
19
20
|
const ROOT_KEYS = new Set(['schema', 'schemaVersion', 'source', 'findings']);
|
|
@@ -219,7 +220,7 @@ export function parseExternalFindingsDocument(value) {
|
|
|
219
220
|
domain,
|
|
220
221
|
category: item.category,
|
|
221
222
|
evidenceClass,
|
|
222
|
-
summary: requiredText(item.summary,
|
|
223
|
+
summary: requiredText(item.summary, MAX_EXTERNAL_FINDING_SUMMARY_LENGTH, `Finding ${index + 1} summary`),
|
|
223
224
|
observedAt: iso(item.observedAt, `Finding ${index + 1} observation time`),
|
|
224
225
|
completeness: item.completeness,
|
|
225
226
|
limitations: limitations(item.limitations, index),
|
|
@@ -399,7 +400,7 @@ function targetedFinding(finding, caseDomain) {
|
|
|
399
400
|
if (finding.domain === caseDomain)
|
|
400
401
|
return finding;
|
|
401
402
|
const prefix = `Captured hostname ${finding.domain}. `;
|
|
402
|
-
const maximumSummaryLength =
|
|
403
|
+
const maximumSummaryLength = MAX_EXTERNAL_FINDING_SUMMARY_LENGTH;
|
|
403
404
|
const remaining = maximumSummaryLength - prefix.length;
|
|
404
405
|
const summary = remaining > 1 && finding.summary.length > remaining
|
|
405
406
|
? `${prefix}${finding.summary.slice(0, remaining - 1).trimEnd()}…`
|
|
@@ -130,13 +130,16 @@ function hostname(value) {
|
|
|
130
130
|
return null;
|
|
131
131
|
return candidate;
|
|
132
132
|
}
|
|
133
|
-
function
|
|
133
|
+
function webOrigin(value) {
|
|
134
134
|
const candidate = text(value, 2_048);
|
|
135
135
|
if (!candidate)
|
|
136
136
|
return null;
|
|
137
137
|
try {
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
const parsed = new URL(/^[a-z][a-z\d+.-]*:\/\//iu.test(candidate) ? candidate : `https://${candidate}`);
|
|
139
|
+
const parsedHostname = hostname(parsed.hostname);
|
|
140
|
+
return parsedHostname && parsed.origin !== 'null'
|
|
141
|
+
? Object.freeze({ origin: parsed.origin, hostname: parsedHostname })
|
|
142
|
+
: null;
|
|
140
143
|
}
|
|
141
144
|
catch {
|
|
142
145
|
return null;
|
|
@@ -233,12 +236,12 @@ export function buildLookupAssetGraph(input) {
|
|
|
233
236
|
...textList(input.profileDomains?.partner, 32),
|
|
234
237
|
...textList(input.profileDomains?.allowlisted, 32),
|
|
235
238
|
].map((value) => value.toLowerCase().replace(/\.$/u, '')));
|
|
236
|
-
const trustBoundary = (candidate, sourceHost = target) => {
|
|
239
|
+
const trustBoundary = (candidate, sourceHost = target, candidateOrigin = null, sourceOrigin = null) => {
|
|
237
240
|
if (!candidate)
|
|
238
241
|
return 'unresolved';
|
|
239
|
-
if (
|
|
242
|
+
if (candidateOrigin && sourceOrigin && candidateOrigin === sourceOrigin)
|
|
240
243
|
return 'same_origin';
|
|
241
|
-
if (candidate === target || candidate.endsWith(`.${target}`))
|
|
244
|
+
if (candidate === sourceHost || candidate === target || candidate.endsWith(`.${target}`))
|
|
242
245
|
return 'same_registrable_domain';
|
|
243
246
|
if (reviewedProfileDomains.has(candidate))
|
|
244
247
|
return 'reviewed_profile';
|
|
@@ -414,7 +417,8 @@ export function buildLookupAssetGraph(input) {
|
|
|
414
417
|
}
|
|
415
418
|
}
|
|
416
419
|
const httpEvidence = record(input.httpEvidence);
|
|
417
|
-
const
|
|
420
|
+
const finalOrigin = webOrigin(httpEvidence.finalUrl);
|
|
421
|
+
const finalHost = finalOrigin?.hostname ?? null;
|
|
418
422
|
const finalHostId = finalHost
|
|
419
423
|
? connect('hostname', finalHost, {
|
|
420
424
|
kind: 'redirects-to',
|
|
@@ -429,9 +433,9 @@ export function buildLookupAssetGraph(input) {
|
|
|
429
433
|
}, 'Observed final website origin')
|
|
430
434
|
: null;
|
|
431
435
|
const identitySource = finalHostId || targetId;
|
|
432
|
-
const
|
|
433
|
-
if (
|
|
434
|
-
connect('origin',
|
|
436
|
+
const canonicalOrigin = webOrigin(record(input.pageCanonical).url);
|
|
437
|
+
if (canonicalOrigin) {
|
|
438
|
+
connect('origin', canonicalOrigin.origin, {
|
|
435
439
|
kind: 'declares-canonical',
|
|
436
440
|
label: 'declares canonical origin',
|
|
437
441
|
sourceLabel: 'HTML canonical metadata',
|
|
@@ -440,12 +444,12 @@ export function buildLookupAssetGraph(input) {
|
|
|
440
444
|
limitations: limitations(record(input.pageIdentity).limitations),
|
|
441
445
|
lenses: ['identity'],
|
|
442
446
|
href: '#evidence-page',
|
|
443
|
-
boundary: trustBoundary(
|
|
447
|
+
boundary: trustBoundary(canonicalOrigin.hostname, finalHost || target, canonicalOrigin.origin, finalOrigin?.origin ?? null),
|
|
444
448
|
}, 'Publisher-declared canonical origin', identitySource);
|
|
445
449
|
}
|
|
446
|
-
const
|
|
447
|
-
if (
|
|
448
|
-
connect('origin',
|
|
450
|
+
const openGraphOrigin = webOrigin(record(input.pageOpenGraphUrl).url);
|
|
451
|
+
if (openGraphOrigin) {
|
|
452
|
+
connect('origin', openGraphOrigin.origin, {
|
|
449
453
|
kind: 'declares-open-graph',
|
|
450
454
|
label: 'declares Open Graph origin',
|
|
451
455
|
sourceLabel: 'Open Graph metadata',
|
|
@@ -454,13 +458,13 @@ export function buildLookupAssetGraph(input) {
|
|
|
454
458
|
limitations: limitations(record(input.pageIdentity).limitations),
|
|
455
459
|
lenses: ['identity'],
|
|
456
460
|
href: '#evidence-page',
|
|
457
|
-
boundary: trustBoundary(
|
|
461
|
+
boundary: trustBoundary(openGraphOrigin.hostname, finalHost || target, openGraphOrigin.origin, finalOrigin?.origin ?? null),
|
|
458
462
|
}, 'Publisher-declared Open Graph origin', identitySource);
|
|
459
463
|
}
|
|
460
464
|
for (const origin of textList(record(input.pageForms).externalActionOrigins)) {
|
|
461
|
-
const
|
|
462
|
-
if (
|
|
463
|
-
connect('origin',
|
|
465
|
+
const destination = webOrigin(origin);
|
|
466
|
+
if (destination) {
|
|
467
|
+
connect('origin', destination.origin, {
|
|
464
468
|
kind: 'form-destination',
|
|
465
469
|
label: 'form may submit to',
|
|
466
470
|
sourceLabel: 'Static HTML form',
|
|
@@ -469,14 +473,14 @@ export function buildLookupAssetGraph(input) {
|
|
|
469
473
|
limitations: ['A declared form action does not prove that a user submitted data or that the endpoint received it.'],
|
|
470
474
|
lenses: ['identity'],
|
|
471
475
|
href: '#evidence-page',
|
|
472
|
-
boundary: trustBoundary(
|
|
476
|
+
boundary: trustBoundary(destination.hostname, finalHost || target, destination.origin, finalOrigin?.origin ?? null),
|
|
473
477
|
}, 'External form-action origin', identitySource);
|
|
474
478
|
}
|
|
475
479
|
}
|
|
476
480
|
for (const origin of textList(record(input.pageResources).externalOrigins)) {
|
|
477
|
-
const
|
|
478
|
-
if (
|
|
479
|
-
connect('
|
|
481
|
+
const resource = webOrigin(origin);
|
|
482
|
+
if (resource) {
|
|
483
|
+
connect('origin', resource.origin, {
|
|
480
484
|
kind: 'loads-from',
|
|
481
485
|
label: 'references resources from',
|
|
482
486
|
sourceLabel: 'Static HTML resources',
|
|
@@ -485,7 +489,7 @@ export function buildLookupAssetGraph(input) {
|
|
|
485
489
|
limitations: ['A static resource reference does not prove that a browser loaded the resource or disclosed data to it.'],
|
|
486
490
|
lenses: ['identity'],
|
|
487
491
|
href: '#evidence-page',
|
|
488
|
-
boundary: trustBoundary(
|
|
492
|
+
boundary: trustBoundary(resource.hostname, finalHost || target, resource.origin, finalOrigin?.origin ?? null),
|
|
489
493
|
}, 'External resource origin', identitySource);
|
|
490
494
|
}
|
|
491
495
|
}
|
|
@@ -507,7 +511,7 @@ export function buildLookupAssetGraph(input) {
|
|
|
507
511
|
const structuredIdentity = record(input.structuredDataIdentity);
|
|
508
512
|
for (const entity of records(structuredIdentity.entities, 12)) {
|
|
509
513
|
const entityLabel = text(entity.name, 180);
|
|
510
|
-
const declaredOrigin =
|
|
514
|
+
const declaredOrigin = webOrigin(entity.declaredOrigin);
|
|
511
515
|
const entityId = entityLabel
|
|
512
516
|
? connect('identity', entityLabel, {
|
|
513
517
|
kind: 'declares-publisher',
|
|
@@ -524,7 +528,7 @@ export function buildLookupAssetGraph(input) {
|
|
|
524
528
|
}, textList(entity.types, 5).join(', ') || 'Publisher-declared entity', identitySource)
|
|
525
529
|
: identitySource;
|
|
526
530
|
if (declaredOrigin) {
|
|
527
|
-
connect('origin', declaredOrigin, {
|
|
531
|
+
connect('origin', declaredOrigin.origin, {
|
|
528
532
|
kind: 'declares-origin',
|
|
529
533
|
label: 'declares origin',
|
|
530
534
|
sourceLabel: 'Structured identity metadata',
|
|
@@ -533,7 +537,7 @@ export function buildLookupAssetGraph(input) {
|
|
|
533
537
|
limitations: limitations(structuredIdentity.limitations),
|
|
534
538
|
lenses: ['identity'],
|
|
535
539
|
href: '#evidence-structured-identity',
|
|
536
|
-
boundary: trustBoundary(declaredOrigin, finalHost || target),
|
|
540
|
+
boundary: trustBoundary(declaredOrigin.hostname, finalHost || target, declaredOrigin.origin, finalOrigin?.origin ?? null),
|
|
537
541
|
}, 'Publisher-declared structured-data origin', entityId || identitySource);
|
|
538
542
|
}
|
|
539
543
|
for (const sameAsHost of textList(entity.sameAsHosts, 12)) {
|
|
@@ -666,7 +670,11 @@ export function buildLookupAssetGraph(input) {
|
|
|
666
670
|
}
|
|
667
671
|
}
|
|
668
672
|
const issuer = record(input.tlsIssuer);
|
|
669
|
-
const
|
|
673
|
+
const issuerOrganizations = textList(issuer.organizations, 8);
|
|
674
|
+
const issuerCommonNames = textList(issuer.commonNames, 8);
|
|
675
|
+
const issuerLabel = issuerOrganizations[0]
|
|
676
|
+
|| issuerCommonNames[0]
|
|
677
|
+
|| text(issuer.organization || issuer.commonName || issuer.CN, 180);
|
|
670
678
|
if (issuerLabel) {
|
|
671
679
|
const issuerId = addNode('issuer', issuerLabel, 'Certificate issuer');
|
|
672
680
|
if (issuerId) {
|