@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
package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs}
RENAMED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Pure, framework-neutral analyst-case records, evidence histories, bounded
|
|
2
2
|
// record normalization, and analyst updates.
|
|
3
|
-
import { normalizeHttpSummary } from "./http-summary.
|
|
4
|
-
import { normalizeOpportunityModelVersion
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import { normalizeHttpSummary } from "./http-summary.mjs";
|
|
4
|
+
import { normalizeOpportunityModelVersion } from "../../lib/opportunity-scoring.mjs";
|
|
5
|
+
import { normalizeRiskModelVersion } from "../../lib/risk-scoring.mjs";
|
|
6
|
+
import { MAX_EVIDENCE_CHANGES, MAX_EVIDENCE_DETAIL_LENGTH, MAX_EVIDENCE_FACTORS, MAX_EVIDENCE_MUTATIONS, MAX_EVIDENCE_NAMESERVERS, MAX_EVIDENCE_SNAPSHOTS_PER_CASE, MAX_EVIDENCE_STRING_LENGTH, MAX_EVIDENCE_TITLE_LENGTH, } from "./case-record-contracts.mjs";
|
|
7
|
+
import { CONCLUSIVE_AVAILABILITY, DEFAULT_EVIDENCE_SOURCE, EVIDENCE_SOURCE_RANK, EVIDENCE_SOURCE_SET, REGISTERED_LIKE, caseTimestampOrNull, hashString, isoOrNull, normalizeEvidenceHostnameForCase, objectRecord, safeId, } from "./case-record-core.mjs";
|
|
7
8
|
function evidenceString(value, max = MAX_EVIDENCE_STRING_LENGTH) {
|
|
8
9
|
if (value == null)
|
|
9
10
|
return null;
|
|
@@ -81,6 +82,12 @@ function dayOf(value) {
|
|
|
81
82
|
const iso = isoOrNull(value);
|
|
82
83
|
return iso ? iso.slice(0, 10) : null;
|
|
83
84
|
}
|
|
85
|
+
function lifecycleTimestamp(value, sourceVersion) {
|
|
86
|
+
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/u.test(value)) {
|
|
87
|
+
return caseTimestampOrNull(`${value}T00:00:00Z`);
|
|
88
|
+
}
|
|
89
|
+
return caseTimestampOrNull(value, sourceVersion);
|
|
90
|
+
}
|
|
84
91
|
// Capture completeness is recorded explicitly, never inferred from whether a
|
|
85
92
|
// boolean happens to be false. 'fast' captures skip the DNS/site/HTML probes;
|
|
86
93
|
// 'deep' evaluates them; 'unknown' is for migrated/imported evidence whose depth
|
|
@@ -104,6 +111,7 @@ const DEEP_SIGNAL_FIELDS = [
|
|
|
104
111
|
// included so captures of differing completeness can never be confused for one
|
|
105
112
|
// another. Deterministic ordering here is what makes the fingerprint stable.
|
|
106
113
|
const MATERIAL_FIELD_ORDER = [
|
|
114
|
+
'inputHostname',
|
|
107
115
|
'scanDepth',
|
|
108
116
|
'availability', 'confidence', 'riskModelVersion', 'riskScore', 'opportunityModelVersion', 'opportunityScore',
|
|
109
117
|
'riskFactors', 'opportunityFactors',
|
|
@@ -114,6 +122,7 @@ const MATERIAL_FIELD_ORDER = [
|
|
|
114
122
|
'httpCrossOriginRedirect', 'httpHttpsDowngrade', 'httpContentType', 'httpSecurityHeaders',
|
|
115
123
|
'faviconMatch', 'faviconNearMatch', 'reusesOfficialAssets', 'hasPasswordField', 'hasExternalFormAction', 'phishingLanguageMatch',
|
|
116
124
|
'privacyProtected', 'idnReferenceMatch', 'pageBaselineMatch', 'hasActiveBrandProfile',
|
|
125
|
+
'profileContextState', 'profileContextLimitation',
|
|
117
126
|
'mutationTypes',
|
|
118
127
|
];
|
|
119
128
|
// The canonical, comparison-safe value of a material field. Registrar casing,
|
|
@@ -156,7 +165,7 @@ function isEmptyMaterial(value) {
|
|
|
156
165
|
}
|
|
157
166
|
// Fields that describe the capture rather than assert evidence, so they never
|
|
158
167
|
// on their own keep an otherwise-empty snapshot alive.
|
|
159
|
-
const NON_EVIDENCE_MATERIAL = new Set(['scanDepth', 'confidence']);
|
|
168
|
+
const NON_EVIDENCE_MATERIAL = new Set(['inputHostname', 'scanDepth', 'confidence']);
|
|
160
169
|
// A snapshot with no material evidence (only timestamps/source/depth, or only a
|
|
161
170
|
// bare confidence/unknown-availability) is dropped rather than added to a
|
|
162
171
|
// timeline.
|
|
@@ -172,8 +181,15 @@ function hasMaterialEvidence(snapshot) {
|
|
|
172
181
|
// Deterministic string form of the material identity, keys in fixed order.
|
|
173
182
|
function canonicalMaterialString(snapshot) {
|
|
174
183
|
const canonical = {};
|
|
175
|
-
for (const field of MATERIAL_FIELD_ORDER)
|
|
176
|
-
|
|
184
|
+
for (const field of MATERIAL_FIELD_ORDER) {
|
|
185
|
+
const value = materialValue(field, snapshot);
|
|
186
|
+
// Preserve historical fingerprints when the new v14 observation-context
|
|
187
|
+
// field is absent. A retained hostname is still material and therefore
|
|
188
|
+
// separates otherwise-identical captures.
|
|
189
|
+
if (field === 'inputHostname' && value === null)
|
|
190
|
+
continue;
|
|
191
|
+
canonical[field] = value;
|
|
192
|
+
}
|
|
177
193
|
return JSON.stringify(canonical);
|
|
178
194
|
}
|
|
179
195
|
/**
|
|
@@ -185,7 +201,7 @@ function canonicalMaterialString(snapshot) {
|
|
|
185
201
|
* pass a real fallback; the import path deliberately passes an older case
|
|
186
202
|
* timestamp (never "now") so malformed imported evidence can't appear newest.
|
|
187
203
|
* @param {unknown} raw
|
|
188
|
-
* @param {{ source?: string, fallback?: string | null }} [options]
|
|
204
|
+
* @param {{ source?: string, fallback?: string | null, sourceVersion?: number | null, caseDomain?: string | null }} [options]
|
|
189
205
|
* @returns {CaseEvidenceSnapshot | null}
|
|
190
206
|
*/
|
|
191
207
|
export function normalizeSnapshot(raw, options = {}) {
|
|
@@ -199,7 +215,12 @@ function buildSnapshot(raw, options) {
|
|
|
199
215
|
const record = objectRecord(raw);
|
|
200
216
|
const scanDepth = normalizeScanDepth(record.scanDepth);
|
|
201
217
|
const httpSummary = normalizeHttpSummary(record);
|
|
218
|
+
const acceptsProfileContext = options.sourceVersion === undefined || Number(options.sourceVersion) >= 12;
|
|
219
|
+
const acceptsInputHostname = options.sourceVersion === undefined || Number(options.sourceVersion) >= 13;
|
|
202
220
|
const fields = {
|
|
221
|
+
inputHostname: acceptsInputHostname
|
|
222
|
+
? normalizeEvidenceHostnameForCase(record.inputHostname, options.caseDomain)
|
|
223
|
+
: null,
|
|
203
224
|
scanDepth,
|
|
204
225
|
availability: evidenceString(record.availability),
|
|
205
226
|
confidence: evidenceString(record.confidence),
|
|
@@ -210,8 +231,8 @@ function buildSnapshot(raw, options) {
|
|
|
210
231
|
riskFactors: normalizeFactors(record.riskFactors),
|
|
211
232
|
opportunityFactors: normalizeFactors(record.opportunityFactors),
|
|
212
233
|
registrar: evidenceString(record.registrar),
|
|
213
|
-
createdDate:
|
|
214
|
-
expiryDate:
|
|
234
|
+
createdDate: lifecycleTimestamp(record.createdDate, options.sourceVersion),
|
|
235
|
+
expiryDate: lifecycleTimestamp(record.expiryDate, options.sourceVersion),
|
|
215
236
|
nameservers: normalizeNameserverList(record.nameservers),
|
|
216
237
|
hasMx: boolOrNull(record.hasMx),
|
|
217
238
|
hasSpf: boolOrNull(record.hasSpf),
|
|
@@ -239,6 +260,12 @@ function buildSnapshot(raw, options) {
|
|
|
239
260
|
idnReferenceMatch: boolOrNull(record.idnReferenceMatch),
|
|
240
261
|
pageBaselineMatch: boolOrNull(record.pageBaselineMatch),
|
|
241
262
|
hasActiveBrandProfile: boolOrNull(record.hasActiveBrandProfile),
|
|
263
|
+
profileContextState: acceptsProfileContext && ['loading', 'ready', 'unavailable'].includes(String(record.profileContextState))
|
|
264
|
+
? record.profileContextState
|
|
265
|
+
: null,
|
|
266
|
+
profileContextLimitation: acceptsProfileContext
|
|
267
|
+
? evidenceString(record.profileContextLimitation, MAX_EVIDENCE_DETAIL_LENGTH)
|
|
268
|
+
: null,
|
|
242
269
|
mutationTypes: normalizeMutationList(record.mutationTypes),
|
|
243
270
|
};
|
|
244
271
|
// A version without an actual risk assessment is orphaned metadata. Drop it
|
|
@@ -278,10 +305,10 @@ function buildSnapshot(raw, options) {
|
|
|
278
305
|
}
|
|
279
306
|
if (!hasMaterialEvidence(fields))
|
|
280
307
|
return null;
|
|
281
|
-
const capturedAt =
|
|
308
|
+
const capturedAt = caseTimestampOrNull(record.capturedAt, options.sourceVersion) || options.fallback || null;
|
|
282
309
|
if (!capturedAt)
|
|
283
310
|
return null; // an evidence entry with no placeable time is skipped
|
|
284
|
-
let firstCapturedAt =
|
|
311
|
+
let firstCapturedAt = caseTimestampOrNull(record.firstCapturedAt, options.sourceVersion) || capturedAt;
|
|
285
312
|
if (Date.parse(firstCapturedAt) > Date.parse(capturedAt))
|
|
286
313
|
firstCapturedAt = capturedAt;
|
|
287
314
|
const source = typeof record.source === 'string' && EVIDENCE_SOURCE_SET.has(record.source)
|
|
@@ -343,7 +370,7 @@ function compareSnapshotChrono(a, b) {
|
|
|
343
370
|
* snapshots are retained up to the per-case bound; ids are made unique within
|
|
344
371
|
* the case.
|
|
345
372
|
* @param {unknown} rawList
|
|
346
|
-
* @param {{ source?: string, fallback?: string | null }} [options]
|
|
373
|
+
* @param {{ source?: string, fallback?: string | null, sourceVersion?: number | null, caseDomain?: string | null }} [options]
|
|
347
374
|
* @returns {CaseEvidenceSnapshot[]}
|
|
348
375
|
*/
|
|
349
376
|
export function normalizeEvidenceHistory(rawList, options = {}) {
|
|
@@ -453,12 +480,14 @@ function valuesMateriallyEqual(field, previous, current) {
|
|
|
453
480
|
* in the same observation produced an ordinary material change.
|
|
454
481
|
* @param {CaseEvidenceSnapshot | null | undefined} previous
|
|
455
482
|
* @param {CaseEvidenceSnapshot | null | undefined} current
|
|
456
|
-
* @returns {Array<'scan-depth' | 'risk-model'>}
|
|
483
|
+
* @returns {Array<'observation-context' | 'opportunity-model' | 'scan-depth' | 'risk-model'>}
|
|
457
484
|
*/
|
|
458
485
|
export function caseEvidenceIncomparableReasons(previous, current) {
|
|
459
486
|
if (!previous || !current || previous.fingerprint === current.fingerprint)
|
|
460
487
|
return [];
|
|
461
488
|
const reasons = [];
|
|
489
|
+
if (previous.inputHostname !== current.inputHostname)
|
|
490
|
+
reasons.push('observation-context');
|
|
462
491
|
const hasRiskEvidence = previous.riskScore !== null || current.riskScore !== null
|
|
463
492
|
|| previous.riskFactors.length > 0 || current.riskFactors.length > 0;
|
|
464
493
|
if (hasRiskEvidence && !riskModelComparable(previous, current))
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { normalizeExplicitIsoTimestamp, normalizeLegacyIsoTimestamp } from "../evidence/observation.mjs";
|
|
2
|
+
export const MAX_CASE_INVESTIGATION_BRANCHES = 8;
|
|
3
|
+
export const MAX_CASE_BRANCH_NAME_LENGTH = 80;
|
|
4
|
+
export const MAX_CASE_BRANCH_REFERENCES = 12;
|
|
5
|
+
export const CASE_INVESTIGATION_BRANCH_STATES = ['active', 'resolved'];
|
|
6
|
+
const SAFE_ID_RE = /^[A-Za-z0-9_-]{1,64}$/u;
|
|
7
|
+
const CONTROL_REPLACE_RE = /[\u0000-\u001f\u007f]+/gu;
|
|
8
|
+
function record(value) {
|
|
9
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
10
|
+
}
|
|
11
|
+
function text(value, maximum) {
|
|
12
|
+
return typeof value === 'string'
|
|
13
|
+
? value.replace(CONTROL_REPLACE_RE, ' ').replace(/\s+/gu, ' ').trim().slice(0, maximum)
|
|
14
|
+
: '';
|
|
15
|
+
}
|
|
16
|
+
function iso(value, fallback, options = {}) {
|
|
17
|
+
return normalizeExplicitIsoTimestamp(value)
|
|
18
|
+
?? (options.legacyTimestamps ? normalizeLegacyIsoTimestamp(value) : null)
|
|
19
|
+
?? fallback;
|
|
20
|
+
}
|
|
21
|
+
function hash(value) {
|
|
22
|
+
let output = 2166136261 >>> 0;
|
|
23
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
24
|
+
output ^= value.charCodeAt(index);
|
|
25
|
+
output = Math.imul(output, 16777619);
|
|
26
|
+
}
|
|
27
|
+
return (output >>> 0).toString(36);
|
|
28
|
+
}
|
|
29
|
+
function safeId(value, raw) {
|
|
30
|
+
return typeof value === 'string' && SAFE_ID_RE.test(value) ? value : `branch-${hash(JSON.stringify(raw))}`;
|
|
31
|
+
}
|
|
32
|
+
function freshId() {
|
|
33
|
+
return typeof globalThis.crypto?.randomUUID === 'function'
|
|
34
|
+
? globalThis.crypto.randomUUID()
|
|
35
|
+
: `branch-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
36
|
+
}
|
|
37
|
+
function ids(value, valid) {
|
|
38
|
+
if (!Array.isArray(value))
|
|
39
|
+
return [];
|
|
40
|
+
const output = new Set();
|
|
41
|
+
for (const item of value.slice(0, MAX_CASE_BRANCH_REFERENCES * 2)) {
|
|
42
|
+
if (typeof item === 'string' && SAFE_ID_RE.test(item) && valid.has(item))
|
|
43
|
+
output.add(item);
|
|
44
|
+
if (output.size >= MAX_CASE_BRANCH_REFERENCES)
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
return [...output];
|
|
48
|
+
}
|
|
49
|
+
function normalizeBranch(value, fallback, references, options = {}) {
|
|
50
|
+
const item = record(value);
|
|
51
|
+
const name = text(item.name, MAX_CASE_BRANCH_NAME_LENGTH);
|
|
52
|
+
if (!name)
|
|
53
|
+
return null;
|
|
54
|
+
const createdAt = iso(item.createdAt, fallback, options);
|
|
55
|
+
const branch = Object.freeze({
|
|
56
|
+
id: safeId(item.id, { name, createdAt }),
|
|
57
|
+
name,
|
|
58
|
+
state: item.state === 'resolved' ? 'resolved' : 'active',
|
|
59
|
+
evidencePinIds: Object.freeze(ids(item.evidencePinIds, references.evidencePinIds)),
|
|
60
|
+
checkpointIds: Object.freeze(ids(item.checkpointIds, references.checkpointIds)),
|
|
61
|
+
assertionIds: Object.freeze(ids(item.assertionIds, references.assertionIds)),
|
|
62
|
+
actionIds: Object.freeze(ids(item.actionIds, references.actionIds)),
|
|
63
|
+
createdAt,
|
|
64
|
+
updatedAt: iso(item.updatedAt, createdAt, options),
|
|
65
|
+
});
|
|
66
|
+
return branch.evidencePinIds.length || branch.checkpointIds.length || branch.assertionIds.length || branch.actionIds.length
|
|
67
|
+
? branch
|
|
68
|
+
: null;
|
|
69
|
+
}
|
|
70
|
+
export function caseInvestigationBranchReferences(input) {
|
|
71
|
+
return Object.freeze({
|
|
72
|
+
evidencePinIds: new Set(input.evidencePins.map((item) => item.id)),
|
|
73
|
+
checkpointIds: new Set(input.evidencePins.flatMap((item) => item.checkpointId ? [item.checkpointId] : [])),
|
|
74
|
+
assertionIds: new Set(input.assertions.map((item) => item.id)),
|
|
75
|
+
actionIds: new Set(input.actions.map((item) => item.id)),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
export function normalizeCaseInvestigationBranches(value, fallback, references, options = {}) {
|
|
79
|
+
if (!Array.isArray(value))
|
|
80
|
+
return [];
|
|
81
|
+
const byId = new Map();
|
|
82
|
+
for (const item of value.slice(0, MAX_CASE_INVESTIGATION_BRANCHES * 2)) {
|
|
83
|
+
const normalized = normalizeBranch(item, fallback, references, options);
|
|
84
|
+
if (!normalized)
|
|
85
|
+
continue;
|
|
86
|
+
const current = byId.get(normalized.id);
|
|
87
|
+
if (!current || normalized.updatedAt > current.updatedAt)
|
|
88
|
+
byId.set(normalized.id, normalized);
|
|
89
|
+
}
|
|
90
|
+
return [...byId.values()]
|
|
91
|
+
.sort((left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id))
|
|
92
|
+
.slice(-MAX_CASE_INVESTIGATION_BRANCHES);
|
|
93
|
+
}
|
|
94
|
+
export function appendCaseInvestigationBranch(current, value, now, references) {
|
|
95
|
+
if (current.length >= MAX_CASE_INVESTIGATION_BRANCHES) {
|
|
96
|
+
throw new Error(`Each case is limited to ${MAX_CASE_INVESTIGATION_BRANCHES} investigation branches.`);
|
|
97
|
+
}
|
|
98
|
+
const item = record(value);
|
|
99
|
+
const branch = normalizeBranch({ ...item, id: freshId(), createdAt: now, updatedAt: now }, now, references);
|
|
100
|
+
if (!branch)
|
|
101
|
+
throw new Error('An investigation branch requires a name and at least one valid case reference.');
|
|
102
|
+
return normalizeCaseInvestigationBranches([...current, branch], now, references);
|
|
103
|
+
}
|
|
104
|
+
export function updateCaseInvestigationBranch(current, value, now, references) {
|
|
105
|
+
const patch = record(value);
|
|
106
|
+
const id = typeof patch.id === 'string' && SAFE_ID_RE.test(patch.id) ? patch.id : '';
|
|
107
|
+
const index = current.findIndex((item) => item.id === id);
|
|
108
|
+
if (index < 0)
|
|
109
|
+
throw new Error('That investigation branch no longer exists.');
|
|
110
|
+
const existing = current[index];
|
|
111
|
+
if (!existing)
|
|
112
|
+
throw new Error('That investigation branch no longer exists.');
|
|
113
|
+
const normalized = normalizeBranch({
|
|
114
|
+
...existing,
|
|
115
|
+
...(Object.hasOwn(patch, 'name') ? { name: patch.name } : {}),
|
|
116
|
+
...(Object.hasOwn(patch, 'state') ? { state: patch.state } : {}),
|
|
117
|
+
...(Object.hasOwn(patch, 'evidencePinIds') ? { evidencePinIds: patch.evidencePinIds } : {}),
|
|
118
|
+
...(Object.hasOwn(patch, 'checkpointIds') ? { checkpointIds: patch.checkpointIds } : {}),
|
|
119
|
+
...(Object.hasOwn(patch, 'assertionIds') ? { assertionIds: patch.assertionIds } : {}),
|
|
120
|
+
...(Object.hasOwn(patch, 'actionIds') ? { actionIds: patch.actionIds } : {}),
|
|
121
|
+
id,
|
|
122
|
+
updatedAt: now,
|
|
123
|
+
}, now, references);
|
|
124
|
+
if (!normalized)
|
|
125
|
+
throw new Error('An investigation branch requires a name and at least one valid case reference.');
|
|
126
|
+
const next = [...current];
|
|
127
|
+
next[index] = normalized;
|
|
128
|
+
return normalizeCaseInvestigationBranches(next, now, references);
|
|
129
|
+
}
|
|
130
|
+
export function mergeCaseInvestigationBranches(local, imported, fallback, references) {
|
|
131
|
+
return normalizeCaseInvestigationBranches([...local, ...imported], fallback, references);
|
|
132
|
+
}
|