@slicedearth/whoisleuth-cli 2.4.0 → 2.6.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 +1 -1
- package/NOTICE +2 -2
- package/cli/argument-values.mjs +55 -0
- package/cli/arguments.mjs +24 -470
- package/cli/artifact-validation/case-response.mjs +67 -9
- package/cli/artifact-validation/lookup-evidence.mjs +18 -4
- package/cli/artifact-validation/signed-review.mjs +61 -64
- package/cli/artifact-verify.mjs +11 -2
- package/cli/assurance-arguments.mjs +56 -0
- package/cli/assurance-command-definitions.mjs +179 -0
- package/cli/assurance-command-runner.mjs +1 -1
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +1 -1
- package/cli/bulk-command-runner.mjs +6 -8
- package/cli/bulk.mjs +2 -3
- package/cli/case-command.mjs +99 -6
- package/cli/case-pack.mjs +19 -4
- package/cli/ci-report.mjs +6 -3
- package/cli/collection-arguments.mjs +123 -0
- package/cli/collection-command-definitions.mjs +330 -0
- package/cli/collection-preflight.mjs +8 -1
- package/cli/command-argument-grammar.mjs +8 -210
- package/cli/command-definition.mjs +330 -0
- package/cli/command-reference.mjs +55 -1388
- package/cli/command-seeds.mjs +18 -0
- package/cli/compare.mjs +1 -1
- package/cli/config-profile.mjs +1 -1
- package/cli/ct-event-intake.mjs +4 -4
- package/cli/discover.mjs +2 -2
- package/cli/discovery-observation-snapshot.mjs +2 -2
- package/cli/discovery-scan-command-runner.mjs +7 -9
- package/cli/discovery-scan.mjs +1 -1
- package/cli/discovery-snapshot.mjs +2 -2
- package/cli/discovery-workflow.mjs +4 -4
- package/cli/domain-control-monitor.mjs +6 -6
- package/cli/domain-control-observations.mjs +2 -1
- package/cli/domain-feed.mjs +61 -0
- package/cli/errors.mjs +5 -6
- package/cli/evidence-arguments.mjs +46 -0
- package/cli/evidence-command-definitions.mjs +69 -0
- package/cli/evidence-command-runner.mjs +2 -3
- package/cli/evidence-signing.mjs +3 -2
- package/cli/external-observation-mapping.mjs +2 -2
- package/cli/formatters/json.mjs +1 -13
- package/cli/formatters/terminal-command-formats.mjs +6 -0
- package/cli/formatters/terminal-lookup.mjs +6 -0
- package/cli/history-arguments.mjs +39 -0
- package/cli/history-command-definitions.mjs +88 -0
- package/cli/indicator-set-command.mjs +32 -0
- package/cli/inline-command-families.mjs +6 -47
- package/cli/inline-command-runner.mjs +4 -31
- package/cli/input.mjs +2 -2
- package/cli/intake-command.mjs +147 -0
- package/cli/interchange-report.mjs +4 -2
- package/cli/investigation-artifacts.mjs +3 -4
- package/cli/investigation-package-review.mjs +2 -0
- package/cli/investigation-plan.mjs +32 -154
- package/cli/investigation-recipes.mjs +177 -0
- package/cli/investigation-run.mjs +78 -12
- package/cli/local-mmdb-review.mjs +220 -17
- package/cli/lookalike-calibration.mjs +1 -1
- package/cli/lookup-brief.mjs +2 -2
- package/cli/lookup-command-runner.mjs +19 -35
- package/cli/lookup-diff.mjs +10 -9
- package/cli/lookup-plan.mjs +10 -14
- package/cli/lookup-reconcile.mjs +6 -5
- package/cli/lookup-timeline.mjs +7 -6
- package/cli/mail-header-review.mjs +24 -247
- package/cli/mail-review.mjs +9 -7
- package/cli/network-arguments.mjs +51 -0
- package/cli/network-command-definitions.mjs +130 -0
- package/cli/network-command-runner.mjs +43 -31
- package/cli/offline-evidence-review.mjs +85 -6
- package/cli/posture.mjs +1 -1
- package/cli/registry-fixture-scaffold.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +8 -7
- package/cli/review-arguments.mjs +132 -0
- package/cli/review-command-definitions.mjs +279 -0
- package/cli/review-command-runner.mjs +72 -159
- package/cli/risk-calibration.mjs +14 -12
- package/cli/runner.mjs +20 -32
- package/cli/saved-lookup.mjs +2 -2
- package/cli/selected-input-worker.mjs +73 -0
- package/cli/sharing-review.mjs +5 -64
- package/cli/source-reliability.mjs +2 -2
- package/cli/support-arguments.mjs +49 -0
- package/cli/support-command-definitions.mjs +209 -0
- package/cli/support-command-runner.mjs +4 -4
- package/cli/watchlist-review.mjs +43 -0
- package/cli/workflow-arguments.mjs +100 -0
- package/cli/workflow-command-definitions.mjs +103 -0
- package/cli/workflow-command-runner.mjs +40 -1
- package/docs/cli-reference.md +27 -0
- package/docs/cli.md +322 -4
- package/docs/contextual-reviews.md +178 -0
- package/docs/infrastructure-observations.md +126 -0
- package/docs/offline-intake-context.md +213 -0
- package/lib/analyst-taxonomy.mjs +2 -40
- package/lib/application-version.mjs +1 -1
- package/lib/availability.mjs +13 -789
- package/lib/bounded-contract-normalizers.mjs +4 -0
- package/lib/bounded-json.mjs +2 -330
- package/lib/browser-library-profile.mjs +62 -84
- package/lib/candidate-provenance-bounds.mjs +2 -0
- package/lib/classify.mjs +1 -1
- package/lib/credential-form-attribution.mjs +136 -0
- package/lib/credential-surface-profile.mjs +6 -2
- package/lib/ct-query.mjs +2 -27
- package/lib/ct-response-bounds.mjs +2 -9
- package/lib/ct-search.mjs +83 -47
- package/lib/dmarc-discovery.mjs +62 -0
- package/lib/dns-convergence-review.mjs +21 -8
- package/lib/dns-delegation-health.mjs +35 -15
- package/lib/dns-inheritance-review.mjs +53 -63
- package/lib/dns-intelligence.mjs +27 -11
- package/lib/dns-query-coverage.mjs +40 -0
- package/lib/domain-assurance.mjs +1 -1
- package/lib/domain-change-packet.mjs +12 -13
- package/lib/domain-change-review.mjs +32 -14
- package/lib/domain-control-flight-recorder.mjs +103 -27
- package/lib/domain-control-manifest.mjs +25 -17
- package/lib/domain-posture-analysis.mjs +75 -7
- package/lib/domain-posture-parsers.mjs +9 -0
- package/lib/domain-posture.mjs +45 -25
- package/lib/domain-registration.mjs +392 -0
- package/lib/evidence-export-privacy.mjs +27 -17
- package/lib/evidence-export.mjs +3 -3
- package/lib/evidence-report.mjs +5 -1
- package/lib/external-intelligence-risk.mjs +2 -124
- package/lib/favicon.mjs +13 -4
- package/lib/generated/cisa-kev-catalog.mjs +41 -3
- package/lib/generated/registrar-standing-catalogue.mjs +18 -10
- package/lib/generated/retire-browser-catalog.mjs +332 -19
- package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
- package/lib/html-signals.mjs +13 -2
- package/lib/http-evidence-bounds.mjs +2 -8
- package/lib/http-intelligence.mjs +13 -1
- package/lib/idn-confusable-policy.mjs +6 -4
- package/lib/idn-confusables.mjs +1 -1
- package/lib/interchange-fidelity-registry.mjs +10 -0
- package/lib/lookup-child-profile-contract.mjs +28 -11
- package/lib/lookup-contract-primitives.mjs +1 -1
- package/lib/lookup-diagnostics.mjs +1 -23
- package/lib/lookup-network-evidence-bounds.mjs +1 -10
- package/lib/lookup-progress.mjs +50 -1
- package/lib/lookup-response-contract.mjs +70 -40
- package/lib/lookup-response.mjs +5 -15
- package/lib/lookup-source-progress.mjs +8 -22
- package/lib/lookup.mjs +1 -1
- package/lib/operation-budget.mjs +4 -0
- package/lib/opportunity-scoring.mjs +2 -107
- package/lib/outbound-identity.mjs +1 -1
- package/lib/page-fingerprints.mjs +18 -1
- package/lib/perceptual-hash-comparison.mjs +2 -34
- package/lib/perceptual-hash.mjs +36 -10
- package/lib/portable-generator.mjs +2 -29
- package/lib/project-metadata.mjs +2 -8
- package/lib/rdap-client.mjs +11 -7
- package/lib/registrable-domain.mjs +2 -56
- package/lib/registrar-standing-contract.mjs +1 -1
- package/lib/registrar-standing.mjs +9 -13
- package/lib/registry-capabilities.mjs +2 -2
- package/lib/registry-capability-catalogue.mjs +27 -19
- package/lib/risk-calibration-summary.mjs +2 -352
- package/lib/risk-scoring.mjs +2 -345
- package/lib/scoring-evidence-quality.mjs +2 -107
- package/lib/semantic-version.mjs +2 -54
- package/lib/service-binding-dns.mjs +46 -2
- package/lib/smtp-transport-review.mjs +48 -19
- package/lib/sslbl-certificates.generated.mjs +142 -139
- package/lib/static-html-analysis.mjs +32 -4
- package/lib/structured-data-identity.mjs +1 -1
- package/lib/technology-evidence-role.mjs +1 -51
- package/lib/threat-intelligence-manifests.mjs +1 -1
- package/lib/threat-intelligence-runtime.mjs +1 -1
- package/lib/threat-intelligence-targets.mjs +1 -1
- package/lib/threat-intelligence-types.mjs +2 -75
- package/lib/tls-intelligence.mjs +30 -5
- package/lib/typosquat-generator.mjs +11 -2
- package/lib/website-enrichment.mjs +404 -0
- package/lib/whois-authority.mjs +43 -5
- package/lib/whois-chain.mjs +12 -3
- package/lib/whois-common-formats.mjs +3 -1
- package/lib/whois-contacts.mjs +13 -2
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-parser.mjs +5 -3
- package/package.json +15 -14
- package/packages/analysis/analyst-taxonomy.mjs +40 -0
- package/packages/analysis/bounded-json.mjs +330 -0
- package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
- package/packages/analysis/cli-argument-grammar.mjs +216 -0
- package/packages/analysis/ct-query.mjs +27 -0
- package/packages/analysis/ct-response-bounds.mjs +9 -0
- package/packages/analysis/external-intelligence-risk.mjs +124 -0
- package/packages/analysis/http-evidence-bounds.mjs +8 -0
- package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
- package/packages/analysis/opportunity-scoring.mjs +107 -0
- package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
- package/packages/analysis/portable-generator.mjs +29 -0
- package/packages/analysis/project-metadata.mjs +8 -0
- package/packages/analysis/registrable-domain.mjs +56 -0
- package/packages/analysis/risk-calibration-summary.mjs +352 -0
- package/packages/analysis/risk-scoring.mjs +348 -0
- package/packages/analysis/scoring-evidence-quality.mjs +107 -0
- package/packages/analysis/semantic-version.mjs +54 -0
- package/packages/analysis/threat-intelligence-types.mjs +75 -0
- package/packages/cases/case-containment-handoff.mjs +195 -0
- package/packages/cases/case-evidence-links.mjs +151 -0
- package/packages/cases/case-evidence-model.mjs +89 -12
- package/packages/cases/case-evidence-quality.mjs +5 -0
- package/packages/cases/case-export-input.mjs +7 -5
- package/packages/cases/case-incident-context.mjs +52 -0
- package/packages/cases/case-markdown.mjs +11 -0
- package/packages/cases/case-migration-model.mjs +40 -24
- package/packages/cases/case-object-coverage.mjs +33 -0
- package/packages/cases/case-packet-correction.mjs +106 -0
- package/packages/cases/case-recheck-model.mjs +124 -36
- package/packages/cases/case-record-contracts.mjs +1 -1
- package/packages/cases/case-record-core.mjs +1 -1
- package/packages/cases/case-record-operations.mjs +111 -118
- package/packages/cases/case-record-projection.mjs +23 -4
- package/packages/cases/case-record-version-input.mjs +41 -0
- package/packages/cases/case-report.mjs +93 -27
- package/packages/cases/case-requested-evidence.mjs +146 -0
- package/packages/cases/case-response-actions.mjs +159 -1
- package/packages/cases/case-response-model.mjs +16 -1
- package/packages/cases/case-response-object.mjs +119 -0
- package/packages/cases/case-response-outcomes.mjs +108 -20
- package/packages/cases/case-response-packet-render.mjs +35 -12
- package/packages/cases/case-response-packet.mjs +55 -13
- package/packages/cases/case-response-records.mjs +0 -1
- package/packages/cases/case-response-review-inputs.mjs +60 -8
- package/packages/cases/case-storage-model.mjs +1 -1
- package/packages/cases/case-workflow-metadata.mjs +217 -0
- package/packages/cases/case-workflow-migration.mjs +43 -0
- package/packages/cases/http-summary.mjs +6 -1
- package/packages/comparison/favicon-similarity.mjs +1 -1
- package/packages/comparison/page-similarity.mjs +1 -1
- package/packages/comparison/relationship-evidence.mjs +21 -9
- package/packages/comparison/relationship-provenance.mjs +7 -1
- package/packages/contracts/analyst-interchange.mjs +35 -15
- package/packages/contracts/candidate-watch-review.mjs +54 -0
- package/packages/contracts/case-portability.mjs +360 -49
- package/packages/contracts/cli-command-semantics.mjs +5 -0
- package/packages/contracts/cli-grammar.mjs +6 -0
- package/packages/contracts/cli-lookup-brief.mjs +2 -0
- package/packages/contracts/context-review.mjs +25 -0
- package/packages/contracts/document-intake.mjs +11 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +30 -14
- package/packages/contracts/domain-control-manifest.mjs +1 -1
- package/packages/contracts/domain-control-monitor.mjs +13 -2
- package/packages/contracts/domain-control-review.mjs +36 -15
- package/packages/contracts/external-observation-interchange.mjs +96 -28
- package/packages/contracts/har-review.mjs +2 -0
- package/packages/contracts/identity-events.mjs +4 -0
- package/packages/contracts/investigation-run.mjs +2 -0
- package/packages/contracts/ip-address.mjs +10 -0
- package/packages/contracts/lookup-evidence.mjs +3 -2
- package/packages/contracts/mail-authentication.mjs +5 -0
- package/packages/contracts/message-intake.mjs +37 -0
- package/packages/contracts/offline-comparison.mjs +4 -1
- package/packages/contracts/passive-mail.mjs +10 -0
- package/packages/contracts/risk-calibration.mjs +44 -29
- package/packages/contracts/schema-lifecycle.mjs +3 -1
- package/packages/contracts/web-capture.mjs +2 -2
- package/packages/contracts/workspace-portability.mjs +144 -28
- package/packages/evidence/collection-quality.mjs +67 -0
- package/packages/evidence/credential-form-attribution.mjs +60 -0
- package/packages/evidence/dns-query-outcome.mjs +72 -0
- package/packages/evidence/email-recipient.mjs +18 -0
- package/packages/evidence/image-regions.mjs +15 -0
- package/packages/evidence/lookup-target.mjs +5 -1
- package/packages/evidence/observation.mjs +2 -0
- package/packages/evidence/png-pixels.mjs +117 -0
- package/packages/evidence/public-address-policy.mjs +83 -0
- package/packages/evidence/record-identity.mjs +60 -16
- package/packages/evidence/technology-evidence-role.mjs +51 -0
- package/packages/interchange/defensive-indicator-export.mjs +264 -0
- package/packages/interchange/external-findings-import.mjs +36 -29
- package/packages/interchange/json-output.mjs +14 -0
- package/packages/interchange/managed-indicator-export.mjs +36 -0
- package/packages/interchange/managed-indicator-set.mjs +223 -0
- package/packages/interchange/misp-indicator-export.mjs +93 -0
- package/packages/interchange/sharing-policy.mjs +56 -0
- package/packages/interchange/stix-indicator-export.mjs +129 -0
- package/packages/interchange/web-capture-import.mjs +22 -15
- package/packages/investigation/acquisition-decision-packet.mjs +79 -1
- package/packages/investigation/bulk-mail-exposure.mjs +7 -13
- package/packages/investigation/capture-coverage.mjs +57 -0
- package/packages/investigation/connector-provenance-review.mjs +100 -0
- package/packages/investigation/context-review.mjs +35 -0
- package/packages/investigation/document-intake.mjs +87 -0
- package/packages/investigation/docx-intake.mjs +133 -0
- package/packages/investigation/domain-history-review.mjs +132 -0
- package/packages/investigation/domain-transition-review.mjs +49 -0
- package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
- package/packages/investigation/har-intake.mjs +68 -0
- package/packages/investigation/identity-event-intake.mjs +131 -0
- package/packages/investigation/identity-incident-review.mjs +39 -0
- package/packages/investigation/incident-sequence-review.mjs +58 -0
- package/packages/investigation/infrastructure-observation.mjs +234 -0
- package/packages/investigation/intake-context.mjs +284 -0
- package/packages/investigation/intake-indicators.mjs +116 -0
- package/packages/investigation/intake-phones.mjs +155 -0
- package/packages/investigation/intake-report.mjs +20 -0
- package/packages/investigation/investigation-manifest.mjs +35 -9
- package/packages/investigation/link-intake.mjs +209 -0
- package/packages/investigation/lookup-asset-graph.mjs +1 -1
- package/packages/investigation/mail-authentication-review.mjs +222 -0
- package/packages/investigation/mail-header-identity.mjs +209 -0
- package/packages/investigation/message-intake.mjs +235 -0
- package/packages/investigation/page-behaviour.mjs +113 -0
- package/packages/investigation/pdf-intake.mjs +155 -0
- package/packages/investigation/platform-continuity-review.mjs +60 -0
- package/packages/investigation/qr-intake.mjs +56 -0
- package/packages/investigation/requested-action-hints.mjs +11 -0
- package/packages/investigation/selected-input-review.mjs +17 -0
- package/packages/investigation/storefront-review.mjs +70 -0
- package/packages/monitoring/domain-feed.mjs +321 -0
- package/packages/workspace/brand-candidate-workflow.mjs +351 -0
- package/packages/workspace/brand-keyword-campaign.mjs +231 -0
- package/packages/workspace/brand-profile-model.mjs +48 -4
- package/packages/workspace/bulk-review-model.mjs +28 -26
- package/packages/workspace/bulk-session-model.mjs +27 -13
- package/packages/workspace/campaign-model.mjs +1 -1
- package/packages/workspace/candidate-watch-handoff.mjs +195 -0
- package/packages/workspace/detection-rule-model.mjs +11 -3
- package/packages/workspace/hostile-input.mjs +1 -1
- package/packages/workspace/investigation-guide.mjs +2 -2
- package/packages/workspace/investigation-template-model.mjs +26 -2
- package/packages/workspace/page-baseline.mjs +2 -2
- package/packages/workspace/relationship-observation-model.mjs +17 -13
- package/packages/workspace/shortlist-model.mjs +24 -7
- package/packages/workspace/watchlist-history.mjs +59 -16
- package/packages/workspace/watchlist-store.mjs +152 -11
- package/packages/workspace/website-snapshot-model.mjs +28 -3
- package/packages/workspace/workspace-archive-crypto.mjs +1 -1
- package/packages/workspace/workspace-archive.mjs +5 -4
- package/third-party-notices.txt +549 -16
- package/lib/hostname.mjs +0 -2
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { getDocument, ImageKind, OPS } from 'pdfjs-dist/legacy/build/pdf.mjs';
|
|
2
|
+
import { createDocumentIntake } from "./document-intake.mjs";
|
|
3
|
+
import { MAX_DOCUMENT_IMAGE_PIXELS, MAX_DOCUMENT_PAGES, MAX_DOCUMENT_TEXT_BYTES, MAX_DOCUMENT_PAGE_OPERATIONS } from "../contracts/document-intake.mjs";
|
|
4
|
+
import { readEvidenceImageDimensions } from "../evidence/image-regions.mjs";
|
|
5
|
+
import { MAX_INTAKE_LINKS, MAX_INTAKE_URL_LENGTH } from "../contracts/message-intake.mjs";
|
|
6
|
+
/** No document-controlled resource can reach a filesystem or network fetcher. */
|
|
7
|
+
class NoExternalBinaryData {
|
|
8
|
+
async fetch() { throw new TypeError('External PDF resources are not loaded.'); }
|
|
9
|
+
}
|
|
10
|
+
function imagePixels(raw) {
|
|
11
|
+
if (!raw || typeof raw !== 'object')
|
|
12
|
+
throw new TypeError('The PDF image is unavailable.');
|
|
13
|
+
const value = raw;
|
|
14
|
+
const { width, height } = readEvidenceImageDimensions(value.width, value.height);
|
|
15
|
+
if (!(value.data instanceof Uint8Array) && !(value.data instanceof Uint8ClampedArray))
|
|
16
|
+
throw new TypeError('The PDF image has no supported pixel data.');
|
|
17
|
+
const data = value.data, count = width * height, pixels = new Uint8ClampedArray(count * 4);
|
|
18
|
+
if (value.kind === ImageKind.RGBA_32BPP && data.length === pixels.length)
|
|
19
|
+
pixels.set(data);
|
|
20
|
+
else if (value.kind === ImageKind.RGB_24BPP && data.length === count * 3) {
|
|
21
|
+
for (let index = 0; index < count; index++) {
|
|
22
|
+
pixels.set(data.subarray(index * 3, index * 3 + 3), index * 4);
|
|
23
|
+
pixels[index * 4 + 3] = 255;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else if (value.kind === ImageKind.GRAYSCALE_1BPP && data.length === Math.ceil(width / 8) * height) {
|
|
27
|
+
const stride = Math.ceil(width / 8);
|
|
28
|
+
for (let y = 0; y < height; y++)
|
|
29
|
+
for (let x = 0; x < width; x++) {
|
|
30
|
+
const colour = data[y * stride + (x >> 3)] & (128 >> (x % 8)) ? 255 : 0;
|
|
31
|
+
pixels.set([colour, colour, colour, 255], (y * width + x) * 4);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else
|
|
35
|
+
throw new TypeError('The PDF image pixel layout is unsupported.');
|
|
36
|
+
return { width, height, pixels };
|
|
37
|
+
}
|
|
38
|
+
export async function reviewPdfInput(bytes, reviewedAt, worker) {
|
|
39
|
+
const review = await createDocumentIntake(bytes, 'pdf', reviewedAt);
|
|
40
|
+
// PDF.js takes ownership of its input. The selected original stays unchanged.
|
|
41
|
+
const task = getDocument({ data: bytes.slice(), ...(worker ? { worker } : {}),
|
|
42
|
+
BinaryDataFactory: NoExternalBinaryData, useWorkerFetch: false, useWasm: false,
|
|
43
|
+
useSystemFonts: false, disableFontFace: true, enableXfa: false,
|
|
44
|
+
isOffscreenCanvasSupported: false, isImageDecoderSupported: false,
|
|
45
|
+
maxImageSize: MAX_DOCUMENT_IMAGE_PIXELS, stopAtErrors: true, verbosity: 0,
|
|
46
|
+
disableAutoFetch: true, disableStream: true, disableRange: true });
|
|
47
|
+
try {
|
|
48
|
+
const document = await task.promise;
|
|
49
|
+
const total = document.numPages, admitted = Math.min(total, MAX_DOCUMENT_PAGES);
|
|
50
|
+
review.pages(total, 0);
|
|
51
|
+
if (total > admitted)
|
|
52
|
+
review.partial('Further PDF pages exceed the page review bound.');
|
|
53
|
+
if (document.isPureXfa) {
|
|
54
|
+
review.unavailable('unsupported', 'XFA documents are not rendered or executed.');
|
|
55
|
+
return review.finish();
|
|
56
|
+
}
|
|
57
|
+
let textCharacters = 0;
|
|
58
|
+
for (let pageNumber = 1; pageNumber <= admitted; pageNumber++) {
|
|
59
|
+
const page = await document.getPage(pageNumber);
|
|
60
|
+
try {
|
|
61
|
+
const reader = page.streamTextContent().getReader();
|
|
62
|
+
let text = '';
|
|
63
|
+
try {
|
|
64
|
+
for (;;) {
|
|
65
|
+
const chunk = await reader.read();
|
|
66
|
+
if (chunk.done)
|
|
67
|
+
break;
|
|
68
|
+
for (const item of chunk.value.items) {
|
|
69
|
+
if (typeof item.str !== 'string')
|
|
70
|
+
continue;
|
|
71
|
+
textCharacters += item.str.length + 1;
|
|
72
|
+
if (textCharacters > MAX_DOCUMENT_TEXT_BYTES) {
|
|
73
|
+
review.partial('Further PDF text exceeds the text review bound.');
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
text += item.str + (item.hasEOL ? '\n' : ' ');
|
|
77
|
+
}
|
|
78
|
+
if (textCharacters > MAX_DOCUMENT_TEXT_BYTES) {
|
|
79
|
+
await reader.cancel();
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
await review.text(text, pageNumber);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
review.partial(`Text on page ${pageNumber} could not be fully decoded.`);
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
reader.releaseLock();
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const annotations = await page.getAnnotations(), destinations = [];
|
|
93
|
+
for (const annotation of annotations) {
|
|
94
|
+
if (typeof annotation.url !== 'string')
|
|
95
|
+
continue;
|
|
96
|
+
if (annotation.url.length > MAX_INTAKE_URL_LENGTH || destinations.length >= MAX_INTAKE_LINKS) {
|
|
97
|
+
review.partial(`Some links on page ${pageNumber} exceed the link review bound.`);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
destinations.push(annotation.url);
|
|
101
|
+
}
|
|
102
|
+
await review.destinations(destinations, pageNumber);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
review.partial(`Links on page ${pageNumber} could not be fully decoded.`);
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const operations = await page.getOperatorList(), seen = new Set();
|
|
109
|
+
if (operations.fnArray.length > MAX_DOCUMENT_PAGE_OPERATIONS)
|
|
110
|
+
review.partial(`Image operations on page ${pageNumber} exceed the review bound.`);
|
|
111
|
+
else
|
|
112
|
+
for (let index = 0; index < operations.fnArray.length; index++) {
|
|
113
|
+
const operation = operations.fnArray[index], args = operations.argsArray[index];
|
|
114
|
+
let raw;
|
|
115
|
+
if (operation === OPS.paintImageXObject || operation === OPS.paintImageXObjectRepeat) {
|
|
116
|
+
const id = args?.[0];
|
|
117
|
+
if (typeof id !== 'string' || seen.has(id))
|
|
118
|
+
continue;
|
|
119
|
+
seen.add(id);
|
|
120
|
+
raw = await new Promise(resolve => (id.startsWith('g_') ? page.commonObjs : page.objs).get(id, resolve));
|
|
121
|
+
}
|
|
122
|
+
else if (operation === OPS.paintInlineImageXObject)
|
|
123
|
+
raw = args?.[0];
|
|
124
|
+
else
|
|
125
|
+
continue;
|
|
126
|
+
try {
|
|
127
|
+
await review.image(imagePixels(raw), pageNumber);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
review.partial(`An image on page ${pageNumber} has unsupported or unavailable pixels.`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
review.partial(`Image operations on page ${pageNumber} could not be fully decoded.`);
|
|
136
|
+
}
|
|
137
|
+
review.pages(total, pageNumber);
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
page.cleanup();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
review.partial('QR review covers decoded embedded raster images, not vector artwork or a rendered-page reconstruction. Scripts, forms, embedded files and external resources are not executed or opened.');
|
|
144
|
+
}
|
|
145
|
+
catch (cause) {
|
|
146
|
+
if (cause instanceof Error && cause.name === 'PasswordException')
|
|
147
|
+
review.unavailable('encrypted', 'The PDF requires a password. Review an explicitly decrypted copy; no password was requested or retained.');
|
|
148
|
+
else
|
|
149
|
+
review.unavailable('unsupported', 'The PDF could not be decoded completely within this review. The original bytes are unchanged.');
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
await task.destroy();
|
|
153
|
+
}
|
|
154
|
+
return review.finish();
|
|
155
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { array, enumeration, exact, iso, text } from "../evidence/artifact-structure.mjs";
|
|
2
|
+
import { pageObservationOrigin } from "./page-behaviour.mjs";
|
|
3
|
+
import { CASE_PROVIDER_OUTCOMES, CASE_OBSERVED_EFFECT_STATES } from "../cases/case-response-records.mjs";
|
|
4
|
+
import { CONTEXT_REVIEW_SCHEMA, CONTEXT_REVIEW_VERSION, MAX_CONTEXT_RECORDS } from "../contracts/context-review.mjs";
|
|
5
|
+
export { PLATFORM_CONTINUITY_INPUT_SCHEMA, PLATFORM_CONTINUITY_INPUT_VERSION } from "../contracts/context-review.mjs";
|
|
6
|
+
export const PLATFORM_OBJECT_TYPES = ['account', 'tenant', 'application', 'extension', 'package', 'page', 'channel', 'post'];
|
|
7
|
+
export const PLATFORM_REPORT_STATES = ['not_reported', 'submitted', 'acknowledged'];
|
|
8
|
+
export function readPlatformObjects(raw) {
|
|
9
|
+
const rows = array(raw, 'Platform objects', MAX_CONTEXT_RECORDS).map(value => {
|
|
10
|
+
const row = exact(value, ['platformOrigin', 'objectType', 'objectId', 'version', 'observedAt', 'source', 'report', 'providerOutcome', 'recheck', 'recheckedAt'], 'Platform object');
|
|
11
|
+
const platformOrigin = text(row.platformOrigin, 'Platform origin', 500);
|
|
12
|
+
if (pageObservationOrigin(platformOrigin) !== platformOrigin)
|
|
13
|
+
throw new TypeError('Platform origin must be an HTTP(S) origin without credentials, paths or query data.');
|
|
14
|
+
const objectId = text(row.objectId, 'Stable object ID', 240);
|
|
15
|
+
if (!/^[A-Za-z0-9_@.:%/+~-]+$/u.test(objectId))
|
|
16
|
+
throw new TypeError('Stable object ID must contain identifier characters, not free text.');
|
|
17
|
+
iso(row.observedAt, 'Object observation time');
|
|
18
|
+
iso(row.recheckedAt, 'Object recheck time', true);
|
|
19
|
+
const recheck = enumeration(row.recheck, CASE_OBSERVED_EFFECT_STATES, 'Object recheck');
|
|
20
|
+
if ((recheck === 'not_checked') !== (row.recheckedAt === null))
|
|
21
|
+
throw new TypeError('A recheck result needs its own observation time; unchecked objects must not carry one.');
|
|
22
|
+
if (row.recheckedAt !== null && Date.parse(row.recheckedAt) < Date.parse(row.observedAt))
|
|
23
|
+
throw new TypeError('Object recheck predates its observation.');
|
|
24
|
+
const report = enumeration(row.report, PLATFORM_REPORT_STATES, 'Report status');
|
|
25
|
+
const providerOutcome = row.providerOutcome === null ? null : enumeration(row.providerOutcome, CASE_PROVIDER_OUTCOMES, 'Provider outcome');
|
|
26
|
+
if (providerOutcome !== null && report === 'not_reported')
|
|
27
|
+
throw new TypeError('A provider outcome needs a reported object.');
|
|
28
|
+
return { platformOrigin, objectType: enumeration(row.objectType, PLATFORM_OBJECT_TYPES, 'Platform object type'), objectId,
|
|
29
|
+
version: row.version === null ? null : text(row.version, 'Object version', 100), observedAt: row.observedAt, source: text(row.source, 'Object source', 500), report, providerOutcome, recheck, recheckedAt: row.recheckedAt };
|
|
30
|
+
});
|
|
31
|
+
const identities = rows.map(row => JSON.stringify([row.platformOrigin, row.objectType, row.objectId, row.version, row.observedAt]));
|
|
32
|
+
if (new Set(identities).size !== rows.length)
|
|
33
|
+
throw new TypeError('Duplicate platform object observations must be reconciled explicitly.');
|
|
34
|
+
return rows;
|
|
35
|
+
}
|
|
36
|
+
export function groupPlatformObjects(rows) {
|
|
37
|
+
const groups = new Map();
|
|
38
|
+
for (const row of rows) {
|
|
39
|
+
const key = JSON.stringify([row.platformOrigin, row.objectType, row.objectId]);
|
|
40
|
+
groups.set(key, [...(groups.get(key) ?? []), row]);
|
|
41
|
+
}
|
|
42
|
+
return [...groups.values()].map(values => values.sort((a, b) => Date.parse(a.observedAt) - Date.parse(b.observedAt)));
|
|
43
|
+
}
|
|
44
|
+
export function reviewPlatformContinuity(raw, reviewedAt) {
|
|
45
|
+
iso(reviewedAt, 'Review time');
|
|
46
|
+
const rows = readPlatformObjects(raw), groups = groupPlatformObjects(rows);
|
|
47
|
+
const observations = [];
|
|
48
|
+
for (const values of groups) {
|
|
49
|
+
const versions = new Set(values.map(row => row.version).filter(value => value !== null));
|
|
50
|
+
for (const row of values)
|
|
51
|
+
observations.push({ label: `${row.objectType} · ${row.objectId}${row.version ? ` · version ${row.version}` : ''}`, state: 'reported',
|
|
52
|
+
detail: `${row.report.replaceAll('_', ' ')}; provider outcome: ${row.providerOutcome?.replaceAll('_', ' ') ?? 'not supplied'}; independent recheck: ${row.recheck.replaceAll('_', ' ')}${row.recheckedAt ? ` at ${row.recheckedAt}` : ''}. ${versions.size > 1 ? `${versions.size} versions retained for this same declared object.` : 'No version change established.'}`,
|
|
53
|
+
source: `${row.platformOrigin} · ${row.source}`, observedAt: row.observedAt, hostname: new URL(row.platformOrigin).hostname });
|
|
54
|
+
}
|
|
55
|
+
const unresolved = rows.filter(row => ['not_checked', 'still_observed', 'changed', 'unavailable'].includes(row.recheck)).length;
|
|
56
|
+
return { schema: CONTEXT_REVIEW_SCHEMA, version: CONTEXT_REVIEW_VERSION, kind: 'platform_continuity', reviewedAt, title: 'Platform object continuity',
|
|
57
|
+
state: rows.length ? 'reviewed' : 'partial', summary: `Recorded observations: ${rows.length}; distinct platform objects: ${groups.length}; observations needing follow-up or a qualified outcome: ${unresolved}.`, observations,
|
|
58
|
+
nextSteps: ['Use the exact object and version when preparing a provider report; do not report an entire shared platform from a hosted URL alone.', 'Record each report and recheck separately. New versions, changed content and newly observed objects require their own review.'],
|
|
59
|
+
limitations: ['Platform origin, object identity and outcomes are analyst-supplied metadata, not verified provider telemetry.', 'A matching object ID establishes only declared continuity within that platform and object type. Provider resolution is separate from independent recheck; not reproduced does not establish worldwide removal.'] };
|
|
60
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { binarize, Decoder, Detector } from '@nuintun/qrcode';
|
|
2
|
+
import { decodeEvidencePng } from "../evidence/png-pixels.mjs";
|
|
3
|
+
import { assertMessageBytes, reviewMessageInput } from "./message-intake.mjs";
|
|
4
|
+
import { readEvidenceImageDimensions } from "../evidence/image-regions.mjs";
|
|
5
|
+
export const MAX_QR_DETECTION_ATTEMPTS = 256;
|
|
6
|
+
export function decodeQrPixels(image) {
|
|
7
|
+
readEvidenceImageDimensions(image.width, image.height);
|
|
8
|
+
if (!(image.pixels instanceof Uint8ClampedArray) || image.pixels.length !== image.width * image.height * 4)
|
|
9
|
+
throw new TypeError('Invalid QR pixel surface.');
|
|
10
|
+
const luminance = new Uint8Array(image.width * image.height);
|
|
11
|
+
for (let index = 0; index < luminance.length; index++) {
|
|
12
|
+
const offset = index * 4, alpha = image.pixels[offset + 3] / 255;
|
|
13
|
+
luminance[index] = Math.round((image.pixels[offset] * 0.299 + image.pixels[offset + 1] * 0.587 + image.pixels[offset + 2] * 0.114) * alpha + 255 * (1 - alpha));
|
|
14
|
+
}
|
|
15
|
+
const texts = new Set();
|
|
16
|
+
let attempts = 0, bounded = false, structured = false;
|
|
17
|
+
// Normal and inverted symbols are both considered, without altering originals.
|
|
18
|
+
for (const inverted of [false, true]) {
|
|
19
|
+
if (inverted)
|
|
20
|
+
for (let index = 0; index < luminance.length; index++)
|
|
21
|
+
luminance[index] = 255 - luminance[index];
|
|
22
|
+
const candidates = new Detector().detect(binarize(luminance, image.width, image.height)), decoder = new Decoder();
|
|
23
|
+
let candidate = candidates.next();
|
|
24
|
+
while (!candidate.done) {
|
|
25
|
+
if (++attempts > MAX_QR_DETECTION_ATTEMPTS) {
|
|
26
|
+
bounded = true;
|
|
27
|
+
candidates.return();
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
let decoded = false;
|
|
31
|
+
try {
|
|
32
|
+
const symbol = decoder.decode(candidate.value.matrix);
|
|
33
|
+
decoded = true;
|
|
34
|
+
if (symbol.structured)
|
|
35
|
+
structured = true;
|
|
36
|
+
else if (symbol.content.length <= 8_192)
|
|
37
|
+
texts.add(symbol.content);
|
|
38
|
+
else
|
|
39
|
+
bounded = true;
|
|
40
|
+
}
|
|
41
|
+
catch { /* A finder-pattern candidate need not contain a decodable symbol. */ }
|
|
42
|
+
candidate = candidates.next(decoded);
|
|
43
|
+
}
|
|
44
|
+
if (bounded)
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
return { texts: [...texts], bounded, structured };
|
|
48
|
+
}
|
|
49
|
+
export async function reviewQrInput(bytes, reviewedAt) {
|
|
50
|
+
assertMessageBytes(bytes);
|
|
51
|
+
const { texts, bounded, structured } = decodeQrPixels(decodeEvidencePng(bytes));
|
|
52
|
+
const result = await reviewMessageInput(bytes, 'qr', reviewedAt, texts);
|
|
53
|
+
const boundsReached = [...result.report.coverage.boundsReached, ...(bounded ? ['QR candidate work'] : []), ...(structured ? ['Multi-symbol structured content requires reassembly'] : [])];
|
|
54
|
+
return { ...result, report: { ...result.report, coverage: { ...result.report.coverage,
|
|
55
|
+
state: boundsReached.length ? 'partial' : result.report.coverage.state, boundsReached } } };
|
|
56
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const HINTS = {
|
|
2
|
+
clipboard_instruction: /(?:clipboard|copy.{0,40}(?:paste|command)|paste.{0,40}(?:terminal|run|console))/iu,
|
|
3
|
+
shell_instruction: /(?:powershell|cmd\.exe|curl\s|wget\s|terminal|windows\s*\+\s*r)/iu,
|
|
4
|
+
verification_prompt: /(?:verify.{0,40}(?:human|browser)|not a robot|captcha|browser.{0,40}(?:repair|update))/iu,
|
|
5
|
+
device_code_instruction: /(?:device[ -]code|enter.{0,40}code.{0,40}(?:sign|login|log in))/iu,
|
|
6
|
+
consent_instruction: /(?:grant.{0,40}(?:access|permission)|consent|approve.{0,40}(?:application|permission))/iu,
|
|
7
|
+
};
|
|
8
|
+
/** Wording matches are review leads, never a conclusion about intent or execution. */
|
|
9
|
+
export function requestedActionHints(value) {
|
|
10
|
+
return Object.entries(HINTS).filter(([, expression]) => expression.test(value)).map(([hint]) => hint);
|
|
11
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** One format dispatch for browser and CLI; parsers load only when selected. */
|
|
2
|
+
export async function reviewSelectedInput(bytes, kind, reviewedAt, pdfWorker) {
|
|
3
|
+
switch (kind) {
|
|
4
|
+
case 'text':
|
|
5
|
+
case 'email':
|
|
6
|
+
case 'calendar': return (await import("./message-intake.mjs")).reviewMessageInput(bytes, kind, reviewedAt);
|
|
7
|
+
case 'qr': return (await import("./qr-intake.mjs")).reviewQrInput(bytes, reviewedAt);
|
|
8
|
+
case 'docx': return (await import("./docx-intake.mjs")).reviewDocxInput(bytes, reviewedAt);
|
|
9
|
+
case 'pdf': return (await import("./pdf-intake.mjs")).reviewPdfInput(bytes, reviewedAt, pdfWorker);
|
|
10
|
+
case 'har': return (await import("./har-intake.mjs")).reviewHarInput(bytes, reviewedAt);
|
|
11
|
+
case 'identity': return (await import("./identity-event-intake.mjs")).reviewIdentityEventInput(bytes, reviewedAt);
|
|
12
|
+
default: {
|
|
13
|
+
const unsupported = kind;
|
|
14
|
+
throw new TypeError(`Unsupported selected input: ${String(unsupported)}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { array, boolean, domain, enumeration, exact, HEX_DIGEST_RE, iso, text } from "../evidence/artifact-structure.mjs";
|
|
2
|
+
import { pageObservationOrigin } from "./page-behaviour.mjs";
|
|
3
|
+
import { CONTEXT_REVIEW_SCHEMA, CONTEXT_REVIEW_VERSION, MAX_CONTEXT_RECORDS } from "../contracts/context-review.mjs";
|
|
4
|
+
export { STOREFRONT_INPUT_SCHEMA, STOREFRONT_INPUT_VERSION } from "../contracts/context-review.mjs";
|
|
5
|
+
export const STOREFRONT_FIELDS = [
|
|
6
|
+
{ id: 'brandNames', label: 'Displayed brand names', treatment: 'text' },
|
|
7
|
+
{ id: 'contactDomains', label: 'Published contact domains', treatment: 'domain' },
|
|
8
|
+
{ id: 'policyHashes', label: 'Policy text SHA-256 digests', treatment: 'hash' },
|
|
9
|
+
{ id: 'checkoutOrigins', label: 'Checkout destinations', treatment: 'origin' },
|
|
10
|
+
{ id: 'paymentMethods', label: 'Advertised payment methods', treatment: 'text' },
|
|
11
|
+
{ id: 'assetHashes', label: 'Distinctive asset SHA-256 digests', treatment: 'hash' },
|
|
12
|
+
];
|
|
13
|
+
export function readStorefrontObservation(raw) {
|
|
14
|
+
const row = exact(raw, ['hostname', 'observedAt', 'source', ...STOREFRONT_FIELDS.map(field => field.id)], 'Storefront observation');
|
|
15
|
+
domain(row.hostname, 'Storefront hostname');
|
|
16
|
+
iso(row.observedAt, 'Storefront observation time');
|
|
17
|
+
const fields = {};
|
|
18
|
+
let count = 0;
|
|
19
|
+
for (const field of STOREFRONT_FIELDS) {
|
|
20
|
+
if (row[field.id] === null) {
|
|
21
|
+
fields[field.id] = null;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const values = array(row[field.id], field.label, MAX_CONTEXT_RECORDS);
|
|
25
|
+
count += values.length;
|
|
26
|
+
if (count > MAX_CONTEXT_RECORDS)
|
|
27
|
+
throw new TypeError('A storefront observation exceeds the aggregate evidence-item bound.');
|
|
28
|
+
fields[field.id] = [...new Set(values.map(value => {
|
|
29
|
+
const candidate = text(value, field.label, 500);
|
|
30
|
+
if (field.treatment === 'domain')
|
|
31
|
+
domain(candidate, field.label);
|
|
32
|
+
if (field.treatment === 'hash' && !HEX_DIGEST_RE.test(candidate))
|
|
33
|
+
throw new TypeError('Storefront digests must be lowercase hexadecimal SHA-256.');
|
|
34
|
+
if (field.treatment === 'origin' && pageObservationOrigin(candidate) !== candidate)
|
|
35
|
+
throw new TypeError('Checkout observations retain HTTP(S) origins only.');
|
|
36
|
+
return candidate;
|
|
37
|
+
}))].sort();
|
|
38
|
+
}
|
|
39
|
+
return { hostname: row.hostname, observedAt: row.observedAt, source: text(row.source, 'Storefront source', 500), ...fields };
|
|
40
|
+
}
|
|
41
|
+
export function reviewStorefront(raw, reviewedAt) {
|
|
42
|
+
iso(reviewedAt, 'Review time');
|
|
43
|
+
const input = exact(raw, ['official', 'candidate', 'authorisedComparator', 'resellerStatus', 'resellerSource'], 'Storefront comparison');
|
|
44
|
+
boolean(input.authorisedComparator, 'Comparator authority');
|
|
45
|
+
if (!input.authorisedComparator)
|
|
46
|
+
throw new TypeError('Confirm that the official comparator is owned or authorised for this review.');
|
|
47
|
+
const official = readStorefrontObservation(input.official), candidate = readStorefrontObservation(input.candidate);
|
|
48
|
+
if (official.hostname === candidate.hostname)
|
|
49
|
+
throw new TypeError('Select a different candidate hostname for storefront comparison.');
|
|
50
|
+
const reseller = enumeration(input.resellerStatus, ['authorised', 'not_authorised', 'unknown'], 'Reseller status');
|
|
51
|
+
const resellerSource = input.resellerSource === null ? null : text(input.resellerSource, 'Reseller evidence', 500);
|
|
52
|
+
if (reseller !== 'unknown' && !resellerSource)
|
|
53
|
+
throw new TypeError('A reseller-status declaration needs its source or authority reference.');
|
|
54
|
+
const observations = [];
|
|
55
|
+
let missing = 0;
|
|
56
|
+
for (const field of STOREFRONT_FIELDS) {
|
|
57
|
+
const left = official[field.id], right = candidate[field.id];
|
|
58
|
+
if (left === null || right === null)
|
|
59
|
+
missing++;
|
|
60
|
+
const shared = left && right ? right.filter(value => left.includes(value)) : [];
|
|
61
|
+
observations.push({ label: field.label, state: left === null || right === null ? 'unknown' : 'observed',
|
|
62
|
+
detail: `Official: ${left === null ? 'not reviewed' : left.length ? left.join(', ') : 'no values recorded'}. Candidate: ${right === null ? 'not reviewed' : right.length ? right.join(', ') : 'no values recorded'}. ${left !== null && right !== null ? `Exact shared values: ${shared.length}.` : 'No comparison established.'}`,
|
|
63
|
+
source: `${official.hostname} · ${official.source} · ${official.observedAt} / ${candidate.hostname} · ${candidate.source}`, observedAt: candidate.observedAt, hostname: candidate.hostname });
|
|
64
|
+
}
|
|
65
|
+
observations.push({ label: 'Reseller or affiliate authority', state: reseller === 'unknown' ? 'unknown' : 'reported', detail: reseller.replaceAll('_', ' '), source: resellerSource ?? 'No authority evidence supplied', observedAt: null, hostname: candidate.hostname });
|
|
66
|
+
return { schema: CONTEXT_REVIEW_SCHEMA, version: CONTEXT_REVIEW_VERSION, kind: 'storefront', reviewedAt, title: 'Storefront comparison', state: missing ? 'partial' : 'reviewed',
|
|
67
|
+
summary: `${candidate.hostname} compared with the authorised reference ${official.hostname}; evidence categories missing one side: ${missing}.`, observations,
|
|
68
|
+
nextSteps: ['Check reseller, affiliate and regional-store authority before preparing a complaint.', 'Retain the source pages and distinctive assets privately; select relevant findings in the Case response packet.', 'Use the observed checkout platform and the rights or policy evidence to choose a reporting route. Do not enter payment information or submit an order.'],
|
|
69
|
+
limitations: ['This compares analyst-supplied observations. It does not collect a page, verify a merchant, submit a payment or issue a scam score.', 'Shared wording, assets and payment providers can have legitimate explanations. Digests establish byte equality only when inputs were captured consistently; missing observations do not establish absence.'] };
|
|
70
|
+
}
|