@slicedearth/whoisleuth-cli 1.45.0 → 2.0.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/DISCLOSURE +7 -3
- package/NOTICE +5 -4
- package/README.md +74 -124
- package/SECURITY.md +6 -3
- package/bin/whoisleuth.mjs +7 -4
- package/cli/archive-inspect.mjs +28 -10
- package/cli/arguments.mjs +450 -173
- package/cli/artifact-structure.mjs +2507 -0
- package/cli/artifact-verify.mjs +286 -64
- package/cli/bounded-json.mjs +1 -0
- package/cli/bulk-checkpoint.mjs +35 -23
- package/cli/bulk-command-runner.mjs +2 -5
- package/cli/bulk.mjs +9 -2
- package/cli/case-pack.mjs +435 -46
- package/cli/ci-report.mjs +6 -1
- package/cli/collection-preflight.mjs +2 -2
- package/cli/command-catalogue.mjs +12 -3
- package/cli/command-reference.mjs +948 -106
- package/cli/completion.mjs +504 -218
- package/cli/config-profile.mjs +13 -3
- package/cli/ct-event-intake.mjs +32 -2
- package/cli/discover.mjs +7 -1
- package/cli/discovery-observation-snapshot.mjs +24 -28
- package/cli/discovery-scan-command-runner.mjs +13 -14
- package/cli/discovery-scan.mjs +17 -3
- package/cli/discovery-snapshot.mjs +15 -7
- package/cli/discovery-workflow.mjs +2 -3
- package/cli/doctor.mjs +2 -2
- package/cli/domain-control-monitor.mjs +215 -36
- package/cli/domain-control-observations.mjs +34 -17
- package/cli/errors.mjs +8 -3
- package/cli/evidence-command-runner.mjs +17 -11
- package/cli/evidence-signing.mjs +68 -50
- package/cli/export-evidence.mjs +11 -4
- package/cli/external-observation-mapping.mjs +3 -3
- package/cli/formatters/html.mjs +3 -3
- package/cli/formatters/json.mjs +20 -32
- package/cli/formatters/terminal.mjs +586 -22
- package/cli/input.mjs +62 -0
- package/cli/interactive-launcher.mjs +226 -0
- package/cli/interchange-report.mjs +226 -0
- package/cli/investigation-manifest.mjs +16 -17
- package/cli/investigation-plan.mjs +167 -16
- package/cli/investigation-run.mjs +80 -24
- package/cli/local-mmdb-review.mjs +45 -14
- package/cli/lookalike-calibration.mjs +3 -1
- package/cli/lookup-brief.mjs +13 -8
- package/cli/lookup-browser.mjs +844 -0
- package/cli/lookup-command-runner.mjs +96 -33
- package/cli/lookup-diff.mjs +11 -2
- package/cli/lookup-plan.mjs +1 -0
- package/cli/mail-review.mjs +206 -94
- package/cli/manual.mjs +5 -5
- package/cli/network-command-runner.mjs +78 -0
- package/cli/offline-evidence-review.mjs +45 -14
- package/cli/open-asset-model-bridge.mjs +1 -1
- package/cli/output-file.mjs +70 -14
- package/cli/page-compare.mjs +18 -10
- package/cli/progress-events.mjs +2 -1
- package/cli/progress.mjs +3 -3
- package/cli/registry-cohort.mjs +343 -61
- package/cli/registry-doctor.mjs +67 -28
- package/cli/registry-support.mjs +2 -1
- package/cli/retained-artifact-diff.mjs +375 -0
- package/cli/risk-calibration.mjs +89 -40
- package/cli/runner.mjs +322 -246
- package/cli/saved-lookup.mjs +439 -49
- package/cli/sharing-review.mjs +18 -7
- package/cli/source-reliability.mjs +75 -36
- package/cli/terminal-presentation.mjs +43 -18
- package/docs/cli-reference.md +125 -1334
- package/docs/cli.md +87 -236
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -0
- package/frontend/src/lib/analysis/domain-control-records.js +3 -0
- package/lib/application-version.mjs +3 -0
- package/lib/availability.mjs +72 -52
- package/lib/bounded-contract-normalizers.mjs +2 -4
- package/lib/bounded-file.mjs +91 -4
- package/lib/bounded-json.mjs +287 -0
- package/lib/browser-library-profile.mjs +1 -1
- package/lib/certificate-extension-profile.mjs +2 -1
- package/lib/classify.mjs +56 -5
- package/lib/client-behavior-profile.mjs +1 -1
- package/lib/credential-surface-profile.mjs +1 -1
- package/lib/cryptographic-assurance.mjs +173 -0
- package/lib/ct-query.mjs +3 -3
- package/lib/ct-search.mjs +120 -64
- package/lib/distributed-operation-budget.mjs +4 -2
- package/lib/dns-delegation-health.mjs +9 -6
- package/lib/dns-intelligence.mjs +4 -3
- package/lib/dns-resolver-selection.mjs +6 -4
- package/lib/dnssec-chain-validation.mjs +1052 -0
- package/lib/dnssec-evidence-validation.mjs +23 -10
- package/lib/domain-assurance.mjs +12 -12
- package/lib/domain-change-packet.mjs +7 -5
- package/lib/domain-control-flight-recorder.mjs +442 -78
- package/lib/domain-control-manifest.mjs +314 -171
- package/lib/domain-posture-analysis.mjs +5 -2
- package/lib/domain-posture.mjs +7 -4
- package/lib/encrypted-dns-contract.mjs +2 -3
- package/lib/evidence-export.mjs +1243 -88
- package/lib/evidence-report-markdown.mjs +3 -0
- package/lib/evidence-report.mjs +5 -0
- package/lib/external-intelligence-risk.mjs +26 -10
- package/lib/favicon.mjs +8 -5
- package/lib/feature-policy.mjs +1 -1
- package/lib/homepage-metadata-contract.mjs +290 -0
- package/lib/hostname.mjs +1 -1
- package/lib/html-signals.mjs +89 -5
- package/lib/http-evidence-bounds.mjs +3 -0
- package/lib/http-intelligence.mjs +332 -6
- package/lib/interchange-fidelity-registry.mjs +124 -0
- package/lib/local-geoip-evidence.mjs +2 -1
- package/lib/lookup-cache.mjs +6 -4
- package/lib/lookup-network-evidence-bounds.mjs +9 -0
- package/lib/lookup.mjs +27 -4
- package/lib/observed-network-context.mjs +1 -1
- package/lib/outbound-identity.mjs +2 -3
- package/lib/page-fingerprints.mjs +2 -2
- package/lib/page-role-profile.mjs +1 -1
- package/lib/perceptual-hash.mjs +12 -4
- package/lib/portable-generator.mjs +29 -0
- package/lib/provider-policy-admission.mjs +11 -4
- package/lib/rdap-bootstrap.mjs +76 -16
- package/lib/rdap-client.mjs +20 -12
- package/lib/rdap-normalization.mjs +324 -80
- package/lib/rdap-registrar.mjs +5 -1
- package/lib/rdap-transport.mjs +2 -2
- package/lib/rdap-validation.mjs +65 -14
- package/lib/rdap.mjs +2 -2
- package/lib/registrable-domain.mjs +36 -0
- package/lib/registry-capabilities.mjs +3 -0
- package/lib/registry-capability-catalogue.mjs +12 -2
- package/lib/registry-comparison.mjs +28 -11
- package/lib/registry-insights.mjs +7 -0
- package/lib/response-policy.mjs +27 -4
- package/lib/risk-calibration-summary.mjs +340 -0
- package/lib/risk-scoring.mjs +4 -3
- package/lib/rpki-evidence.mjs +9 -8
- package/lib/safe-fetch.mjs +121 -50
- package/lib/security-txt.mjs +27 -4
- package/lib/service-binding-dns.mjs +6 -1
- package/lib/smtp-transport-review.mjs +787 -0
- package/lib/sslbl-certificates.generated.mjs +136 -134
- package/lib/sslbl-intelligence.mjs +6 -8
- package/lib/static-html-analysis.mjs +301 -4
- package/lib/structured-data-identity.mjs +51 -19
- package/lib/threat-intelligence-fixtures.mjs +2 -4
- package/lib/threat-intelligence-manifests.mjs +2 -7
- package/lib/threat-intelligence-runtime.mjs +5 -15
- package/lib/threat-intelligence-types.mjs +21 -1
- package/lib/threatfox-intelligence.mjs +7 -3
- package/lib/tls-intelligence.mjs +15 -9
- package/lib/tlsa-evidence.mjs +37 -7
- package/lib/urlhaus-intelligence.mjs +4 -2
- package/lib/urlscan-intelligence.mjs +6 -5
- package/lib/website-security-posture.mjs +1 -1
- package/lib/website-technology.mjs +1 -1
- package/lib/whois-authority.mjs +43 -8
- package/lib/whois-chain.mjs +25 -3
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-transport.mjs +12 -3
- package/lib/zone-intent-review.mjs +1 -1
- package/package.json +12 -4
- package/packages/cases/case-brand-profile-references.mjs +85 -0
- package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +43 -14
- package/packages/cases/case-investigation-branch-model.mjs +132 -0
- package/packages/cases/case-migration-model.mjs +476 -0
- package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
- package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -19
- package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
- package/packages/cases/case-record-model.mjs +9 -0
- package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +106 -33
- package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +143 -16
- package/packages/cases/case-response-model.mjs +1604 -0
- package/packages/cases/case-response-packet.mjs +1312 -0
- package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
- package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
- package/packages/cases/opaque-reference-id.mjs +9 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +366 -0
- package/packages/comparison/comparison-ledger-contract.mjs +241 -0
- package/packages/comparison/comparison-ledger-json.mjs +80 -0
- package/packages/comparison/comparison-ledger-serialization.mjs +52 -0
- package/packages/comparison/favicon-similarity.mjs +78 -0
- package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
- package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
- package/packages/contracts/analyst-review-state-contract.mjs +61 -0
- package/packages/contracts/analyst-review-state.mjs +50 -0
- package/packages/contracts/case-portability.mjs +1310 -0
- package/packages/contracts/cli-command-catalogue.mjs +2 -0
- package/packages/contracts/cli-lookup.mjs +394 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
- package/packages/contracts/domain-control-manifest.mjs +1027 -0
- package/packages/contracts/domain-control-monitor.mjs +310 -0
- package/packages/contracts/domain-control-review.mjs +828 -0
- package/packages/contracts/domain-name.mjs +4 -0
- package/packages/contracts/external-observation-interchange.mjs +224 -0
- package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
- package/packages/contracts/investigation-portability.mjs +331 -0
- package/packages/contracts/offline-comparison.mjs +160 -0
- package/packages/contracts/risk-calibration.mjs +1288 -0
- package/packages/contracts/schema-compatibility.mjs +71 -0
- package/packages/contracts/schema-lifecycle.mjs +1702 -0
- package/packages/contracts/sslbl-snapshot.mjs +17 -0
- package/packages/contracts/workspace-portability.mjs +1347 -0
- package/packages/evidence/artifact-integrity.mjs +65 -0
- package/packages/evidence/decision-fact.mjs +735 -0
- package/packages/evidence/domain-control-runtime.mjs +547 -0
- package/packages/evidence/domain-name.mjs +41 -0
- package/packages/evidence/observation.mjs +384 -0
- package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +86 -20
- package/packages/interchange/retained-text.mjs +8 -0
- package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +11 -8
- package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +28 -9
- package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +67 -18
- package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +10 -9
- package/packages/investigation/bulk-review-policy.mjs +1 -0
- package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +50 -18
- package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
- package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
- package/packages/investigation/lookup-claim-passport.mjs +232 -0
- package/packages/investigation/lookup-claim-readiness.mjs +287 -0
- package/packages/investigation/lookup-investigation-brief.mjs +172 -0
- package/packages/monitoring/analyst-review-state.mjs +680 -0
- package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +97 -50
- package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +15 -15
- package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +179 -39
- package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +12 -5
- package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
- package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
- package/packages/workspace/hostile-input.mjs +172 -0
- package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +51 -46
- package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +15 -15
- package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
- package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
- package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
- package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
- package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
- package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +18 -12
- package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
- package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +195 -84
- package/third-party-notices.txt +112 -5834
- package/frontend/src/lib/analysis/artifact-integrity.js +0 -21
- package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
- package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
- package/frontend/src/lib/analysis/case-migration-model.js +0 -253
- package/frontend/src/lib/analysis/case-record-model.js +0 -7
- package/frontend/src/lib/analysis/case-response-model.js +0 -669
- package/frontend/src/lib/analysis/case-response-packet.js +0 -573
- package/frontend/src/lib/analysis/lookup-decision-support.js +0 -487
- package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
- package/frontend/src/lib/analysis/lookup-presentation.js +0 -58
- package/frontend/src/lib/analysis/lookup-response.js +0 -2
- package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
- package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
- package/frontend/src/lib/analysis/scoring.js +0 -55
- package/frontend/src/lib/analysis/utils.js +0 -232
- package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
- package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
- package/frontend/src/lib/analysis/visualization-matrix.js +0 -88
- package/frontend/src/lib/analysis/visualization-models.js +0 -369
- package/frontend/src/lib/analysis/visualization-time-series.js +0 -144
- package/lib/bounded-json-response.mjs +0 -157
- package/lib/lookup-response-contract.mjs +0 -346
- package/lib/observation.mjs +0 -114
- /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
- /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
- /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Stable case-record facade. Core validation, evidence history/comparison,
|
|
2
|
+
// and analyst-record operations remain framework-neutral and independently
|
|
3
|
+
// testable while existing consumers keep one compatibility import.
|
|
4
|
+
export * from "./case-record-contracts.mjs";
|
|
5
|
+
export * from "./case-record-core.mjs";
|
|
6
|
+
export * from "./case-brand-profile-references.mjs";
|
|
7
|
+
export * from "./case-evidence-model.mjs";
|
|
8
|
+
export * from "./case-record-operations.mjs";
|
|
9
|
+
export * from "./case-investigation-branch-model.mjs";
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// Pure, framework-neutral analyst-case records, evidence histories, bounded
|
|
2
2
|
// record normalization, and analyst updates.
|
|
3
|
-
import { appendCaseAction, appendCaseAssertion, appendCaseDecision, appendCaseEvidencePin, appendCaseEvidencePins, appendCaseManualTrailEvent, appendCaseSighting, normalizeCaseActions, normalizeCaseAssertions, normalizeCaseDecisions, normalizeCaseEvidencePins, normalizeCaseManualTrail, normalizeCaseSightings, updateCaseAction, updateCaseAssertion, } from "./case-response-model.
|
|
4
|
-
import { MAX_CASES, MAX_NOTES_PER_CASE, } from "./case-record-contracts.
|
|
5
|
-
import { DEFAULT_EVIDENCE_SOURCE, EVIDENCE_SOURCE_SET,
|
|
6
|
-
import {
|
|
3
|
+
import { appendCaseAction, appendCaseAssertion, appendCaseClosure, appendCaseDecision, appendCaseEvidencePin, appendCaseEvidencePins, appendCaseManualTrailEvent, appendCaseObservedEffectReview, appendCaseSighting, buildCaseClosureLinkContext, normalizeCaseActions, normalizeCaseAssertions, normalizeCaseClosureHistory, normalizeCaseDecisions, normalizeCaseEvidencePins, normalizeCaseManualTrail, normalizeCaseObservedEffectHistory, normalizeCaseSightings, updateCaseAction, updateCaseAssertion, } from "./case-response-model.mjs";
|
|
4
|
+
import { CASE_SCHEMA_VERSION, MAX_CASES, MAX_NOTES_PER_CASE, } from "./case-record-contracts.mjs";
|
|
5
|
+
import { DEFAULT_EVIDENCE_SOURCE, EVIDENCE_SOURCE_SET, caseTimestampOrNull, deterministicId, makeId, normalizeDisposition, normalizeDomain, normalizeNoteBody, normalizeReviewReasonCode, normalizeNotes, normalizeSource, normalizeStatus, normalizeTags, objectRecord, safeId, } from "./case-record-core.mjs";
|
|
6
|
+
import { assertCaseBrandProfileIds, normalizeCaseBrandProfileIds, } from "./case-brand-profile-references.mjs";
|
|
7
|
+
import { normalizeEvidenceHistory, } from "./case-evidence-model.mjs";
|
|
8
|
+
import { appendCaseInvestigationBranch, caseInvestigationBranchReferences, normalizeCaseInvestigationBranches, updateCaseInvestigationBranch, } from "./case-investigation-branch-model.mjs";
|
|
7
9
|
// ---------------------------------------------------------------------------
|
|
8
10
|
// Case normalization
|
|
9
11
|
// ---------------------------------------------------------------------------
|
|
@@ -16,10 +18,15 @@ function inferCaptureSource(caseSource) {
|
|
|
16
18
|
}
|
|
17
19
|
// Builds a case's bounded current-schema evidence history. Uses a lenient
|
|
18
20
|
// local fallback timestamp so recoverable current data always loads.
|
|
19
|
-
function normalizeCaseEvidence(record, createdAt, updatedAt, now) {
|
|
21
|
+
function normalizeCaseEvidence(record, domain, createdAt, updatedAt, now, sourceVersion) {
|
|
20
22
|
const localFallback = updatedAt || createdAt || now;
|
|
21
23
|
if (Array.isArray(record.evidenceHistory)) {
|
|
22
|
-
return normalizeEvidenceHistory(record.evidenceHistory, {
|
|
24
|
+
return normalizeEvidenceHistory(record.evidenceHistory, {
|
|
25
|
+
source: DEFAULT_EVIDENCE_SOURCE,
|
|
26
|
+
fallback: localFallback,
|
|
27
|
+
caseDomain: domain,
|
|
28
|
+
...(sourceVersion === undefined ? {} : { sourceVersion }),
|
|
29
|
+
});
|
|
23
30
|
}
|
|
24
31
|
return [];
|
|
25
32
|
}
|
|
@@ -34,32 +41,51 @@ function normalizeCaseEvidence(record, createdAt, updatedAt, now) {
|
|
|
34
41
|
* @param {string} [nowIso]
|
|
35
42
|
* @returns {CaseRecord | null}
|
|
36
43
|
*/
|
|
37
|
-
export function normalizeCase(raw, existing, nowIso) {
|
|
38
|
-
const now = nowIso || new Date().toISOString();
|
|
44
|
+
export function normalizeCase(raw, existing, nowIso, sourceVersion) {
|
|
45
|
+
const now = caseTimestampOrNull(nowIso) || new Date().toISOString();
|
|
39
46
|
const record = objectRecord(raw);
|
|
40
47
|
const domain = normalizeDomain(existing ? existing.domain : record.domain);
|
|
41
48
|
if (!domain)
|
|
42
49
|
return null;
|
|
43
|
-
const createdAt = existing ? existing.createdAt :
|
|
44
|
-
const updatedAt =
|
|
45
|
-
const
|
|
50
|
+
const createdAt = existing ? existing.createdAt : caseTimestampOrNull(record.createdAt, sourceVersion) || now;
|
|
51
|
+
const updatedAt = caseTimestampOrNull(record.updatedAt, sourceVersion) || createdAt;
|
|
52
|
+
const timestampOptions = {
|
|
53
|
+
legacyTimestamps: sourceVersion != null && sourceVersion < CASE_SCHEMA_VERSION,
|
|
54
|
+
...(sourceVersion === undefined ? {} : { sourceVersion }),
|
|
55
|
+
};
|
|
56
|
+
const evidencePins = normalizeCaseEvidencePins(record.evidencePins, updatedAt, timestampOptions);
|
|
46
57
|
const pinIds = new Set(evidencePins.map((item) => item.id));
|
|
58
|
+
const actions = normalizeCaseActions(record.actions, updatedAt, { ...timestampOptions, validEvidencePinIds: pinIds });
|
|
59
|
+
const assertions = normalizeCaseAssertions(record.assertions, updatedAt, pinIds, timestampOptions);
|
|
60
|
+
const sightings = normalizeCaseSightings(record.sightings, updatedAt, pinIds, timestampOptions);
|
|
61
|
+
const sightingIds = new Set(sightings.map((item) => item.id));
|
|
62
|
+
const observedEffects = normalizeCaseObservedEffectHistory(sourceVersion != null && sourceVersion < 13 ? undefined : record.observedEffects, updatedAt, pinIds, sightingIds, timestampOptions);
|
|
63
|
+
const closures = normalizeCaseClosureHistory(sourceVersion != null && sourceVersion < 13 ? undefined : record.closures, updatedAt, new Set(observedEffects.reviews.map((item) => item.id)), new Set(actions.map((item) => item.id)), timestampOptions, buildCaseClosureLinkContext(observedEffects, actions));
|
|
64
|
+
const normalizedStatus = normalizeStatus(record.status);
|
|
65
|
+
const branchReferences = caseInvestigationBranchReferences({ evidencePins, actions, assertions });
|
|
47
66
|
return {
|
|
48
67
|
id: existing ? existing.id : safeId(record.id) || deterministicId(domain),
|
|
49
68
|
domain,
|
|
50
|
-
status:
|
|
69
|
+
status: normalizedStatus === 'resolved'
|
|
70
|
+
&& closures.records.length === 0 && !closures.preV13HistoryUnavailable
|
|
71
|
+
? 'reviewing'
|
|
72
|
+
: normalizedStatus,
|
|
51
73
|
disposition: normalizeDisposition(record.disposition),
|
|
52
74
|
reviewReasonCode: normalizeReviewReasonCode(record.reviewReasonCode),
|
|
75
|
+
brandProfileIds: normalizeCaseBrandProfileIds(record.brandProfileIds),
|
|
53
76
|
tags: normalizeTags(record.tags),
|
|
54
|
-
notes: normalizeNotes(record.notes, now),
|
|
77
|
+
notes: normalizeNotes(record.notes, now, sourceVersion),
|
|
55
78
|
source: normalizeSource(record.source),
|
|
56
|
-
evidenceHistory: normalizeCaseEvidence(record, createdAt, updatedAt, now),
|
|
79
|
+
evidenceHistory: normalizeCaseEvidence(record, domain, createdAt, updatedAt, now, sourceVersion),
|
|
57
80
|
evidencePins,
|
|
58
|
-
decisions: normalizeCaseDecisions(record.decisions, updatedAt, pinIds),
|
|
59
|
-
actions
|
|
60
|
-
assertions
|
|
61
|
-
manualTrail: normalizeCaseManualTrail(record.manualTrail, updatedAt),
|
|
62
|
-
sightings
|
|
81
|
+
decisions: normalizeCaseDecisions(record.decisions, updatedAt, pinIds, timestampOptions),
|
|
82
|
+
actions,
|
|
83
|
+
assertions,
|
|
84
|
+
manualTrail: normalizeCaseManualTrail(record.manualTrail, updatedAt, timestampOptions),
|
|
85
|
+
sightings,
|
|
86
|
+
observedEffects,
|
|
87
|
+
closures,
|
|
88
|
+
branches: normalizeCaseInvestigationBranches(record.branches, updatedAt, branchReferences, timestampOptions),
|
|
63
89
|
createdAt,
|
|
64
90
|
updatedAt,
|
|
65
91
|
};
|
|
@@ -70,7 +96,7 @@ export function normalizeCase(raw, existing, nowIso) {
|
|
|
70
96
|
* @returns {CaseRecord}
|
|
71
97
|
*/
|
|
72
98
|
export function createCase(input, nowIso) {
|
|
73
|
-
const now = nowIso || new Date().toISOString();
|
|
99
|
+
const now = caseTimestampOrNull(nowIso) || new Date().toISOString();
|
|
74
100
|
const domain = normalizeDomain(input.domain);
|
|
75
101
|
if (!domain)
|
|
76
102
|
throw new Error('A valid domain is required to open a case.');
|
|
@@ -82,34 +108,54 @@ export function createCase(input, nowIso) {
|
|
|
82
108
|
? appendCaseEvidencePin([], input.evidencePin, now)
|
|
83
109
|
: [];
|
|
84
110
|
const pinIds = new Set(evidencePins.map((item) => item.id));
|
|
111
|
+
const actions = input.action !== undefined
|
|
112
|
+
? appendCaseAction([], input.action, now)
|
|
113
|
+
: [];
|
|
114
|
+
const assertions = input.assertion !== undefined
|
|
115
|
+
? appendCaseAssertion([], input.assertion, now)
|
|
116
|
+
: [];
|
|
117
|
+
const branchReferences = caseInvestigationBranchReferences({ evidencePins, actions, assertions });
|
|
118
|
+
const sightings = input.sighting !== undefined
|
|
119
|
+
? appendCaseSighting([], input.sighting, now, pinIds)
|
|
120
|
+
: [];
|
|
121
|
+
const observedEffects = input.observedEffectReview !== undefined
|
|
122
|
+
? appendCaseObservedEffectReview(normalizeCaseObservedEffectHistory(undefined, now), input.observedEffectReview, now, pinIds, new Set(sightings.map((item) => item.id)))
|
|
123
|
+
: normalizeCaseObservedEffectHistory(undefined, now);
|
|
124
|
+
const closures = input.closure !== undefined
|
|
125
|
+
? appendCaseClosure(normalizeCaseClosureHistory(undefined, now), input.closure, now, observedEffects, actions)
|
|
126
|
+
: normalizeCaseClosureHistory(undefined, now);
|
|
127
|
+
if (normalizeStatus(input.status) === 'resolved' && input.closure === undefined) {
|
|
128
|
+
throw new Error('Opening a resolved case requires a deliberate closure reason and its linked review context.');
|
|
129
|
+
}
|
|
85
130
|
return {
|
|
86
131
|
id: makeId(),
|
|
87
132
|
domain,
|
|
88
|
-
status: normalizeStatus(input.status),
|
|
133
|
+
status: input.closure !== undefined ? 'resolved' : normalizeStatus(input.status),
|
|
89
134
|
disposition: normalizeDisposition(input.disposition),
|
|
90
135
|
reviewReasonCode: normalizeReviewReasonCode(input.reviewReasonCode),
|
|
136
|
+
brandProfileIds: input.brandProfileIds === undefined ? [] : assertCaseBrandProfileIds(input.brandProfileIds),
|
|
91
137
|
tags: normalizeTags(input.tags),
|
|
92
138
|
notes: noteBody ? [{ id: makeId(), body: noteBody, createdAt: now }] : [],
|
|
93
139
|
source,
|
|
94
140
|
evidenceHistory: normalizeEvidenceHistory(input.evidence ? [input.evidence] : [], {
|
|
95
141
|
source: inferCaptureSource(source),
|
|
96
142
|
fallback: now,
|
|
143
|
+
caseDomain: domain,
|
|
97
144
|
}),
|
|
98
145
|
evidencePins,
|
|
99
146
|
decisions: input.decision !== undefined
|
|
100
147
|
? appendCaseDecision([], input.decision, now)
|
|
101
148
|
: [],
|
|
102
|
-
actions
|
|
103
|
-
|
|
104
|
-
: [],
|
|
105
|
-
assertions: input.assertion !== undefined
|
|
106
|
-
? appendCaseAssertion([], input.assertion, now)
|
|
107
|
-
: [],
|
|
149
|
+
actions,
|
|
150
|
+
assertions,
|
|
108
151
|
manualTrail: input.trailEvent !== undefined
|
|
109
152
|
? appendCaseManualTrailEvent([], input.trailEvent, now)
|
|
110
153
|
: [],
|
|
111
|
-
sightings
|
|
112
|
-
|
|
154
|
+
sightings,
|
|
155
|
+
observedEffects,
|
|
156
|
+
closures,
|
|
157
|
+
branches: input.branch !== undefined
|
|
158
|
+
? appendCaseInvestigationBranch([], input.branch, now, branchReferences)
|
|
113
159
|
: [],
|
|
114
160
|
createdAt: now,
|
|
115
161
|
updatedAt: now,
|
|
@@ -148,7 +194,7 @@ export function openOrCreateCase(cases, input, nowIso) {
|
|
|
148
194
|
* @returns {{ cases: CaseRecord[], record: CaseRecord }}
|
|
149
195
|
*/
|
|
150
196
|
export function updateCase(cases, id, patch, nowIso) {
|
|
151
|
-
const now = nowIso || new Date().toISOString();
|
|
197
|
+
const now = caseTimestampOrNull(nowIso) || new Date().toISOString();
|
|
152
198
|
const index = cases.findIndex((item) => item.id === id);
|
|
153
199
|
if (index < 0)
|
|
154
200
|
throw new Error('That case no longer exists.');
|
|
@@ -168,7 +214,7 @@ export function updateCase(cases, id, patch, nowIso) {
|
|
|
168
214
|
const source = patch.source !== undefined ? normalizeSource(patch.source) : current.source;
|
|
169
215
|
let evidenceHistory = current.evidenceHistory;
|
|
170
216
|
if (patch.evidence !== undefined) {
|
|
171
|
-
evidenceHistory = normalizeEvidenceHistory([...current.evidenceHistory, ...(patch.evidence ? [patch.evidence] : [])], { source: inferCaptureSource(source), fallback: now });
|
|
217
|
+
evidenceHistory = normalizeEvidenceHistory([...current.evidenceHistory, ...(patch.evidence ? [patch.evidence] : [])], { source: inferCaptureSource(source), fallback: now, caseDomain: current.domain });
|
|
172
218
|
}
|
|
173
219
|
let evidencePins = current.evidencePins;
|
|
174
220
|
if (patch.evidencePins !== undefined) {
|
|
@@ -187,7 +233,7 @@ export function updateCase(cases, id, patch, nowIso) {
|
|
|
187
233
|
actions = appendCaseAction(current.actions, patch.action, now);
|
|
188
234
|
}
|
|
189
235
|
if (patch.actionUpdate !== undefined) {
|
|
190
|
-
actions = updateCaseAction(actions, patch.actionUpdate, now);
|
|
236
|
+
actions = updateCaseAction(actions, patch.actionUpdate, now, pinIds);
|
|
191
237
|
}
|
|
192
238
|
let assertions = current.assertions;
|
|
193
239
|
if (patch.assertion !== undefined) {
|
|
@@ -196,6 +242,14 @@ export function updateCase(cases, id, patch, nowIso) {
|
|
|
196
242
|
if (patch.assertionUpdate !== undefined) {
|
|
197
243
|
assertions = updateCaseAssertion(assertions, patch.assertionUpdate, now, pinIds);
|
|
198
244
|
}
|
|
245
|
+
const branchReferences = caseInvestigationBranchReferences({ evidencePins, actions, assertions });
|
|
246
|
+
let branches = normalizeCaseInvestigationBranches(current.branches ?? [], current.updatedAt, branchReferences);
|
|
247
|
+
if (patch.branch !== undefined) {
|
|
248
|
+
branches = appendCaseInvestigationBranch(branches, patch.branch, now, branchReferences);
|
|
249
|
+
}
|
|
250
|
+
if (patch.branchUpdate !== undefined) {
|
|
251
|
+
branches = updateCaseInvestigationBranch(branches, patch.branchUpdate, now, branchReferences);
|
|
252
|
+
}
|
|
199
253
|
let manualTrail = current.manualTrail;
|
|
200
254
|
if (patch.trailEvent !== undefined) {
|
|
201
255
|
manualTrail = appendCaseManualTrailEvent(current.manualTrail, patch.trailEvent, now);
|
|
@@ -204,13 +258,29 @@ export function updateCase(cases, id, patch, nowIso) {
|
|
|
204
258
|
if (patch.sighting !== undefined) {
|
|
205
259
|
sightings = appendCaseSighting(current.sightings, patch.sighting, now, pinIds);
|
|
206
260
|
}
|
|
261
|
+
let observedEffects = normalizeCaseObservedEffectHistory(current.observedEffects, current.updatedAt, pinIds, new Set(sightings.map((item) => item.id)));
|
|
262
|
+
if (patch.observedEffectReview !== undefined) {
|
|
263
|
+
observedEffects = appendCaseObservedEffectReview(observedEffects, patch.observedEffectReview, now, pinIds, new Set(sightings.map((item) => item.id)));
|
|
264
|
+
}
|
|
265
|
+
let closures = normalizeCaseClosureHistory(current.closures, current.updatedAt, new Set(observedEffects.reviews.map((item) => item.id)), new Set(actions.map((item) => item.id)), {}, buildCaseClosureLinkContext(observedEffects, actions));
|
|
266
|
+
if (patch.closure !== undefined) {
|
|
267
|
+
closures = appendCaseClosure(closures, patch.closure, now, observedEffects, actions);
|
|
268
|
+
}
|
|
269
|
+
if (patch.status === 'resolved' && patch.closure === undefined) {
|
|
270
|
+
throw new Error('Resolve this case through the deliberate closure review so the reason and evidence state remain explicit.');
|
|
271
|
+
}
|
|
207
272
|
const record = {
|
|
208
273
|
...current,
|
|
209
|
-
status: patch.
|
|
274
|
+
status: patch.closure !== undefined
|
|
275
|
+
? 'resolved'
|
|
276
|
+
: patch.status !== undefined ? normalizeStatus(patch.status) : current.status,
|
|
210
277
|
disposition: patch.disposition !== undefined ? normalizeDisposition(patch.disposition) : current.disposition,
|
|
211
278
|
reviewReasonCode: patch.reviewReasonCode !== undefined
|
|
212
279
|
? normalizeReviewReasonCode(patch.reviewReasonCode)
|
|
213
280
|
: current.reviewReasonCode ?? null,
|
|
281
|
+
brandProfileIds: patch.brandProfileIds !== undefined
|
|
282
|
+
? assertCaseBrandProfileIds(patch.brandProfileIds)
|
|
283
|
+
: current.brandProfileIds,
|
|
214
284
|
tags: patch.tags !== undefined ? normalizeTags(patch.tags) : current.tags,
|
|
215
285
|
source,
|
|
216
286
|
evidenceHistory,
|
|
@@ -220,6 +290,9 @@ export function updateCase(cases, id, patch, nowIso) {
|
|
|
220
290
|
assertions,
|
|
221
291
|
manualTrail,
|
|
222
292
|
sightings,
|
|
293
|
+
observedEffects,
|
|
294
|
+
closures,
|
|
295
|
+
branches,
|
|
223
296
|
notes,
|
|
224
297
|
updatedAt: now,
|
|
225
298
|
};
|
|
@@ -8,22 +8,25 @@
|
|
|
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 { caseEvidenceIncomparableReasons, compareCaseEvidence, latestCaseEvidence } from "./case-model.
|
|
12
|
-
import { httpSecurityHeaderLabel } from "./http-summary.
|
|
13
|
-
import { analystInteroperabilityTags } from "
|
|
14
|
-
import {
|
|
11
|
+
import { caseEvidenceIncomparableReasons, compareCaseEvidence, latestCaseEvidence } from "./case-model.mjs";
|
|
12
|
+
import { httpSecurityHeaderLabel } from "./http-summary.mjs";
|
|
13
|
+
import { analystInteroperabilityTags } from "../../lib/analyst-taxonomy.mjs";
|
|
14
|
+
import { buildPortableGeneratorMetadata, portableGeneratorAttribution, } from "../../lib/portable-generator.mjs";
|
|
15
|
+
import { buildCaseResponseLifecycleSummary, CASE_EVIDENCE_RELATION_STANCES } from "./case-response-model.mjs";
|
|
16
|
+
import { normalizeCaseBrandProfileIds } from "./case-brand-profile-references.mjs";
|
|
17
|
+
import { CASE_REPORT_SCHEMA, CASE_REPORT_SCHEMA_VERSION, } from "../contracts/case-portability.mjs";
|
|
15
18
|
// ---------------------------------------------------------------------------
|
|
16
19
|
// Constants
|
|
17
20
|
// ---------------------------------------------------------------------------
|
|
18
|
-
export
|
|
19
|
-
export const CASE_REPORT_SCHEMA_VERSION = 6;
|
|
20
|
-
const APPLICATION_NAME = 'WHOISleuth';
|
|
21
|
+
export { CASE_REPORT_SCHEMA, CASE_REPORT_SCHEMA_VERSION };
|
|
21
22
|
const LIMITATIONS_TEXT = [
|
|
22
23
|
'This report contains normalised browser-local observations from WHOISleuth analyst cases.',
|
|
23
24
|
'It is not a live lookup and does not contain raw WHOIS, RDAP, DNS, HTML, or responses collected during website checks.',
|
|
24
25
|
'Absence of a signal (e.g. no MX record observed) does not prove nonexistence. It may not have been evaluated.',
|
|
25
26
|
'Snapshot fingerprints are deduplication identifiers, not cryptographic evidence hashes.',
|
|
26
27
|
'Scan-depth and scoring-model gates prevent misleading comparisons; "incomparable" means observations differ materially but one or more fields cannot be compared reliably.',
|
|
28
|
+
'Brand Profile references record an explicit analyst-selected association only; they do not establish ownership, attribution, intent, safety, or maliciousness.',
|
|
29
|
+
'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.',
|
|
27
30
|
'Generated locally in the browser. Review the package before sharing it.',
|
|
28
31
|
].join(' ');
|
|
29
32
|
function escapeMarkdownInline(text) {
|
|
@@ -139,6 +142,8 @@ function pickKnownSnapshotFields(snapshot) {
|
|
|
139
142
|
idnReferenceMatch: snapshot.idnReferenceMatch ?? null,
|
|
140
143
|
pageBaselineMatch: snapshot.pageBaselineMatch ?? null,
|
|
141
144
|
hasActiveBrandProfile: snapshot.hasActiveBrandProfile ?? null,
|
|
145
|
+
profileContextState: snapshot.profileContextState ?? null,
|
|
146
|
+
profileContextLimitation: snapshot.profileContextLimitation ?? null,
|
|
142
147
|
mutationTypes: Array.isArray(snapshot.mutationTypes) ? [...snapshot.mutationTypes] : [],
|
|
143
148
|
};
|
|
144
149
|
}
|
|
@@ -158,6 +163,7 @@ export function buildCaseReport(caseRecord, options = {}) {
|
|
|
158
163
|
const { generatedAt } = options;
|
|
159
164
|
const includeNotes = options.includeNotes === true;
|
|
160
165
|
const now = generatedAt || new Date().toISOString();
|
|
166
|
+
const generator = buildPortableGeneratorMetadata(options.applicationVersion);
|
|
161
167
|
// --- Build JSON report ---
|
|
162
168
|
const timelineEntries = [];
|
|
163
169
|
if (Array.isArray(caseRecord.evidenceHistory) && caseRecord.evidenceHistory.length > 0) {
|
|
@@ -176,7 +182,9 @@ export function buildCaseReport(caseRecord, options = {}) {
|
|
|
176
182
|
if (!previous)
|
|
177
183
|
continue;
|
|
178
184
|
const rawChanges = compareCaseEvidence(previous, snapshot);
|
|
179
|
-
|
|
185
|
+
const allIncomparableReasons = caseEvidenceIncomparableReasons(previous, snapshot);
|
|
186
|
+
const hostnameContextChanged = allIncomparableReasons.includes('observation-context');
|
|
187
|
+
incomparableReasons = allIncomparableReasons.filter((reason) => reason !== 'observation-context');
|
|
180
188
|
if (rawChanges.length > 0) {
|
|
181
189
|
changes = rawChanges.map((change) => ({
|
|
182
190
|
field: change.field,
|
|
@@ -186,7 +194,9 @@ export function buildCaseReport(caseRecord, options = {}) {
|
|
|
186
194
|
tone: change.tone,
|
|
187
195
|
}));
|
|
188
196
|
}
|
|
189
|
-
else if (snapshot.fingerprint !== previous.fingerprint
|
|
197
|
+
else if (snapshot.fingerprint !== previous.fingerprint
|
|
198
|
+
&& incomparableReasons.length === 0
|
|
199
|
+
&& !hostnameContextChanged) {
|
|
190
200
|
incomparableReasons = ['other'];
|
|
191
201
|
}
|
|
192
202
|
hasIncomparableChange = incomparableReasons.length > 0;
|
|
@@ -203,17 +213,31 @@ export function buildCaseReport(caseRecord, options = {}) {
|
|
|
203
213
|
}
|
|
204
214
|
const latest = latestCaseEvidence({ evidenceHistory: caseRecord.evidenceHistory ?? undefined });
|
|
205
215
|
const currentAssessment = latest ? pickKnownSnapshotFields(latest) : null;
|
|
216
|
+
const responseLifecycle = buildCaseResponseLifecycleSummary(caseRecord);
|
|
217
|
+
const observedEffects = caseRecord.observedEffects ?? {
|
|
218
|
+
reviews: [],
|
|
219
|
+
omitted: 0,
|
|
220
|
+
preV13HistoryUnavailable: false,
|
|
221
|
+
limitations: ['Observed-effect reviews are independent point-in-time records; provider workflow events do not create or replace them.'],
|
|
222
|
+
};
|
|
223
|
+
const closures = caseRecord.closures ?? {
|
|
224
|
+
records: [],
|
|
225
|
+
omitted: 0,
|
|
226
|
+
preV13HistoryUnavailable: false,
|
|
227
|
+
limitations: ['Closure records are deliberate analyst actions and do not establish absence, safety, provider performance, or legal sufficiency.'],
|
|
228
|
+
};
|
|
206
229
|
const json = {
|
|
207
230
|
schema: CASE_REPORT_SCHEMA,
|
|
208
231
|
schemaVersion: CASE_REPORT_SCHEMA_VERSION,
|
|
209
232
|
generatedAt: now,
|
|
210
|
-
application:
|
|
233
|
+
application: generator,
|
|
211
234
|
case: {
|
|
212
235
|
id: caseRecord.id,
|
|
213
236
|
domain: caseRecord.domain,
|
|
214
237
|
status: caseRecord.status,
|
|
215
238
|
disposition: caseRecord.disposition,
|
|
216
239
|
reviewReasonCode: caseRecord.reviewReasonCode ?? null,
|
|
240
|
+
brandProfileIds: normalizeCaseBrandProfileIds(caseRecord.brandProfileIds),
|
|
217
241
|
interoperabilityTags: analystInteroperabilityTags(caseRecord.disposition, caseRecord.reviewReasonCode),
|
|
218
242
|
tags: Array.isArray(caseRecord.tags) ? [...caseRecord.tags] : [],
|
|
219
243
|
source: caseRecord.source,
|
|
@@ -227,7 +251,12 @@ export function buildCaseReport(caseRecord, options = {}) {
|
|
|
227
251
|
analystResponse: {
|
|
228
252
|
evidencePins: caseRecord.evidencePins.map((item) => ({ ...item, limitations: [...item.limitations] })),
|
|
229
253
|
decisions: caseRecord.decisions.map((item) => ({ ...item, evidencePinIds: [...item.evidencePinIds] })),
|
|
230
|
-
actions: caseRecord.actions.map((item) => ({
|
|
254
|
+
actions: caseRecord.actions.map((item) => ({
|
|
255
|
+
...item,
|
|
256
|
+
contactLimitations: [...item.contactLimitations],
|
|
257
|
+
history: item.history.map((event) => ({ ...event, limitations: [...event.limitations] })),
|
|
258
|
+
historyLimitations: [...item.historyLimitations],
|
|
259
|
+
})),
|
|
231
260
|
assertions: caseRecord.assertions.map((item) => ({
|
|
232
261
|
...item,
|
|
233
262
|
evidencePinIds: [...item.evidencePinIds],
|
|
@@ -242,11 +271,31 @@ export function buildCaseReport(caseRecord, options = {}) {
|
|
|
242
271
|
})),
|
|
243
272
|
manualTrail: caseRecord.manualTrail.map((item) => ({ ...item })),
|
|
244
273
|
sightings: caseRecord.sightings.map((item) => ({ ...item, limitations: [...item.limitations] })),
|
|
274
|
+
observedEffects: {
|
|
275
|
+
reviews: observedEffects.reviews.map((item) => ({ ...item, limitations: [...item.limitations] })),
|
|
276
|
+
omitted: observedEffects.omitted,
|
|
277
|
+
preV13HistoryUnavailable: observedEffects.preV13HistoryUnavailable,
|
|
278
|
+
limitations: [...observedEffects.limitations],
|
|
279
|
+
},
|
|
280
|
+
closures: {
|
|
281
|
+
records: closures.records.map((item) => ({ ...item, limitations: [...item.limitations] })),
|
|
282
|
+
omitted: closures.omitted,
|
|
283
|
+
preV13HistoryUnavailable: closures.preV13HistoryUnavailable,
|
|
284
|
+
limitations: [...closures.limitations],
|
|
285
|
+
},
|
|
286
|
+
branches: (caseRecord.branches ?? []).map((item) => ({
|
|
287
|
+
...item,
|
|
288
|
+
evidencePinIds: [...item.evidencePinIds],
|
|
289
|
+
checkpointIds: [...item.checkpointIds],
|
|
290
|
+
assertionIds: [...item.assertionIds],
|
|
291
|
+
actionIds: [...item.actionIds],
|
|
292
|
+
})),
|
|
245
293
|
},
|
|
294
|
+
responseLifecycle,
|
|
246
295
|
limitations: LIMITATIONS_TEXT,
|
|
247
296
|
};
|
|
248
297
|
// --- Build Markdown ---
|
|
249
|
-
const md = buildMarkdown(json);
|
|
298
|
+
const md = buildMarkdown(json, options.includeAttribution !== false);
|
|
250
299
|
return { json, markdown: md };
|
|
251
300
|
}
|
|
252
301
|
// ---------------------------------------------------------------------------
|
|
@@ -257,7 +306,7 @@ export function buildCaseReport(caseRecord, options = {}) {
|
|
|
257
306
|
* @param {object} report
|
|
258
307
|
* @returns {string}
|
|
259
308
|
*/
|
|
260
|
-
function buildMarkdown(report) {
|
|
309
|
+
function buildMarkdown(report, includeAttribution) {
|
|
261
310
|
const lines = [];
|
|
262
311
|
// Title
|
|
263
312
|
const domain = escapeMarkdownInline(report.case.domain || 'unknown');
|
|
@@ -276,6 +325,7 @@ function buildMarkdown(report) {
|
|
|
276
325
|
lines.push(`| Disposition | ${escapeMarkdownInline(report.case.disposition)} |`);
|
|
277
326
|
if (report.case.reviewReasonCode)
|
|
278
327
|
lines.push(`| Review reason | ${escapeMarkdownInline(report.case.reviewReasonCode.replaceAll('_', ' '))} |`);
|
|
328
|
+
lines.push(`| Brand Profile references | ${report.case.brandProfileIds.length ? escapeMarkdownInline(report.case.brandProfileIds.join(', ')) : 'None'} |`);
|
|
279
329
|
lines.push(`| Source | ${escapeMarkdownInline(report.case.source)} |`);
|
|
280
330
|
lines.push(`| Opened | ${escapeMarkdownInline(report.case.openedAt)} |`);
|
|
281
331
|
lines.push(`| Updated | ${escapeMarkdownInline(report.case.updatedAt)} |`);
|
|
@@ -292,6 +342,9 @@ function buildMarkdown(report) {
|
|
|
292
342
|
lines.push(`- **Availability:** ${escapeMarkdownInline(formatReportValue(a.availability))}`);
|
|
293
343
|
lines.push(`- **Risk score:** ${escapeMarkdownInline(formatReportValue(a.riskScore))}`);
|
|
294
344
|
lines.push(`- **Risk model:** ${a.riskModelVersion === null ? 'Unversioned' : `v${escapeMarkdownInline(formatReportValue(a.riskModelVersion))}`}`);
|
|
345
|
+
lines.push(`- **Brand Profile context:** ${escapeMarkdownInline(formatReportValue(a.profileContextState))}`);
|
|
346
|
+
if (a.profileContextLimitation)
|
|
347
|
+
lines.push(`- **Profile-context limitation:** ${escapeMarkdownInline(a.profileContextLimitation)}`);
|
|
295
348
|
lines.push(`- **Opportunity model:** ${a.opportunityModelVersion == null ? 'Unversioned' : `v${escapeMarkdownInline(formatReportValue(a.opportunityModelVersion))}`}`);
|
|
296
349
|
lines.push(`- **Registrar:** ${escapeMarkdownInline(formatReportValue(a.registrar))}`);
|
|
297
350
|
lines.push(`- **Website activity:** ${escapeMarkdownInline(formatReportValue(a.activityStatus))}`);
|
|
@@ -342,6 +395,9 @@ function buildMarkdown(report) {
|
|
|
342
395
|
lines.push(`- Availability: ${escapeMarkdownInline(formatReportValue(snap.availability))}`);
|
|
343
396
|
lines.push(`- Risk score: ${escapeMarkdownInline(formatReportValue(snap.riskScore))}`);
|
|
344
397
|
lines.push(`- Risk model: ${snap.riskModelVersion === null ? 'Unversioned' : `v${escapeMarkdownInline(formatReportValue(snap.riskModelVersion))}`}`);
|
|
398
|
+
lines.push(`- Brand Profile context: ${escapeMarkdownInline(formatReportValue(snap.profileContextState))}`);
|
|
399
|
+
if (snap.profileContextLimitation)
|
|
400
|
+
lines.push(`- Profile-context limitation: ${escapeMarkdownInline(snap.profileContextLimitation)}`);
|
|
345
401
|
lines.push(`- Registrar: ${escapeMarkdownInline(formatReportValue(snap.registrar))}`);
|
|
346
402
|
lines.push(`- Website activity: ${escapeMarkdownInline(formatReportValue(snap.activityStatus))}`);
|
|
347
403
|
if (snap.httpResponseStatus != null)
|
|
@@ -411,8 +467,8 @@ function buildMarkdown(report) {
|
|
|
411
467
|
lines.push('## Analyst Decision Packet');
|
|
412
468
|
lines.push('');
|
|
413
469
|
const response = report.analystResponse;
|
|
414
|
-
if (!response.evidencePins.length && !response.sightings.length && !response.decisions.length && !response.actions.length && !response.assertions.length && !response.manualTrail.length) {
|
|
415
|
-
lines.push('No evidence pins, source-qualified sightings, structured assertions, decision records,
|
|
470
|
+
if (!response.evidencePins.length && !response.sightings.length && !response.decisions.length && !response.actions.length && !response.assertions.length && !response.manualTrail.length && !response.observedEffects.reviews.length && !response.closures.records.length && !response.branches.length) {
|
|
471
|
+
lines.push('No evidence pins, source-qualified sightings, structured assertions, decision records, response actions, independent effect reviews, closures, or manual investigation steps recorded.');
|
|
416
472
|
lines.push('');
|
|
417
473
|
}
|
|
418
474
|
else {
|
|
@@ -499,16 +555,44 @@ function buildMarkdown(report) {
|
|
|
499
555
|
for (const action of response.actions) {
|
|
500
556
|
lines.push(`- **${escapeMarkdownInline(action.type.replaceAll('_', ' '))}:** ${escapeMarkdownInline(action.recipient)} (${escapeMarkdownInline(action.state)})`);
|
|
501
557
|
lines.push(` Contact source: ${escapeMarkdownInline(action.contactSource)}; updated ${escapeMarkdownInline(action.updatedAt)}.`);
|
|
558
|
+
if (action.originActionId)
|
|
559
|
+
lines.push(` Originating action: ${escapeMarkdownInline(action.originActionId)}`);
|
|
502
560
|
if (action.dueAt)
|
|
503
561
|
lines.push(` Due: ${escapeMarkdownInline(action.dueAt)}`);
|
|
504
562
|
if (action.followUpAt)
|
|
505
563
|
lines.push(` Follow-up: ${escapeMarkdownInline(action.followUpAt)}`);
|
|
506
564
|
if (action.reference)
|
|
507
565
|
lines.push(` Reference: ${escapeMarkdownInline(action.reference)}`);
|
|
566
|
+
if (action.providerOutcome)
|
|
567
|
+
lines.push(` Latest typed provider outcome: ${escapeMarkdownInline(action.providerOutcome.replaceAll('_', ' '))}`);
|
|
508
568
|
if (action.outcome)
|
|
509
|
-
lines.push(`
|
|
569
|
+
lines.push(` ${action.providerOutcome ? 'Provider' : 'Legacy recorded'} outcome detail: ${escapeMarkdownInline(action.outcome)}`);
|
|
510
570
|
if (action.contactLimitations.length)
|
|
511
571
|
lines.push(` Contact limitations: ${escapeMarkdownInline(action.contactLimitations.join('; '))}`);
|
|
572
|
+
for (const event of action.history) {
|
|
573
|
+
lines.push(` - ${escapeMarkdownInline(event.occurredAt)} · ${escapeMarkdownInline(event.previousState ?? 'none')} → ${escapeMarkdownInline(event.nextState)} · ${escapeMarkdownInline(event.sourceClass)} · ${event.applied ? 'applied' : 'retained conflict'}`);
|
|
574
|
+
if (event.providerOutcome)
|
|
575
|
+
lines.push(` Provider outcome at this time: ${escapeMarkdownInline(event.providerOutcome.replaceAll('_', ' '))}`);
|
|
576
|
+
if (event.reference)
|
|
577
|
+
lines.push(` Event reference: ${escapeMarkdownInline(event.reference)}`);
|
|
578
|
+
if (event.evidencePinId)
|
|
579
|
+
lines.push(` Evidence pin: ${escapeMarkdownInline(event.evidencePinId)}`);
|
|
580
|
+
if (event.limitations.length)
|
|
581
|
+
lines.push(` Limitations: ${escapeMarkdownInline(event.limitations.join('; '))}`);
|
|
582
|
+
}
|
|
583
|
+
if (action.historyOmitted)
|
|
584
|
+
lines.push(` Earlier transition events omitted by bound: ${action.historyOmitted}`);
|
|
585
|
+
if (action.historyLimitations.length)
|
|
586
|
+
lines.push(` History limitations: ${escapeMarkdownInline(action.historyLimitations.join('; '))}`);
|
|
587
|
+
}
|
|
588
|
+
lines.push('');
|
|
589
|
+
lines.push('### Investigation branches');
|
|
590
|
+
lines.push('');
|
|
591
|
+
if (!response.branches.length)
|
|
592
|
+
lines.push('No named investigation branches recorded.');
|
|
593
|
+
for (const branch of response.branches) {
|
|
594
|
+
lines.push(`- **${escapeMarkdownInline(branch.name)}** (${escapeMarkdownInline(branch.state)}; updated ${escapeMarkdownInline(branch.updatedAt)})`);
|
|
595
|
+
lines.push(` References: ${branch.evidencePinIds.length} evidence pins; ${branch.checkpointIds.length} checkpoints; ${branch.assertionIds.length} assertions; ${branch.actionIds.length} actions.`);
|
|
512
596
|
}
|
|
513
597
|
lines.push('');
|
|
514
598
|
lines.push('### Explicit investigation trail');
|
|
@@ -522,6 +606,45 @@ function buildMarkdown(report) {
|
|
|
522
606
|
}
|
|
523
607
|
lines.push('');
|
|
524
608
|
}
|
|
609
|
+
lines.push('### Independent remediation review and closure');
|
|
610
|
+
lines.push('');
|
|
611
|
+
const providerOutcome = report.responseLifecycle.latestProviderOutcome;
|
|
612
|
+
const observedEffect = report.responseLifecycle.latestObservedEffect;
|
|
613
|
+
lines.push(providerOutcome
|
|
614
|
+
? `- Latest provider outcome time: ${escapeMarkdownInline(providerOutcome.occurredAt)} (${escapeMarkdownInline(providerOutcome.outcome.replaceAll('_', ' '))})`
|
|
615
|
+
: `- Latest provider outcome time: Withheld because the typed event state is ${escapeMarkdownInline(report.responseLifecycle.providerOutcomeState)}.`);
|
|
616
|
+
lines.push(report.responseLifecycle.latestObservedChangeAt
|
|
617
|
+
? `- Latest independently observed change time: ${escapeMarkdownInline(report.responseLifecycle.latestObservedChangeAt)}`
|
|
618
|
+
: `- Latest independently observed change time: Withheld because the independent change state is ${escapeMarkdownInline(report.responseLifecycle.observedChangeState)}.`);
|
|
619
|
+
if (observedEffect)
|
|
620
|
+
lines.push(`- Latest independent review: ${escapeMarkdownInline(observedEffect.state.replaceAll('_', ' '))} at ${escapeMarkdownInline(observedEffect.observedAt)} from ${escapeMarkdownInline(observedEffect.source)}.`);
|
|
621
|
+
if (!response.observedEffects.reviews.length)
|
|
622
|
+
lines.push('- No independent observed-effect review recorded.');
|
|
623
|
+
for (const review of response.observedEffects.reviews) {
|
|
624
|
+
lines.push(`- **${escapeMarkdownInline(review.state.replaceAll('_', ' '))}** (${escapeMarkdownInline(review.observedAt)}): ${escapeMarkdownInline(review.source)}; class ${escapeMarkdownInline(review.sourceClass)}; completeness ${escapeMarkdownInline(review.completeness)}.`);
|
|
625
|
+
if (review.evidencePinId)
|
|
626
|
+
lines.push(` Evidence pin: ${escapeMarkdownInline(review.evidencePinId)}`);
|
|
627
|
+
if (review.sightingId)
|
|
628
|
+
lines.push(` Sighting: ${escapeMarkdownInline(review.sightingId)}`);
|
|
629
|
+
if (review.followUpAt)
|
|
630
|
+
lines.push(` Scheduled local follow-up: ${escapeMarkdownInline(review.followUpAt)}`);
|
|
631
|
+
if (review.limitations.length)
|
|
632
|
+
lines.push(` Limitations: ${escapeMarkdownInline(review.limitations.join('; '))}`);
|
|
633
|
+
}
|
|
634
|
+
if (response.observedEffects.omitted)
|
|
635
|
+
lines.push(`- Earlier independent reviews omitted by bound: ${response.observedEffects.omitted}`);
|
|
636
|
+
if (!response.closures.records.length)
|
|
637
|
+
lines.push('- No deliberate analyst closure recorded.');
|
|
638
|
+
for (const closure of response.closures.records) {
|
|
639
|
+
lines.push(`- **Closure · ${escapeMarkdownInline(closure.reason.replaceAll('_', ' '))}** (${escapeMarkdownInline(closure.createdAt)}): ${escapeMarkdownInline(closure.summary)}`);
|
|
640
|
+
if (closure.observedEffectReviewId)
|
|
641
|
+
lines.push(` Independent review: ${escapeMarkdownInline(closure.observedEffectReviewId)}`);
|
|
642
|
+
if (closure.actionId)
|
|
643
|
+
lines.push(` Provider action: ${escapeMarkdownInline(closure.actionId)}`);
|
|
644
|
+
if (closure.limitations.length)
|
|
645
|
+
lines.push(` Limitations: ${escapeMarkdownInline(closure.limitations.join('; '))}`);
|
|
646
|
+
}
|
|
647
|
+
lines.push('');
|
|
525
648
|
// Analyst notes (only when included)
|
|
526
649
|
if (report.case.notesIncluded && Array.isArray(report.case.notes) && report.case.notes.length > 0) {
|
|
527
650
|
lines.push('## Analyst Notes');
|
|
@@ -542,6 +665,10 @@ function buildMarkdown(report) {
|
|
|
542
665
|
lines.push('');
|
|
543
666
|
lines.push(LIMITATIONS_TEXT);
|
|
544
667
|
lines.push('');
|
|
668
|
+
if (includeAttribution) {
|
|
669
|
+
lines.push(portableGeneratorAttribution(report.application));
|
|
670
|
+
lines.push('');
|
|
671
|
+
}
|
|
545
672
|
return lines.join('\n');
|
|
546
673
|
}
|
|
547
674
|
// ---------------------------------------------------------------------------
|