@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,80 @@
|
|
|
1
|
+
// Bounded deterministic JSON projection shared by browser and CLI comparison paths.
|
|
2
|
+
const MAX_COLLECTION_SCAN = 256;
|
|
3
|
+
const MAX_DEPTH = 8;
|
|
4
|
+
const MAX_SCALAR_LENGTH = 2_000;
|
|
5
|
+
const MAX_TRAVERSAL_NODES = 1_024;
|
|
6
|
+
const MAX_SERIALISED_LENGTH = 8_192;
|
|
7
|
+
function record(value) {
|
|
8
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
9
|
+
? value
|
|
10
|
+
: null;
|
|
11
|
+
}
|
|
12
|
+
function boundedJsonString(value, state) {
|
|
13
|
+
if (value.length > MAX_SCALAR_LENGTH)
|
|
14
|
+
state.truncated = true;
|
|
15
|
+
return JSON.stringify(value.slice(0, MAX_SCALAR_LENGTH));
|
|
16
|
+
}
|
|
17
|
+
function stableJson(value, depth, state) {
|
|
18
|
+
if (depth > MAX_DEPTH) {
|
|
19
|
+
state.truncated = true;
|
|
20
|
+
return '"[bounded depth]"';
|
|
21
|
+
}
|
|
22
|
+
if (state.nodes >= MAX_TRAVERSAL_NODES) {
|
|
23
|
+
state.truncated = true;
|
|
24
|
+
return '"[bounded traversal]"';
|
|
25
|
+
}
|
|
26
|
+
state.nodes += 1;
|
|
27
|
+
if (value === null)
|
|
28
|
+
return 'null';
|
|
29
|
+
if (typeof value === 'string')
|
|
30
|
+
return boundedJsonString(value, state);
|
|
31
|
+
if (typeof value === 'number')
|
|
32
|
+
return Number.isFinite(value) ? String(value) : 'null';
|
|
33
|
+
if (typeof value === 'boolean')
|
|
34
|
+
return value ? 'true' : 'false';
|
|
35
|
+
if (Array.isArray(value)) {
|
|
36
|
+
const values = [];
|
|
37
|
+
const limit = Math.min(value.length, MAX_COLLECTION_SCAN);
|
|
38
|
+
for (let index = 0; index < limit && state.nodes < MAX_TRAVERSAL_NODES; index += 1) {
|
|
39
|
+
values.push(stableJson(value[index], depth + 1, state));
|
|
40
|
+
}
|
|
41
|
+
if (values.length < value.length)
|
|
42
|
+
state.truncated = true;
|
|
43
|
+
return `[${values.join(',')}]`;
|
|
44
|
+
}
|
|
45
|
+
const item = record(value);
|
|
46
|
+
if (!item)
|
|
47
|
+
return 'null';
|
|
48
|
+
const keys = [];
|
|
49
|
+
for (const key in item) {
|
|
50
|
+
if (!Object.hasOwn(item, key))
|
|
51
|
+
continue;
|
|
52
|
+
if (keys.length >= MAX_COLLECTION_SCAN) {
|
|
53
|
+
state.truncated = true;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
keys.push(key);
|
|
57
|
+
}
|
|
58
|
+
keys.sort();
|
|
59
|
+
const values = [];
|
|
60
|
+
for (const key of keys) {
|
|
61
|
+
if (state.nodes >= MAX_TRAVERSAL_NODES) {
|
|
62
|
+
state.truncated = true;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
values.push(`${boundedJsonString(key, state)}:${stableJson(item[key], depth + 1, state)}`);
|
|
66
|
+
}
|
|
67
|
+
if (values.length < keys.length)
|
|
68
|
+
state.truncated = true;
|
|
69
|
+
return `{${values.join(',')}}`;
|
|
70
|
+
}
|
|
71
|
+
export function projectComparisonLedgerJson(value) {
|
|
72
|
+
const state = { truncated: false, nodes: 0 };
|
|
73
|
+
const serialised = stableJson(value, 0, state);
|
|
74
|
+
if (serialised.length > MAX_SERIALISED_LENGTH)
|
|
75
|
+
state.truncated = true;
|
|
76
|
+
return Object.freeze({ value: serialised.slice(0, MAX_SERIALISED_LENGTH), truncated: state.truncated });
|
|
77
|
+
}
|
|
78
|
+
export function stableComparisonLedgerJson(value) {
|
|
79
|
+
return projectComparisonLedgerJson(value).value;
|
|
80
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Stable comparison-ledger identifiers and safe local destinations are runtime-neutral.
|
|
2
|
+
const CONTROL_RE = /[\u0000-\u001f\u007f]/gu;
|
|
3
|
+
const MAX_ID_PARTS = 32;
|
|
4
|
+
const MAX_ID_PART_LENGTH = 500;
|
|
5
|
+
function stableHash(value) {
|
|
6
|
+
let first = 0x811c9dc5;
|
|
7
|
+
let second = 0;
|
|
8
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
9
|
+
const code = value.charCodeAt(index);
|
|
10
|
+
first ^= code;
|
|
11
|
+
first = Math.imul(first, 0x01000193);
|
|
12
|
+
second = (second + code) | 0;
|
|
13
|
+
second = (second + (second << 10)) | 0;
|
|
14
|
+
second ^= second >>> 6;
|
|
15
|
+
}
|
|
16
|
+
second = (second + (second << 3)) | 0;
|
|
17
|
+
second ^= second >>> 11;
|
|
18
|
+
second = (second + (second << 15)) | 0;
|
|
19
|
+
return `${(first >>> 0).toString(36).padStart(7, '0')}${(second >>> 0).toString(36).padStart(7, '0')}`;
|
|
20
|
+
}
|
|
21
|
+
export function stableComparisonLedgerId(prefix, parts) {
|
|
22
|
+
const encoded = parts.slice(0, MAX_ID_PARTS).map((part) => {
|
|
23
|
+
let value = '';
|
|
24
|
+
try {
|
|
25
|
+
value = part === null || part === undefined || ['string', 'number', 'boolean', 'bigint'].includes(typeof part)
|
|
26
|
+
? String(part ?? '')
|
|
27
|
+
: Object.prototype.toString.call(part);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
value = '[unreadable]';
|
|
31
|
+
}
|
|
32
|
+
const bounded = value.slice(0, MAX_ID_PART_LENGTH);
|
|
33
|
+
const typed = `${typeof part}:${value.length}:${bounded}`;
|
|
34
|
+
return `${typed.length}:${typed}`;
|
|
35
|
+
}).join('');
|
|
36
|
+
const material = `${parts.length}:${encoded}`;
|
|
37
|
+
return `${prefix.slice(0, 64)}-${stableHash(material)}`;
|
|
38
|
+
}
|
|
39
|
+
export function safeComparisonLedgerHref(value) {
|
|
40
|
+
if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean')
|
|
41
|
+
return '';
|
|
42
|
+
const href = String(value).replace(CONTROL_RE, ' ').replace(/\s+/gu, ' ').trim().slice(0, 500).trim();
|
|
43
|
+
if (!href.startsWith('/') || href.startsWith('//') || href.includes('\\'))
|
|
44
|
+
return '';
|
|
45
|
+
try {
|
|
46
|
+
const base = new URL('https://comparison-ledger.invalid/');
|
|
47
|
+
return new URL(href, base).origin === base.origin ? href : '';
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return '';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { hammingDistanceHex, isInformativePerceptualHash, isPerceptualHash, } from "../../lib/perceptual-hash-comparison.mjs";
|
|
2
|
+
function plainRecord(value) {
|
|
3
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
4
|
+
? value
|
|
5
|
+
: null;
|
|
6
|
+
}
|
|
7
|
+
function faviconRecord(value) {
|
|
8
|
+
const record = plainRecord(value);
|
|
9
|
+
if (!record || typeof record.domain !== 'string')
|
|
10
|
+
return null;
|
|
11
|
+
const faviconHash = typeof record.faviconHash === 'string' && record.faviconHash
|
|
12
|
+
? record.faviconHash
|
|
13
|
+
: null;
|
|
14
|
+
const faviconPHash = isPerceptualHash(record.faviconPHash)
|
|
15
|
+
? record.faviconPHash
|
|
16
|
+
: null;
|
|
17
|
+
return faviconHash || faviconPHash
|
|
18
|
+
? { domain: record.domain, faviconHash, faviconPHash }
|
|
19
|
+
: null;
|
|
20
|
+
}
|
|
21
|
+
export function groupBySimilarFavicon(records, maxDistance) {
|
|
22
|
+
const items = Array.isArray(records)
|
|
23
|
+
? records.map(faviconRecord).filter((record) => record !== null)
|
|
24
|
+
: [];
|
|
25
|
+
const parent = items.map((_, index) => index);
|
|
26
|
+
const find = (value) => {
|
|
27
|
+
let root = value;
|
|
28
|
+
while ((parent[root] ?? root) !== root)
|
|
29
|
+
root = parent[root] ?? root;
|
|
30
|
+
while ((parent[value] ?? value) !== root) {
|
|
31
|
+
const next = parent[value] ?? value;
|
|
32
|
+
parent[value] = root;
|
|
33
|
+
value = next;
|
|
34
|
+
}
|
|
35
|
+
return root;
|
|
36
|
+
};
|
|
37
|
+
const union = (left, right) => {
|
|
38
|
+
const leftRoot = find(left);
|
|
39
|
+
const rightRoot = find(right);
|
|
40
|
+
if (leftRoot !== rightRoot)
|
|
41
|
+
parent[leftRoot] = rightRoot;
|
|
42
|
+
};
|
|
43
|
+
const firstByHash = new Map();
|
|
44
|
+
items.forEach((item, index) => {
|
|
45
|
+
if (!item.faviconHash)
|
|
46
|
+
return;
|
|
47
|
+
const existing = firstByHash.get(item.faviconHash);
|
|
48
|
+
if (existing !== undefined)
|
|
49
|
+
union(index, existing);
|
|
50
|
+
else
|
|
51
|
+
firstByHash.set(item.faviconHash, index);
|
|
52
|
+
});
|
|
53
|
+
const withPerceptualHash = [];
|
|
54
|
+
items.forEach((item, index) => {
|
|
55
|
+
if (isInformativePerceptualHash(item.faviconPHash)) {
|
|
56
|
+
withPerceptualHash.push({ index, hash: item.faviconPHash });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
for (let leftIndex = 0; leftIndex < withPerceptualHash.length; leftIndex += 1) {
|
|
60
|
+
for (let rightIndex = leftIndex + 1; rightIndex < withPerceptualHash.length; rightIndex += 1) {
|
|
61
|
+
const left = withPerceptualHash[leftIndex];
|
|
62
|
+
const right = withPerceptualHash[rightIndex];
|
|
63
|
+
if (!left || !right)
|
|
64
|
+
continue;
|
|
65
|
+
const distance = hammingDistanceHex(left.hash, right.hash);
|
|
66
|
+
if (distance !== null && distance <= maxDistance)
|
|
67
|
+
union(left.index, right.index);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const groups = new Map();
|
|
71
|
+
items.forEach((item, index) => {
|
|
72
|
+
const root = find(index);
|
|
73
|
+
if (!groups.has(root))
|
|
74
|
+
groups.set(root, []);
|
|
75
|
+
groups.get(root)?.push(item.domain);
|
|
76
|
+
});
|
|
77
|
+
return [...groups.values()].filter((domains) => domains.length >= 2);
|
|
78
|
+
}
|
package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs}
RENAMED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
// module deliberately reports each component independently: it does not
|
|
3
3
|
// produce an aggregate similarity score, risk factor, or maliciousness
|
|
4
4
|
// verdict. Inputs pass through the strict baseline normalizer before use.
|
|
5
|
-
import { normalizePageBaseline } from "
|
|
6
|
-
import { hammingDistanceHex } from "
|
|
7
|
-
|
|
5
|
+
import { normalizePageBaseline } from "../workspace/page-baseline.mjs";
|
|
6
|
+
import { hammingDistanceHex } from "../../lib/perceptual-hash-comparison.mjs";
|
|
7
|
+
import { PAGE_COMPARISON_VERSION } from "../contracts/offline-comparison.mjs";
|
|
8
|
+
export { PAGE_COMPARISON_VERSION };
|
|
8
9
|
function component(id, label, method, status, outcome, detail, partial) {
|
|
9
10
|
return { id, label, method, status, outcome, detail, partial, sharedValues: [] };
|
|
10
11
|
}
|
|
@@ -3,11 +3,10 @@
|
|
|
3
3
|
// proof of common ownership, coordination, intent, or maliciousness. This
|
|
4
4
|
// module deliberately produces no aggregate score and is never a persistence
|
|
5
5
|
// boundary.
|
|
6
|
-
import { normalizeDomain } from "
|
|
7
|
-
import { groupBySimilarFavicon } from "./
|
|
8
|
-
|
|
9
|
-
export
|
|
10
|
-
export const SUPPORTED_TLS_RELATIONSHIP_PROFILE_VERSIONS = Object.freeze([1, TLS_RELATIONSHIP_PROFILE_VERSION]);
|
|
6
|
+
import { normalizeDomain } from "../cases/case-model.mjs";
|
|
7
|
+
import { groupBySimilarFavicon } from "./favicon-similarity.mjs";
|
|
8
|
+
import { RELATIONSHIP_EVIDENCE_SCHEMA, RELATIONSHIP_EVIDENCE_VERSION, SUPPORTED_TLS_RELATIONSHIP_PROFILE_VERSIONS, TLS_RELATIONSHIP_PROFILE_VERSION, } from "../contracts/offline-comparison.mjs";
|
|
9
|
+
export { RELATIONSHIP_EVIDENCE_SCHEMA, RELATIONSHIP_EVIDENCE_VERSION, SUPPORTED_TLS_RELATIONSHIP_PROFILE_VERSIONS, TLS_RELATIONSHIP_PROFILE_VERSION, };
|
|
11
10
|
export const MAX_RELATIONSHIP_ROWS = 2000;
|
|
12
11
|
export const MAX_RELATIONSHIP_GROUPS = 100;
|
|
13
12
|
export const MAX_RELATIONSHIP_DOMAINS = 50;
|
|
@@ -93,7 +92,7 @@ function tlsCertificateFingerprint(availability) {
|
|
|
93
92
|
const certificate = record(tls?.certificate);
|
|
94
93
|
const fingerprint = certificate?.fingerprintSha256;
|
|
95
94
|
const status = tls?.status;
|
|
96
|
-
if (tls?.source !== 'tls' || !SUPPORTED_TLS_RELATIONSHIP_PROFILE_VERSIONS.
|
|
95
|
+
if (tls?.source !== 'tls' || !SUPPORTED_TLS_RELATIONSHIP_PROFILE_VERSIONS.some((version) => version === Number(tls?.profileVersion))
|
|
97
96
|
|| typeof status !== 'string' || !['success', 'partial'].includes(status) || typeof fingerprint !== 'string'
|
|
98
97
|
|| fingerprint.length !== 64 || !CERTIFICATE_SHA_RE.test(fingerprint))
|
|
99
98
|
return null;
|
|
@@ -240,7 +239,7 @@ export function buildScanRelationships(rawRows) {
|
|
|
240
239
|
}
|
|
241
240
|
}
|
|
242
241
|
for (const [value, domains] of officialAssets)
|
|
243
|
-
output.push(group('official_asset', 'Official asset
|
|
242
|
+
output.push(group('official_asset', 'Official asset host match', 'Configured-domain host match', value, [...domains].sort(), 'One or more pages loaded an asset from this configured official domain or its subdomain.'));
|
|
244
243
|
const order = new Map(['nameserver_set', 'ip_address', 'certificate', 'tracking_identifier', 'favicon', 'official_asset']
|
|
245
244
|
.map((value, index) => [value, index]));
|
|
246
245
|
output.sort((left, right) => (Number(order.get(left.type)) - Number(order.get(right.type))) || left.value.localeCompare(right.value) || left.domains.join('|').localeCompare(right.domains.join('|')));
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded analyst Review Item schema metadata and shared value types.
|
|
3
|
+
*
|
|
4
|
+
* Lifecycle behaviour is owned by packages/monitoring/analyst-review-state.mts.
|
|
5
|
+
* This dependency-free contract module lets storage and lifecycle registries
|
|
6
|
+
* depend only on declared shapes, versions, and limits.
|
|
7
|
+
*/
|
|
8
|
+
export const ANALYST_REVIEW_STATE_SCHEMA = 'whoisleuth.analyst-review-state';
|
|
9
|
+
export const ANALYST_REVIEW_STATE_SCHEMA_VERSION = 1;
|
|
10
|
+
export const ANALYST_REVIEW_STATE_SUPPORTED_VERSIONS = Object.freeze([ANALYST_REVIEW_STATE_SCHEMA_VERSION]);
|
|
11
|
+
/** Internal IndexedDB revision; it is deliberately independent of the public document version. */
|
|
12
|
+
export const ANALYST_REVIEW_STATE_BROWSER_STORAGE_REVISION = 2;
|
|
13
|
+
export const MAX_ANALYST_REVIEW_STATE_RECORDS = 500;
|
|
14
|
+
export const MAX_ANALYST_REVIEW_STATE_BYTES = 512 * 1024;
|
|
15
|
+
export const MAX_ANALYST_REVIEW_RATIONALE_LENGTH = 1_000;
|
|
16
|
+
export const MAX_ANALYST_REVIEW_ASSOCIATIONS = 20;
|
|
17
|
+
export const MAX_ANALYST_REVIEW_HISTORY = 8;
|
|
18
|
+
export const MAX_ANALYST_REVIEW_ITEMS = 500;
|
|
19
|
+
export const MAX_ANALYST_REVIEW_IDENTITY_BYTES = 1024 * 1024;
|
|
20
|
+
export const ANALYST_REVIEW_KINDS = Object.freeze([
|
|
21
|
+
'case',
|
|
22
|
+
'case_action',
|
|
23
|
+
'observed_effect_review',
|
|
24
|
+
'evidence_gap',
|
|
25
|
+
'watchlist_change',
|
|
26
|
+
'bulk_session',
|
|
27
|
+
'comparison',
|
|
28
|
+
'suppression',
|
|
29
|
+
'change_window',
|
|
30
|
+
'desired_posture',
|
|
31
|
+
'certificate',
|
|
32
|
+
'incomplete_packet',
|
|
33
|
+
'detection_rule',
|
|
34
|
+
'orphaned_state',
|
|
35
|
+
]);
|
|
36
|
+
export const ANALYST_REVIEW_EVIDENCE_FAMILIES = Object.freeze([
|
|
37
|
+
'case',
|
|
38
|
+
'registration',
|
|
39
|
+
'dns',
|
|
40
|
+
'routing',
|
|
41
|
+
'http',
|
|
42
|
+
'certificate_transparency',
|
|
43
|
+
'live_tls',
|
|
44
|
+
'caa',
|
|
45
|
+
'certificate_identity',
|
|
46
|
+
'comparison',
|
|
47
|
+
'desired_posture',
|
|
48
|
+
'suppression',
|
|
49
|
+
'change_window',
|
|
50
|
+
'relationship',
|
|
51
|
+
'bulk',
|
|
52
|
+
'rule',
|
|
53
|
+
'packet',
|
|
54
|
+
]);
|
|
55
|
+
export const ANALYST_REVIEW_DISPOSITIONS = Object.freeze(['open', 'expected', 'suppressed', 'resolved']);
|
|
56
|
+
export const ANALYST_REVIEW_DISPOSITION_OPTIONS = Object.freeze([
|
|
57
|
+
{ value: 'open', label: 'Open' },
|
|
58
|
+
{ value: 'expected', label: 'Expected' },
|
|
59
|
+
{ value: 'suppressed', label: 'Suppressed' },
|
|
60
|
+
{ value: 'resolved', label: 'Resolved' },
|
|
61
|
+
]);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { defineSchemaCompatibility } from "./schema-compatibility.mjs";
|
|
2
|
+
import { buildExtractedLifecycleFamilyV2 } from "./extracted-domain-lifecycle.mjs";
|
|
3
|
+
import { defineSchemaLifecycleFamily } from "./schema-lifecycle.mjs";
|
|
4
|
+
import { ANALYST_REVIEW_STATE_SCHEMA, ANALYST_REVIEW_STATE_SCHEMA_VERSION, ANALYST_REVIEW_STATE_SUPPORTED_VERSIONS, MAX_ANALYST_REVIEW_STATE_BYTES, } from "./analyst-review-state-contract.mjs";
|
|
5
|
+
export * from "./analyst-review-state-contract.mjs";
|
|
6
|
+
export const ANALYST_REVIEW_STATE_CONTRACT_OWNER = 'packages/contracts/analyst-review-state.mts';
|
|
7
|
+
export const ANALYST_REVIEW_STATE_COMPATIBILITY = defineSchemaCompatibility({
|
|
8
|
+
id: 'browser.analyst-review-state',
|
|
9
|
+
kind: 'browser_store',
|
|
10
|
+
schema: ANALYST_REVIEW_STATE_SCHEMA,
|
|
11
|
+
currentVersion: ANALYST_REVIEW_STATE_SCHEMA_VERSION,
|
|
12
|
+
supportedVersions: ANALYST_REVIEW_STATE_SUPPORTED_VERSIONS,
|
|
13
|
+
acceptsUnversionedLegacy: false,
|
|
14
|
+
futureVersionBehavior: 'preserve_without_write',
|
|
15
|
+
migration: 'exact_current_only',
|
|
16
|
+
writeSemantics: 'optimistic_replace',
|
|
17
|
+
byteBudget: MAX_ANALYST_REVIEW_STATE_BYTES,
|
|
18
|
+
owner: ANALYST_REVIEW_STATE_CONTRACT_OWNER,
|
|
19
|
+
note: 'Bounded analyst-authored lifecycle overlay. Material evidence changes, expiry, and incomplete evidence reopen review without rewriting source evidence or losing earlier rationale.',
|
|
20
|
+
});
|
|
21
|
+
export const ANALYST_REVIEW_STATE_LIFECYCLE_FAMILY = defineSchemaLifecycleFamily(buildExtractedLifecycleFamilyV2({
|
|
22
|
+
id: 'analyst-review-state',
|
|
23
|
+
owner: ANALYST_REVIEW_STATE_CONTRACT_OWNER,
|
|
24
|
+
serializerModule: 'packages/monitoring/analyst-review-state.mts',
|
|
25
|
+
serializerExportName: 'serialiseAnalystReviewStateJson',
|
|
26
|
+
plane: 'browser',
|
|
27
|
+
projection: 'browser_export',
|
|
28
|
+
retention: 'operator_controlled_output',
|
|
29
|
+
includedCategories: ['subject-identity', 'material-fingerprint', 'analyst-disposition', 'rationale', 'review-times', 'case-and-campaign-references'],
|
|
30
|
+
excludedCategories: ['raw-upstream-responses', 'expanded-contacts', 'complete-query-urls', 'credentials', 'cookies', 'certificate-transparency-history'],
|
|
31
|
+
formats: [{
|
|
32
|
+
descriptor: ANALYST_REVIEW_STATE_COMPATIBILITY,
|
|
33
|
+
lifecycleSchema: ANALYST_REVIEW_STATE_SCHEMA,
|
|
34
|
+
requiredKeys: ['schema', 'version', 'records'],
|
|
35
|
+
optionalKeys: [],
|
|
36
|
+
hook: {
|
|
37
|
+
module: 'packages/monitoring/analyst-review-state.mts',
|
|
38
|
+
exportName: 'normalizeAnalystReviewStateStore',
|
|
39
|
+
role: 'normaliser',
|
|
40
|
+
runtime: 'shared',
|
|
41
|
+
},
|
|
42
|
+
fixtures: [{
|
|
43
|
+
id: 'analyst-review-state-v1',
|
|
44
|
+
path: 'test/fixtures/extracted-domain-lifecycle/analyst-review-state-v1.json',
|
|
45
|
+
bytes: 663,
|
|
46
|
+
sha256: 'af0b191a79a50ef28f82975d181bdd3f7ba0bf7245cf4fc2b3adb84603d8dae6',
|
|
47
|
+
version: ANALYST_REVIEW_STATE_SCHEMA_VERSION,
|
|
48
|
+
}],
|
|
49
|
+
}],
|
|
50
|
+
}));
|