@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
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { PUBLIC_CASE_SCHEMA_VERSION, INCIDENT_CASE_SCHEMA_VERSION } from "../contracts/case-portability.mjs";
|
|
2
|
+
import { caseStatusIsClosed } from "./case-record-decisions.mjs";
|
|
3
|
+
const PRESERVE = Object.freeze({
|
|
4
|
+
durable: 'preserve', internal: 'preserve', trusted: 'preserve', public: 'preserve',
|
|
5
|
+
});
|
|
6
|
+
const PUBLIC_TRANSFORM = Object.freeze({ ...PRESERVE, public: 'transform' });
|
|
7
|
+
const PUBLIC_EXCLUDE = Object.freeze({ ...PRESERVE, public: 'exclude' });
|
|
8
|
+
const SHARED_EXCLUDE = Object.freeze({ ...PRESERVE, trusted: 'exclude', public: 'exclude' });
|
|
9
|
+
const SHARED_REDACT = Object.freeze({ ...PRESERVE, trusted: 'redact', public: 'redact' });
|
|
10
|
+
const TRUSTED_REDACT_PUBLIC_EXCLUDE = Object.freeze({
|
|
11
|
+
...PRESERVE, trusted: 'redact', public: 'exclude',
|
|
12
|
+
});
|
|
13
|
+
const PUBLIC_REDACT = Object.freeze({ ...PRESERVE, public: 'redact' });
|
|
14
|
+
function fieldRule(key, treatment, value, options = {}) {
|
|
15
|
+
const audienceExclusions = Object.freeze(Object.fromEntries(Object.entries(options.audienceExclusions ?? {}).map(([audience, exclusion]) => [
|
|
16
|
+
audience,
|
|
17
|
+
Object.freeze({ ...exclusion }),
|
|
18
|
+
])));
|
|
19
|
+
for (const audience of ['internal', 'trusted', 'public']) {
|
|
20
|
+
const requiresExplanation = treatment[audience] === 'exclude' || treatment[audience] === 'redact';
|
|
21
|
+
if (requiresExplanation !== Boolean(audienceExclusions[audience])) {
|
|
22
|
+
throw new TypeError(`Case field ${key} must explain every redacted or excluded ${audience} treatment exactly once.`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return Object.freeze({
|
|
26
|
+
key,
|
|
27
|
+
treatment,
|
|
28
|
+
value,
|
|
29
|
+
nestedSensitiveFields: Object.freeze([...(options.nestedSensitiveFields ?? [])]),
|
|
30
|
+
audienceExclusions,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function preservedField(key) {
|
|
34
|
+
return fieldRule(key, PRESERVE, (record) => record[key]);
|
|
35
|
+
}
|
|
36
|
+
const PUBLIC_OBSERVED_EFFECT_LIMITATION = 'Independent observed-effect review records were excluded from this public Case pack.';
|
|
37
|
+
const PUBLIC_CLOSURE_LIMITATION = 'Deliberate closure records were excluded from this public Case pack.';
|
|
38
|
+
function publicObservedEffectHistory(preV13HistoryUnavailable) {
|
|
39
|
+
return {
|
|
40
|
+
reviews: [],
|
|
41
|
+
omitted: 0,
|
|
42
|
+
preV13HistoryUnavailable,
|
|
43
|
+
limitations: [
|
|
44
|
+
...(preV13HistoryUnavailable
|
|
45
|
+
? ['Migrated from a pre-v13 Case; earlier independent observed-effect review history is unavailable.']
|
|
46
|
+
: []),
|
|
47
|
+
'Observed-effect reviews are independent point-in-time records; provider workflow events do not create or replace them.',
|
|
48
|
+
PUBLIC_OBSERVED_EFFECT_LIMITATION,
|
|
49
|
+
].sort(),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function publicClosureHistory(preV13HistoryUnavailable) {
|
|
53
|
+
return {
|
|
54
|
+
records: [],
|
|
55
|
+
omitted: 0,
|
|
56
|
+
preV13HistoryUnavailable,
|
|
57
|
+
limitations: [
|
|
58
|
+
...(preV13HistoryUnavailable
|
|
59
|
+
? ['Migrated from a pre-v13 Case; earlier deliberate closure history is unavailable.']
|
|
60
|
+
: []),
|
|
61
|
+
'Closure records are deliberate analyst actions and do not establish absence, safety, provider performance, or legal sufficiency.',
|
|
62
|
+
PUBLIC_CLOSURE_LIMITATION,
|
|
63
|
+
].sort(),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The complete current Case field policy. Browser persistence and ordinary
|
|
68
|
+
* exports deliberately share the `durable` projection. Case-pack audiences
|
|
69
|
+
* must classify every field here before a new Case property can compile.
|
|
70
|
+
*/
|
|
71
|
+
const CASE_FIELD_RULES = Object.freeze({
|
|
72
|
+
id: preservedField('id'),
|
|
73
|
+
domain: preservedField('domain'),
|
|
74
|
+
title: fieldRule('title', SHARED_EXCLUDE, (record, profile) => (profile === 'trusted' || profile === 'public' ? '' : record.title ?? ''), {
|
|
75
|
+
audienceExclusions: {
|
|
76
|
+
trusted: { label: 'Case titles', order: 1, sinceVersion: INCIDENT_CASE_SCHEMA_VERSION },
|
|
77
|
+
public: { label: 'Case titles', order: 1, sinceVersion: INCIDENT_CASE_SCHEMA_VERSION },
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
status: fieldRule('status', PUBLIC_TRANSFORM, (record, profile) => (profile === 'public' && caseStatusIsClosed(record.status) && record.closures.records.length
|
|
81
|
+
? 'reviewing'
|
|
82
|
+
: record.status)),
|
|
83
|
+
disposition: preservedField('disposition'),
|
|
84
|
+
reviewReasonCode: fieldRule('reviewReasonCode', PRESERVE, (record) => record.reviewReasonCode ?? null),
|
|
85
|
+
brandProfileIds: fieldRule('brandProfileIds', PUBLIC_EXCLUDE, (record, profile) => (profile === 'public' ? [] : [...record.brandProfileIds]), {
|
|
86
|
+
audienceExclusions: { public: { label: 'Brand Profile references', order: 2 } },
|
|
87
|
+
}),
|
|
88
|
+
tags: fieldRule('tags', PRESERVE, (record) => [...record.tags]),
|
|
89
|
+
notes: fieldRule('notes', SHARED_EXCLUDE, (record, profile) => (profile === 'trusted' || profile === 'public'
|
|
90
|
+
? []
|
|
91
|
+
: record.notes.map((item) => ({ ...item }))), {
|
|
92
|
+
audienceExclusions: {
|
|
93
|
+
trusted: { label: 'Case notes', order: 1 },
|
|
94
|
+
public: { label: 'Case notes', order: 1 },
|
|
95
|
+
},
|
|
96
|
+
}),
|
|
97
|
+
source: preservedField('source'),
|
|
98
|
+
evidenceHistory: fieldRule('evidenceHistory', PRESERVE, (record) => structuredClone(record.evidenceHistory)),
|
|
99
|
+
evidencePins: fieldRule('evidencePins', PRESERVE, (record) => structuredClone(record.evidencePins)),
|
|
100
|
+
decisions: fieldRule('decisions', PRESERVE, (record) => structuredClone(record.decisions)),
|
|
101
|
+
actions: fieldRule('actions', TRUSTED_REDACT_PUBLIC_EXCLUDE, (record, profile) => (profile === 'public'
|
|
102
|
+
? []
|
|
103
|
+
: profile === 'trusted'
|
|
104
|
+
? structuredClone(record.actions).map((item) => ({ ...item, recipient: '[redacted]' }))
|
|
105
|
+
: structuredClone(record.actions)), {
|
|
106
|
+
nestedSensitiveFields: ['recipient'],
|
|
107
|
+
audienceExclusions: {
|
|
108
|
+
trusted: { label: 'Recipient values', order: 2 },
|
|
109
|
+
public: { label: 'Actions and recipient values', order: 3 },
|
|
110
|
+
},
|
|
111
|
+
}),
|
|
112
|
+
assertions: fieldRule('assertions', PUBLIC_EXCLUDE, (record, profile) => (profile === 'public' ? [] : structuredClone(record.assertions)), {
|
|
113
|
+
audienceExclusions: { public: { label: 'Analyst assertions', order: 4 } },
|
|
114
|
+
}),
|
|
115
|
+
manualTrail: fieldRule('manualTrail', SHARED_REDACT, (record, profile) => (profile === 'trusted' || profile === 'public'
|
|
116
|
+
? structuredClone(record.manualTrail).map((item) => ({ ...item, target: null }))
|
|
117
|
+
: structuredClone(record.manualTrail)), {
|
|
118
|
+
nestedSensitiveFields: ['target'],
|
|
119
|
+
audienceExclusions: {
|
|
120
|
+
trusted: { label: 'Manual trail targets', order: 3 },
|
|
121
|
+
public: { label: 'Manual trail targets', order: 6 },
|
|
122
|
+
},
|
|
123
|
+
}),
|
|
124
|
+
sightings: fieldRule('sightings', PRESERVE, (record) => structuredClone(record.sightings)),
|
|
125
|
+
observedEffects: fieldRule('observedEffects', PUBLIC_REDACT, (record, profile) => (profile === 'public'
|
|
126
|
+
? publicObservedEffectHistory(record.observedEffects.preV13HistoryUnavailable)
|
|
127
|
+
: structuredClone(record.observedEffects)), {
|
|
128
|
+
audienceExclusions: {
|
|
129
|
+
public: { label: 'Independent observed-effect reviews and closure history', order: 8 },
|
|
130
|
+
},
|
|
131
|
+
}),
|
|
132
|
+
closures: fieldRule('closures', PUBLIC_REDACT, (record, profile) => (profile === 'public'
|
|
133
|
+
? publicClosureHistory(record.closures.preV13HistoryUnavailable)
|
|
134
|
+
: structuredClone(record.closures)), {
|
|
135
|
+
audienceExclusions: {
|
|
136
|
+
public: { label: 'Independent observed-effect reviews and closure history', order: 8 },
|
|
137
|
+
},
|
|
138
|
+
}),
|
|
139
|
+
branches: fieldRule('branches', PUBLIC_EXCLUDE, (record, profile) => (profile === 'public' ? [] : structuredClone(record.branches ?? [])), {
|
|
140
|
+
audienceExclusions: { public: { label: 'Investigation branches', order: 5 } },
|
|
141
|
+
}),
|
|
142
|
+
attachments: fieldRule('attachments', SHARED_EXCLUDE, (record, profile) => (profile === 'trusted' || profile === 'public' ? undefined : structuredClone(record.attachments)), {
|
|
143
|
+
audienceExclusions: {
|
|
144
|
+
trusted: { label: 'Retained file references and provenance', order: 5, sinceVersion: INCIDENT_CASE_SCHEMA_VERSION },
|
|
145
|
+
public: { label: 'Retained file references and provenance', order: 5, sinceVersion: INCIDENT_CASE_SCHEMA_VERSION },
|
|
146
|
+
},
|
|
147
|
+
}),
|
|
148
|
+
createdAt: preservedField('createdAt'),
|
|
149
|
+
updatedAt: preservedField('updatedAt'),
|
|
150
|
+
});
|
|
151
|
+
function projectCase(record, profile) {
|
|
152
|
+
const projected = {};
|
|
153
|
+
for (const key of Object.keys(CASE_FIELD_RULES)) {
|
|
154
|
+
const rule = CASE_FIELD_RULES[key];
|
|
155
|
+
const value = rule.value(record, profile);
|
|
156
|
+
if (value === undefined)
|
|
157
|
+
continue;
|
|
158
|
+
Object.defineProperty(projected, key, {
|
|
159
|
+
configurable: true,
|
|
160
|
+
enumerable: true,
|
|
161
|
+
value,
|
|
162
|
+
writable: true,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return projected;
|
|
166
|
+
}
|
|
167
|
+
export function projectCaseForDurableWrite(record) {
|
|
168
|
+
return projectCase(record, 'durable');
|
|
169
|
+
}
|
|
170
|
+
export function projectCaseForAudience(record, audience) {
|
|
171
|
+
return projectCase(record, audience);
|
|
172
|
+
}
|
|
173
|
+
/** Field names used by the bounded hidden-copy defence, derived from policy. */
|
|
174
|
+
export const CASE_AUDIENCE_SENSITIVE_FIELD_NAMES = Object.freeze([...new Set(Object.entries(CASE_FIELD_RULES).flatMap(([key, rule]) => {
|
|
175
|
+
const audienceTreatments = [rule.treatment.trusted, rule.treatment.public];
|
|
176
|
+
const sensitive = audienceTreatments.some((value) => value === 'exclude' || value === 'redact');
|
|
177
|
+
return sensitive ? [key, ...rule.nestedSensitiveFields] : [];
|
|
178
|
+
}))]);
|
|
179
|
+
export const CASE_RECORD_FIELD_NAMES = Object.freeze(Object.values(CASE_FIELD_RULES).map((rule) => rule.key));
|
|
180
|
+
const OUTSIDE_SCHEMA_EXCLUSIONS = Object.freeze({
|
|
181
|
+
internal: Object.freeze({ label: 'Raw upstream payloads and credentials are outside the case schema.', order: 1 }),
|
|
182
|
+
trusted: Object.freeze({ label: 'Raw upstream payloads and credentials', order: 4 }),
|
|
183
|
+
public: Object.freeze({ label: 'Raw upstream payloads and credentials', order: 7 }),
|
|
184
|
+
});
|
|
185
|
+
function currentAudienceExclusions(audience, caseVersion) {
|
|
186
|
+
const entries = [
|
|
187
|
+
...Object.values(CASE_FIELD_RULES).flatMap((rule) => (rule.audienceExclusions[audience] ? [rule.audienceExclusions[audience]] : [])),
|
|
188
|
+
OUTSIDE_SCHEMA_EXCLUSIONS[audience],
|
|
189
|
+
];
|
|
190
|
+
const orderByLabel = new Map();
|
|
191
|
+
for (const entry of entries) {
|
|
192
|
+
if (entry.sinceVersion !== undefined && caseVersion < entry.sinceVersion)
|
|
193
|
+
continue;
|
|
194
|
+
if (!entry.label || entry.label.length > 160 || entry.label.trim() !== entry.label
|
|
195
|
+
|| !Number.isSafeInteger(entry.order) || entry.order < 1 || entry.order > 64) {
|
|
196
|
+
throw new TypeError('Case audience exclusion metadata is malformed or exceeds its bound.');
|
|
197
|
+
}
|
|
198
|
+
const prior = orderByLabel.get(entry.label);
|
|
199
|
+
orderByLabel.set(entry.label, Math.min(prior ?? entry.order, entry.order));
|
|
200
|
+
}
|
|
201
|
+
return Object.freeze([...orderByLabel]
|
|
202
|
+
.sort((left, right) => left[1] - right[1] || (left[0] < right[0] ? -1 : left[0] > right[0] ? 1 : 0))
|
|
203
|
+
.map(([label]) => label));
|
|
204
|
+
}
|
|
205
|
+
// Schema 12 is an immutable public compatibility contract and deliberately
|
|
206
|
+
// remains independent of the current field-policy projection.
|
|
207
|
+
const PUBLIC_V12_EXCLUSIONS = Object.freeze([
|
|
208
|
+
'Case notes',
|
|
209
|
+
'Brand Profile references',
|
|
210
|
+
'Actions and recipient values',
|
|
211
|
+
'Analyst assertions',
|
|
212
|
+
'Investigation branches',
|
|
213
|
+
'Manual trail targets',
|
|
214
|
+
'Raw upstream payloads and credentials',
|
|
215
|
+
]);
|
|
216
|
+
export function caseAudienceExclusions(audience, caseVersion) {
|
|
217
|
+
return audience === 'public' && caseVersion === PUBLIC_CASE_SCHEMA_VERSION
|
|
218
|
+
? PUBLIC_V12_EXCLUSIONS
|
|
219
|
+
: currentAudienceExclusions(audience, caseVersion);
|
|
220
|
+
}
|
|
221
|
+
export { CASE_FIELD_RULES };
|
|
@@ -8,18 +8,19 @@
|
|
|
8
8
|
// contains raw RDAP/WHOIS material. This module deliberately excludes all raw
|
|
9
9
|
// registry/web responses, contacts, cookies, screenshots, and authentication
|
|
10
10
|
// data. Reports contain only the normalized case record.
|
|
11
|
-
import {
|
|
11
|
+
import { caseEvidenceTimeline, compareCaseEvidence, currentCaseEvidence } from "./case-model.mjs";
|
|
12
12
|
import { httpSecurityHeaderLabel } from "./http-summary.mjs";
|
|
13
13
|
import { analystInteroperabilityTags } from "../../lib/analyst-taxonomy.mjs";
|
|
14
14
|
import { buildPortableGeneratorMetadata, portableGeneratorAttribution, } from "../../lib/portable-generator.mjs";
|
|
15
15
|
import { buildCaseResponseLifecycleSummary, CASE_EVIDENCE_RELATION_STANCES } from "./case-response-model.mjs";
|
|
16
16
|
import { normalizeCaseBrandProfileIds } from "./case-brand-profile-references.mjs";
|
|
17
|
-
import {
|
|
17
|
+
import { CASE_RECHECK_CONDITIONS } from "./case-recheck-model.mjs";
|
|
18
|
+
import { CASE_REPORT_SCHEMA, CASE_REPORT_SCHEMA_VERSION, PUBLISHED_V2_3_CASE_REPORT_SCHEMA_VERSION, } from "../contracts/case-portability.mjs";
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
19
20
|
// Constants
|
|
20
21
|
// ---------------------------------------------------------------------------
|
|
21
22
|
export { CASE_REPORT_SCHEMA, CASE_REPORT_SCHEMA_VERSION };
|
|
22
|
-
const
|
|
23
|
+
const PUBLISHED_V2_3_LIMITATIONS = Object.freeze([
|
|
23
24
|
'This report contains normalised browser-local observations from WHOISleuth analyst cases.',
|
|
24
25
|
'It is not a live lookup and does not contain raw WHOIS, RDAP, DNS, HTML, or responses collected during website checks.',
|
|
25
26
|
'Absence of a signal (e.g. no MX record observed) does not prove nonexistence. It may not have been evaluated.',
|
|
@@ -28,6 +29,11 @@ const LIMITATIONS_TEXT = [
|
|
|
28
29
|
'Brand Profile references record an explicit analyst-selected association only; they do not establish ownership, attribution, intent, safety, or maliciousness.',
|
|
29
30
|
'Provider workflow outcomes and independently observed technical effects remain separately attributed. A provider outcome does not establish independent remediation, absence, or safety; neither evidence family establishes legal sufficiency or provider performance.',
|
|
30
31
|
'Generated locally in the browser. Review the package before sharing it.',
|
|
32
|
+
]);
|
|
33
|
+
const LIMITATIONS_TEXT = [
|
|
34
|
+
...PUBLISHED_V2_3_LIMITATIONS.slice(0, 2),
|
|
35
|
+
'Snapshot hostnames are excluded; supporting DNS, TLS and web observations need not concern the Case registration domain.',
|
|
36
|
+
...PUBLISHED_V2_3_LIMITATIONS.slice(2),
|
|
31
37
|
].join(' ');
|
|
32
38
|
function escapeMarkdownInline(text) {
|
|
33
39
|
return String(text)
|
|
@@ -100,6 +106,7 @@ function pickKnownSnapshotFields(snapshot) {
|
|
|
100
106
|
return {
|
|
101
107
|
id: snapshot.id,
|
|
102
108
|
fingerprint: snapshot.fingerprint,
|
|
109
|
+
...(snapshot.factorOrder ? { factorOrder: snapshot.factorOrder } : {}),
|
|
103
110
|
firstCapturedAt: snapshot.firstCapturedAt,
|
|
104
111
|
capturedAt: snapshot.capturedAt,
|
|
105
112
|
source: snapshot.source,
|
|
@@ -165,53 +172,17 @@ export function buildCaseReport(caseRecord, options = {}) {
|
|
|
165
172
|
const now = generatedAt || new Date().toISOString();
|
|
166
173
|
const generator = buildPortableGeneratorMetadata(options.applicationVersion);
|
|
167
174
|
// --- Build JSON report ---
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
let incomparableReasons = [];
|
|
180
|
-
if (!isBaseline) {
|
|
181
|
-
const previous = chronological[i - 1];
|
|
182
|
-
if (!previous)
|
|
183
|
-
continue;
|
|
184
|
-
const rawChanges = compareCaseEvidence(previous, snapshot);
|
|
185
|
-
const allIncomparableReasons = caseEvidenceIncomparableReasons(previous, snapshot);
|
|
186
|
-
const hostnameContextChanged = allIncomparableReasons.includes('observation-context');
|
|
187
|
-
incomparableReasons = allIncomparableReasons.filter((reason) => reason !== 'observation-context');
|
|
188
|
-
if (rawChanges.length > 0) {
|
|
189
|
-
changes = rawChanges.map((change) => ({
|
|
190
|
-
field: change.field,
|
|
191
|
-
label: change.label,
|
|
192
|
-
before: change.before,
|
|
193
|
-
after: change.after,
|
|
194
|
-
tone: change.tone,
|
|
195
|
-
}));
|
|
196
|
-
}
|
|
197
|
-
else if (snapshot.fingerprint !== previous.fingerprint
|
|
198
|
-
&& incomparableReasons.length === 0
|
|
199
|
-
&& !hostnameContextChanged) {
|
|
200
|
-
incomparableReasons = ['other'];
|
|
201
|
-
}
|
|
202
|
-
hasIncomparableChange = incomparableReasons.length > 0;
|
|
203
|
-
}
|
|
204
|
-
timelineEntries.push({
|
|
205
|
-
snapshot: pickKnownSnapshotFields(snapshot),
|
|
206
|
-
isBaseline,
|
|
207
|
-
hasRepeatedObservation,
|
|
208
|
-
changes,
|
|
209
|
-
hasIncomparableChange,
|
|
210
|
-
incomparableReasons,
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
const latest = latestCaseEvidence({ evidenceHistory: caseRecord.evidenceHistory ?? undefined });
|
|
175
|
+
const timeline = caseEvidenceTimeline(caseRecord.evidenceHistory);
|
|
176
|
+
const timelineEntries = timeline.map((entry) => ({
|
|
177
|
+
snapshot: pickKnownSnapshotFields(entry.snapshot),
|
|
178
|
+
isBaseline: entry.isBaseline,
|
|
179
|
+
hasRepeatedObservation: entry.hasRepeatedObservation,
|
|
180
|
+
changes: entry.changes?.map(({ field, label, before, after, tone }) => ({ field, label, before, after, tone })) ?? null,
|
|
181
|
+
hasIncomparableChange: entry.hasIncomparableChange,
|
|
182
|
+
incomparableReasons: [...entry.incomparableReasons],
|
|
183
|
+
}));
|
|
184
|
+
const selection = currentCaseEvidence(caseRecord);
|
|
185
|
+
const latest = selection.snapshot;
|
|
215
186
|
const currentAssessment = latest ? pickKnownSnapshotFields(latest) : null;
|
|
216
187
|
const responseLifecycle = buildCaseResponseLifecycleSummary(caseRecord);
|
|
217
188
|
const observedEffects = caseRecord.observedEffects ?? {
|
|
@@ -233,6 +204,7 @@ export function buildCaseReport(caseRecord, options = {}) {
|
|
|
233
204
|
application: generator,
|
|
234
205
|
case: {
|
|
235
206
|
id: caseRecord.id,
|
|
207
|
+
title: caseRecord.title ?? '',
|
|
236
208
|
domain: caseRecord.domain,
|
|
237
209
|
status: caseRecord.status,
|
|
238
210
|
disposition: caseRecord.disposition,
|
|
@@ -292,12 +264,28 @@ export function buildCaseReport(caseRecord, options = {}) {
|
|
|
292
264
|
})),
|
|
293
265
|
},
|
|
294
266
|
responseLifecycle,
|
|
295
|
-
limitations: LIMITATIONS_TEXT,
|
|
267
|
+
limitations: [LIMITATIONS_TEXT, selection.limitation, ...new Set(timeline.map((entry) => entry.orderingLimitation))].filter(Boolean).join(' '),
|
|
296
268
|
};
|
|
297
269
|
// --- Build Markdown ---
|
|
298
270
|
const md = buildMarkdown(json, options.includeAttribution !== false);
|
|
299
271
|
return { json, markdown: md };
|
|
300
272
|
}
|
|
273
|
+
/** Expected strict reader projection; historical disclosure wording is immutable. */
|
|
274
|
+
export function buildCaseReportVerificationProjection(caseRecord, options, schemaVersion) {
|
|
275
|
+
const current = buildCaseReport(caseRecord, options).json;
|
|
276
|
+
if (schemaVersion === CASE_REPORT_SCHEMA_VERSION)
|
|
277
|
+
return current;
|
|
278
|
+
if (schemaVersion !== PUBLISHED_V2_3_CASE_REPORT_SCHEMA_VERSION) {
|
|
279
|
+
throw new TypeError('No strict Case report projection is defined for this version.');
|
|
280
|
+
}
|
|
281
|
+
const { title: _title, ...publishedCase } = current.case;
|
|
282
|
+
return {
|
|
283
|
+
...current,
|
|
284
|
+
case: publishedCase,
|
|
285
|
+
schemaVersion,
|
|
286
|
+
limitations: PUBLISHED_V2_3_LIMITATIONS.join(' ') + current.limitations.slice(LIMITATIONS_TEXT.length),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
301
289
|
// ---------------------------------------------------------------------------
|
|
302
290
|
// Markdown generation
|
|
303
291
|
// ---------------------------------------------------------------------------
|
|
@@ -312,6 +300,11 @@ function buildMarkdown(report, includeAttribution) {
|
|
|
312
300
|
const domain = escapeMarkdownInline(report.case.domain || 'unknown');
|
|
313
301
|
lines.push(`# Case Report: ${domain}`);
|
|
314
302
|
lines.push('');
|
|
303
|
+
if (report.case.title) {
|
|
304
|
+
lines.push(`**Incident:** ${escapeMarkdownInline(report.case.title)}`);
|
|
305
|
+
lines.push('');
|
|
306
|
+
}
|
|
307
|
+
lines.push(`**Case ID:** ${escapeMarkdownInline(report.case.id)}`);
|
|
315
308
|
// Metadata
|
|
316
309
|
lines.push(`**Generated:** ${escapeMarkdownInline(report.generatedAt)}`);
|
|
317
310
|
lines.push(`**Report schema:** ${escapeMarkdownInline(report.schema)} v${report.schemaVersion}`);
|
|
@@ -357,7 +350,9 @@ function buildMarkdown(report, includeAttribution) {
|
|
|
357
350
|
lines.push(`- **Source:** ${escapeMarkdownInline(formatReportValue(a.source))}`);
|
|
358
351
|
}
|
|
359
352
|
else {
|
|
360
|
-
lines.push(
|
|
353
|
+
lines.push(report.evidenceTimeline.length
|
|
354
|
+
? 'Retained snapshots have no unique latest assessment. Review the timeline and limitations.'
|
|
355
|
+
: 'No evidence captured.');
|
|
361
356
|
}
|
|
362
357
|
lines.push('');
|
|
363
358
|
// Evidence timeline
|
|
@@ -452,6 +447,8 @@ function buildMarkdown(report, includeAttribution) {
|
|
|
452
447
|
}
|
|
453
448
|
if (entry.hasIncomparableChange) {
|
|
454
449
|
const reasons = Array.isArray(entry.incomparableReasons) ? entry.incomparableReasons : [];
|
|
450
|
+
if (reasons.includes('observation-context'))
|
|
451
|
+
lines.push('> Observation targets differ or are unknown. Field-level additions, removals and beneficial changes are withheld because their scopes cannot be compared reliably. Exact hostnames remain excluded from this report.');
|
|
455
452
|
if (reasons.includes('risk-model'))
|
|
456
453
|
lines.push('> Risk scores and factors use different or unversioned models, so their numeric difference is not treated as a domain change.');
|
|
457
454
|
if (reasons.includes('opportunity-model'))
|
|
@@ -478,7 +475,7 @@ function buildMarkdown(report, includeAttribution) {
|
|
|
478
475
|
lines.push('No evidence pins recorded.');
|
|
479
476
|
for (const pin of response.evidencePins) {
|
|
480
477
|
lines.push(`- **${escapeMarkdownInline(pin.label)}:** ${escapeMarkdownInline(pin.value)}`);
|
|
481
|
-
lines.push(` Source: ${escapeMarkdownInline(pin.source)}; observed ${escapeMarkdownInline(pin.observedAt)}; completeness ${escapeMarkdownInline(pin.completeness)}.`);
|
|
478
|
+
lines.push(` Source: ${escapeMarkdownInline(pin.source)}; observed ${escapeMarkdownInline(pin.observedAt ?? 'Time unavailable')}; completeness ${escapeMarkdownInline(pin.completeness)}.`);
|
|
482
479
|
if (pin.limitations.length)
|
|
483
480
|
lines.push(` Limitations: ${escapeMarkdownInline(pin.limitations.join('; '))}`);
|
|
484
481
|
}
|
|
@@ -488,7 +485,7 @@ function buildMarkdown(report, includeAttribution) {
|
|
|
488
485
|
if (!response.sightings.length)
|
|
489
486
|
lines.push('No source-qualified sightings recorded.');
|
|
490
487
|
for (const sighting of response.sightings) {
|
|
491
|
-
lines.push(`- **${escapeMarkdownInline(sighting.state.replaceAll('_', ' '))}:** ${escapeMarkdownInline(sighting.category)} (${escapeMarkdownInline(sighting.observedAt)})`);
|
|
488
|
+
lines.push(`- **${escapeMarkdownInline(sighting.state.replaceAll('_', ' '))}:** ${escapeMarkdownInline(sighting.category)} (${escapeMarkdownInline(sighting.observedAt ?? 'Observation time unavailable')})`);
|
|
492
489
|
lines.push(` Source: ${escapeMarkdownInline(sighting.source)}; class ${escapeMarkdownInline(sighting.sourceClass)}; completeness ${escapeMarkdownInline(sighting.completeness)}.`);
|
|
493
490
|
if (sighting.evidencePinId)
|
|
494
491
|
lines.push(` Evidence pin: ${escapeMarkdownInline(sighting.evidencePinId)}`);
|
|
@@ -512,6 +509,11 @@ function buildMarkdown(report, includeAttribution) {
|
|
|
512
509
|
lines.push(`- **${escapeMarkdownInline(assertion.state)}:** ${escapeMarkdownInline(assertion.statement)}`);
|
|
513
510
|
if (assertion.rationale)
|
|
514
511
|
lines.push(` ${escapeMarkdownInline(assertion.rationale)}`);
|
|
512
|
+
if (assertion.recheck) {
|
|
513
|
+
lines.push(` Recheck target: ${escapeMarkdownInline(assertion.recheck.targetHostname)}; comparison conditions: ${escapeMarkdownInline(assertion.recheck.conditions)}`);
|
|
514
|
+
if (assertion.recheck.baselinePinId)
|
|
515
|
+
lines.push(` Baseline evidence pin: ${escapeMarkdownInline(assertion.recheck.baselinePinId)}`);
|
|
516
|
+
}
|
|
515
517
|
if (assertion.provenance) {
|
|
516
518
|
lines.push(` External provenance: ${escapeMarkdownInline(assertion.provenance.format.toUpperCase())}; source ${escapeMarkdownInline(assertion.provenance.sourceName)}; file SHA-256 ${escapeMarkdownInline(assertion.provenance.sourceDigestSha256)}.`);
|
|
517
519
|
if (assertion.provenance.publisher)
|
|
@@ -544,6 +546,7 @@ function buildMarkdown(report, includeAttribution) {
|
|
|
544
546
|
for (const decision of response.decisions) {
|
|
545
547
|
lines.push(`- **${escapeMarkdownInline(decision.summary)}** (${escapeMarkdownInline(decision.createdAt)})`);
|
|
546
548
|
lines.push(` ${escapeMarkdownInline(decision.rationale)}`);
|
|
549
|
+
lines.push(` Analyst confidence: ${escapeMarkdownInline(decision.confidence)}${decision.confidenceBasis ? ` — ${escapeMarkdownInline(decision.confidenceBasis)}` : ''}.`);
|
|
547
550
|
if (decision.evidencePinIds.length)
|
|
548
551
|
lines.push(` Evidence pins: ${escapeMarkdownInline(decision.evidencePinIds.join(', '))}`);
|
|
549
552
|
}
|
|
@@ -555,6 +558,10 @@ function buildMarkdown(report, includeAttribution) {
|
|
|
555
558
|
for (const action of response.actions) {
|
|
556
559
|
lines.push(`- **${escapeMarkdownInline(action.type.replaceAll('_', ' '))}:** ${escapeMarkdownInline(action.recipient)} (${escapeMarkdownInline(action.state)})`);
|
|
557
560
|
lines.push(` Contact source: ${escapeMarkdownInline(action.contactSource)}; updated ${escapeMarkdownInline(action.updatedAt)}.`);
|
|
561
|
+
if (action.routeObservedAt)
|
|
562
|
+
lines.push(` Route reviewed: ${escapeMarkdownInline(action.routeObservedAt)}`);
|
|
563
|
+
if (action.routeReviewAfter)
|
|
564
|
+
lines.push(` Route review due: ${escapeMarkdownInline(action.routeReviewAfter)}`);
|
|
558
565
|
if (action.originActionId)
|
|
559
566
|
lines.push(` Originating action: ${escapeMarkdownInline(action.originActionId)}`);
|
|
560
567
|
if (action.dueAt)
|
|
@@ -618,10 +625,19 @@ function buildMarkdown(report, includeAttribution) {
|
|
|
618
625
|
: `- Latest independently observed change time: Withheld because the independent change state is ${escapeMarkdownInline(report.responseLifecycle.observedChangeState)}.`);
|
|
619
626
|
if (observedEffect)
|
|
620
627
|
lines.push(`- Latest independent review: ${escapeMarkdownInline(observedEffect.state.replaceAll('_', ' '))} at ${escapeMarkdownInline(observedEffect.observedAt)} from ${escapeMarkdownInline(observedEffect.source)}.`);
|
|
628
|
+
else if (response.observedEffects.reviews.length)
|
|
629
|
+
lines.push('- A single latest independent review cannot be selected from the retained observation times. Review the individual records below.');
|
|
621
630
|
if (!response.observedEffects.reviews.length)
|
|
622
631
|
lines.push('- No independent observed-effect review recorded.');
|
|
623
632
|
for (const review of response.observedEffects.reviews) {
|
|
624
633
|
lines.push(`- **${escapeMarkdownInline(review.state.replaceAll('_', ' '))}** (${escapeMarkdownInline(review.observedAt)}): ${escapeMarkdownInline(review.source)}; class ${escapeMarkdownInline(review.sourceClass)}; completeness ${escapeMarkdownInline(review.completeness)}.`);
|
|
634
|
+
if (review.recheck) {
|
|
635
|
+
lines.push(` Question ${escapeMarkdownInline(review.recheck.questionId)}: ${escapeMarkdownInline(review.recheck.question)}`);
|
|
636
|
+
lines.push(` Recheck target: ${escapeMarkdownInline(review.recheck.targetHostname)}; retained comparison conditions: ${escapeMarkdownInline(review.recheck.conditions)}`);
|
|
637
|
+
lines.push(` Condition comparison: ${escapeMarkdownInline(CASE_RECHECK_CONDITIONS[review.recheck.conditionsMatch])}`);
|
|
638
|
+
if (review.recheck.baselinePinId)
|
|
639
|
+
lines.push(` Baseline evidence pin: ${escapeMarkdownInline(review.recheck.baselinePinId)}`);
|
|
640
|
+
}
|
|
625
641
|
if (review.evidencePinId)
|
|
626
642
|
lines.push(` Evidence pin: ${escapeMarkdownInline(review.evidencePinId)}`);
|
|
627
643
|
if (review.sightingId)
|