@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,129 @@
|
|
|
1
|
+
// Pure STIX 2.1 interchange for locally reviewed Bulk candidates. Direct
|
|
2
|
+
// observations and heuristic inferences remain separate objects so consumers
|
|
3
|
+
// do not mistake a Risk score for confirmed maliciousness.
|
|
4
|
+
import { collectDefensiveIndicatorCandidates, defensiveIndicatorProvenance, } from "./defensive-indicator-export.mjs";
|
|
5
|
+
import { STIX_INDICATOR_EXPORT_VERSION } from "../contracts/analyst-interchange.mjs";
|
|
6
|
+
export { STIX_INDICATOR_EXPORT_VERSION } from "../contracts/analyst-interchange.mjs";
|
|
7
|
+
export const MAX_STIX_INDICATORS = 1000;
|
|
8
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
9
|
+
const CONTROL_RE = /[\x00-\x1f\x7f]/;
|
|
10
|
+
const WARNING = 'Heuristic finding; review before operational use because false positives are possible.';
|
|
11
|
+
function record(value) {
|
|
12
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
13
|
+
? value
|
|
14
|
+
: {};
|
|
15
|
+
}
|
|
16
|
+
function isoTimestamp(value) {
|
|
17
|
+
if (typeof value !== 'string' || value.length > 64 || CONTROL_RE.test(value))
|
|
18
|
+
return null;
|
|
19
|
+
const parsed = Date.parse(value);
|
|
20
|
+
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null;
|
|
21
|
+
}
|
|
22
|
+
function defaultIdFactory(type) {
|
|
23
|
+
const uuid = globalThis.crypto?.randomUUID?.();
|
|
24
|
+
if (!uuid)
|
|
25
|
+
throw new Error('Secure random identifiers are unavailable for the STIX export.');
|
|
26
|
+
return `${type}--${uuid}`;
|
|
27
|
+
}
|
|
28
|
+
function stixId(type, idFactory) {
|
|
29
|
+
const value = idFactory(type);
|
|
30
|
+
const prefix = `${type}--`;
|
|
31
|
+
if (typeof value !== 'string' || !value.startsWith(prefix) || !UUID_RE.test(value.slice(prefix.length))) {
|
|
32
|
+
throw new Error(`The STIX identifier factory returned an invalid ${type} identifier.`);
|
|
33
|
+
}
|
|
34
|
+
return value.toLowerCase();
|
|
35
|
+
}
|
|
36
|
+
export function stixIndicatorProducer(producerId, generatedAt) {
|
|
37
|
+
return {
|
|
38
|
+
type: 'identity', spec_version: '2.1', id: producerId,
|
|
39
|
+
created: generatedAt, modified: generatedAt,
|
|
40
|
+
name: 'WHOISleuth', identity_class: 'system',
|
|
41
|
+
description: 'Producer of locally generated defensive-domain observations and heuristic inferences.',
|
|
42
|
+
x_whoisleuth_export_version: STIX_INDICATOR_EXPORT_VERSION,
|
|
43
|
+
x_whoisleuth_generated_at: generatedAt,
|
|
44
|
+
x_whoisleuth_false_positive_warning: WARNING,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** The caller owns validated candidates and either fresh or retained identities. */
|
|
48
|
+
export function stixIndicatorObjects(domain, availability, provenance, generatedAt, producerId, nextId) {
|
|
49
|
+
const { observedAt, riskScore: score, riskModelVersion: modelVersion } = provenance;
|
|
50
|
+
const domainId = nextId('domain-name');
|
|
51
|
+
const observationId = nextId(observedAt ? 'observed-data' : 'note');
|
|
52
|
+
const indicatorId = nextId('indicator');
|
|
53
|
+
const observationContext = {
|
|
54
|
+
created_by_ref: producerId, created: generatedAt, modified: generatedAt,
|
|
55
|
+
x_whoisleuth_observed_at_basis: observedAt ? 'scan' : 'unknown',
|
|
56
|
+
x_whoisleuth_source: 'bulk', x_whoisleuth_availability: availability,
|
|
57
|
+
x_whoisleuth_scan_depth: provenance.scanDepth,
|
|
58
|
+
};
|
|
59
|
+
const objects = [
|
|
60
|
+
{ type: 'domain-name', spec_version: '2.1', id: domainId, value: domain },
|
|
61
|
+
observedAt ? {
|
|
62
|
+
type: 'observed-data', spec_version: '2.1', id: observationId, ...observationContext,
|
|
63
|
+
first_observed: observedAt, last_observed: observedAt, number_observed: 1, object_refs: [domainId],
|
|
64
|
+
x_whoisleuth_evidence_kind: 'direct-observation',
|
|
65
|
+
} : {
|
|
66
|
+
type: 'note', spec_version: '2.1', id: observationId, ...observationContext,
|
|
67
|
+
content: 'Observation time was not retained. This candidate is supplied for heuristic review; its export time is not a sighting time.',
|
|
68
|
+
object_refs: [domainId, indicatorId], x_whoisleuth_evidence_kind: 'observation-context',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
type: 'indicator', spec_version: '2.1', id: indicatorId,
|
|
72
|
+
created_by_ref: producerId, created: generatedAt, modified: generatedAt,
|
|
73
|
+
name: `Heuristic domain candidate: ${domain}`, description: WARNING,
|
|
74
|
+
pattern: `[domain-name:value = '${domain}']`, pattern_type: 'stix', pattern_version: '2.1',
|
|
75
|
+
valid_from: generatedAt, x_whoisleuth_validity_basis: 'export',
|
|
76
|
+
labels: ['heuristic', 'defensive-review'], x_whoisleuth_evidence_kind: 'heuristic-inference',
|
|
77
|
+
x_whoisleuth_risk_score: score,
|
|
78
|
+
...(modelVersion === null ? {} : { x_whoisleuth_risk_model_version: modelVersion }),
|
|
79
|
+
x_whoisleuth_false_positive_warning: WARNING,
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
if (observedAt)
|
|
83
|
+
objects.push({
|
|
84
|
+
type: 'relationship', spec_version: '2.1', id: nextId('relationship'),
|
|
85
|
+
created_by_ref: producerId, created: generatedAt, modified: generatedAt,
|
|
86
|
+
relationship_type: 'based-on', source_ref: indicatorId, target_ref: observationId,
|
|
87
|
+
description: 'The heuristic Indicator is based on the separately represented domain observation.',
|
|
88
|
+
});
|
|
89
|
+
return objects;
|
|
90
|
+
}
|
|
91
|
+
export function buildStixIndicatorExport(records, options = {}) {
|
|
92
|
+
const generatedAt = isoTimestamp(options.generatedAt) || new Date().toISOString();
|
|
93
|
+
const idFactory = typeof options.idFactory === 'function'
|
|
94
|
+
? options.idFactory
|
|
95
|
+
: defaultIdFactory;
|
|
96
|
+
let collected;
|
|
97
|
+
try {
|
|
98
|
+
collected = collectDefensiveIndicatorCandidates(records, MAX_STIX_INDICATORS);
|
|
99
|
+
}
|
|
100
|
+
catch (cause) {
|
|
101
|
+
if (cause instanceof TypeError)
|
|
102
|
+
throw new TypeError('STIX indicator export requires an array of Bulk results.');
|
|
103
|
+
throw cause;
|
|
104
|
+
}
|
|
105
|
+
const usedIds = new Set();
|
|
106
|
+
const nextId = (type) => {
|
|
107
|
+
const id = stixId(type, idFactory);
|
|
108
|
+
if (usedIds.has(id))
|
|
109
|
+
throw new Error('The STIX identifier factory returned a duplicate identifier.');
|
|
110
|
+
usedIds.add(id);
|
|
111
|
+
return id;
|
|
112
|
+
};
|
|
113
|
+
const producerId = nextId('identity');
|
|
114
|
+
const objects = [stixIndicatorProducer(producerId, generatedAt)];
|
|
115
|
+
for (const { domain, source } of collected.entries) {
|
|
116
|
+
objects.push(...stixIndicatorObjects(domain, record(source).availability, defensiveIndicatorProvenance(source), generatedAt, producerId, nextId));
|
|
117
|
+
}
|
|
118
|
+
const bundle = { type: 'bundle', id: nextId('bundle'), objects };
|
|
119
|
+
return {
|
|
120
|
+
version: STIX_INDICATOR_EXPORT_VERSION,
|
|
121
|
+
format: 'stix',
|
|
122
|
+
generatedAt,
|
|
123
|
+
domains: collected.domains,
|
|
124
|
+
truncated: collected.truncated,
|
|
125
|
+
filename: `whoisleuth-defensive-domains-${generatedAt.slice(0, 10)}.stix.json`,
|
|
126
|
+
mimeType: 'application/stix+json;charset=utf-8',
|
|
127
|
+
content: `${JSON.stringify(bundle, null, 2)}\n`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { normalizeDomain } from "../
|
|
2
|
-
import { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, MAX_EXTERNAL_FINDING_DOMAINS, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, MAX_EXTERNAL_FINDING_SUMMARY_LENGTH, parseExternalFindingsDocument, } from "./external-findings-import.mjs";
|
|
3
|
-
import { MAX_WEB_CAPTURE_DOM_DIGEST_BYTES, MAX_WEB_CAPTURE_SCREENSHOT_BYTES, WEB_CAPTURE_MANIFEST_SCHEMA, WEB_CAPTURE_MANIFEST_VERSION, WEB_CAPTURE_SUMMARY_SCHEMA, WEB_CAPTURE_SUMMARY_VERSION, } from "../contracts/web-capture.mjs";
|
|
1
|
+
import { normalizeDomain } from "../evidence/domain-name.mjs";
|
|
2
|
+
import { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, MAX_EXTERNAL_FINDING_DOMAINS, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, MAX_EXTERNAL_FINDING_SUMMARY_LENGTH, parseExternalFindingsDocument, retainExternalFindingLimitations, } from "./external-findings-import.mjs";
|
|
3
|
+
import { MAX_WEB_CAPTURE_DOM_DIGEST_BYTES, MAX_WEB_CAPTURE_SCREENSHOT_BYTES, WEB_CAPTURE_MANIFEST_SCHEMA, WEB_CAPTURE_MANIFEST_VERSION, WEB_CAPTURE_MANIFEST_SUPPORTED_VERSIONS, WEB_CAPTURE_SUMMARY_SCHEMA, WEB_CAPTURE_SUMMARY_VERSION, } from "../contracts/web-capture.mjs";
|
|
4
4
|
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
5
|
+
import { readManifestPageBehaviour } from "../investigation/page-behaviour.mjs";
|
|
5
6
|
import { readCaptureConditions, readObservationLabel } from "../comparison/capture-context.mjs";
|
|
6
7
|
export { WEB_CAPTURE_MANIFEST_SCHEMA, WEB_CAPTURE_MANIFEST_VERSION, WEB_CAPTURE_SUMMARY_SCHEMA, WEB_CAPTURE_SUMMARY_VERSION, } from "../contracts/web-capture.mjs";
|
|
7
8
|
export const MAX_WEB_CAPTURE_SUMMARIES = 50;
|
|
@@ -25,6 +26,7 @@ const CAPTURE_KEYS = new Set([
|
|
|
25
26
|
'networkOrigins',
|
|
26
27
|
]);
|
|
27
28
|
const MANIFEST_CAPTURE_KEYS = new Set([
|
|
29
|
+
'pageBehaviour',
|
|
28
30
|
'conditions', 'observerLabel', 'vantageLabel',
|
|
29
31
|
'domain',
|
|
30
32
|
'capturedAt',
|
|
@@ -151,17 +153,17 @@ export function parseWebCaptureSummary(value) {
|
|
|
151
153
|
].filter(Boolean);
|
|
152
154
|
if (!summaries.length)
|
|
153
155
|
throw new Error(`Web capture ${index + 1} contains no supported summary evidence.`);
|
|
156
|
+
const retainedLimitations = retainExternalFindingLimitations(limitations, [
|
|
157
|
+
'Imported sanitised capture summary; WHOISleuth did not collect or independently verify this observation.',
|
|
158
|
+
]);
|
|
154
159
|
findings.push({
|
|
155
160
|
domain,
|
|
156
161
|
category: technologies.length && summaries.length === 1 ? 'http' : 'page',
|
|
157
162
|
evidenceClass: 'deployment_observation',
|
|
158
163
|
summary: summaries.join(' '),
|
|
159
164
|
observedAt,
|
|
160
|
-
completeness,
|
|
161
|
-
limitations:
|
|
162
|
-
'Imported sanitised capture summary; WHOISleuth did not collect or independently verify this observation.',
|
|
163
|
-
...limitations,
|
|
164
|
-
].slice(0, 8),
|
|
165
|
+
completeness: limitations.every((item) => retainedLimitations.includes(item)) ? completeness : 'partial',
|
|
166
|
+
limitations: retainedLimitations,
|
|
165
167
|
reference: sourceReference,
|
|
166
168
|
});
|
|
167
169
|
}
|
|
@@ -230,7 +232,7 @@ export function readWebCaptureManifest(value) {
|
|
|
230
232
|
if (!root
|
|
231
233
|
|| !onlyKeys(root, ROOT_KEYS)
|
|
232
234
|
|| root.schema !== WEB_CAPTURE_MANIFEST_SCHEMA
|
|
233
|
-
|| root.schemaVersion
|
|
235
|
+
|| !WEB_CAPTURE_MANIFEST_SUPPORTED_VERSIONS.includes(root.schemaVersion)) {
|
|
234
236
|
throw new Error(`Web capture manifests must use ${WEB_CAPTURE_MANIFEST_SCHEMA} schema version ${WEB_CAPTURE_MANIFEST_VERSION}.`);
|
|
235
237
|
}
|
|
236
238
|
const source = record(root.source);
|
|
@@ -270,7 +272,10 @@ export function readWebCaptureManifest(value) {
|
|
|
270
272
|
? capture.completeness
|
|
271
273
|
: 'unknown';
|
|
272
274
|
const limitations = stringList(capture.limitations, 8, 240, `Web capture manifest ${index + 1} limitations`);
|
|
273
|
-
|
|
275
|
+
const pageBehaviour = readManifestPageBehaviour(capture.pageBehaviour, root.schemaVersion);
|
|
276
|
+
if (pageBehaviour?.state === 'partial' && completeness === 'complete')
|
|
277
|
+
throw new Error('Partial page observations cannot declare a complete capture.');
|
|
278
|
+
contexts.push({ domain, observedAt, completeness: String(completeness), conditions, observerLabel, vantageLabel, pageBehaviour });
|
|
274
279
|
const page = record(capture.page);
|
|
275
280
|
if (page && !onlyKeys(page, PAGE_KEYS))
|
|
276
281
|
throw new Error(`Web capture manifest ${index + 1} page metadata contains unsupported fields.`);
|
|
@@ -334,6 +339,8 @@ export function readWebCaptureManifest(value) {
|
|
|
334
339
|
: '',
|
|
335
340
|
...listSummaryFragments('Observed technology labels', technologies),
|
|
336
341
|
...listSummaryFragments('Observed request domains', requestDomains),
|
|
342
|
+
...(pageBehaviour ? [`Page observations: ${pageBehaviour.requests.length} navigation/script/frame responses; ${pageBehaviour.elements.length} script/frame/form elements; ${pageBehaviour.clipboardWriteAttempts} blocked Clipboard API write attempts. State: ${pageBehaviour.state}. Full observations remain in the selected manifest.`,
|
|
343
|
+
...listSummaryFragments('Requested-action wording in body text', pageBehaviour.actionHints)] : []),
|
|
337
344
|
...artifactSummaries,
|
|
338
345
|
].filter(Boolean);
|
|
339
346
|
const summaries = partitionSummary(summaryFragments);
|
|
@@ -342,6 +349,9 @@ export function readWebCaptureManifest(value) {
|
|
|
342
349
|
throw new Error(`Web capture manifests exceed the ${MAX_EXTERNAL_FINDINGS_PER_DOMAIN}-finding per-domain import limit after preserving bounded metadata.`);
|
|
343
350
|
}
|
|
344
351
|
findingCounts.set(domain, priorFindingCount + summaries.length);
|
|
352
|
+
const retainedLimitations = retainExternalFindingLimitations(limitations, [
|
|
353
|
+
'Imported capture metadata, not independently collected website evidence; artefact bytes and separate byte checks are not retained in these findings.',
|
|
354
|
+
]);
|
|
345
355
|
for (const summary of summaries) {
|
|
346
356
|
findings.push({
|
|
347
357
|
domain,
|
|
@@ -349,11 +359,8 @@ export function readWebCaptureManifest(value) {
|
|
|
349
359
|
evidenceClass: 'deployment_observation',
|
|
350
360
|
summary,
|
|
351
361
|
observedAt,
|
|
352
|
-
completeness,
|
|
353
|
-
limitations:
|
|
354
|
-
'Imported capture metadata, not independently collected website evidence; artefact bytes and separate byte checks are not retained in these findings.',
|
|
355
|
-
...limitations,
|
|
356
|
-
].slice(0, 8),
|
|
362
|
+
completeness: limitations.every((item) => retainedLimitations.includes(item)) ? completeness : 'partial',
|
|
363
|
+
limitations: retainedLimitations,
|
|
357
364
|
reference: sourceReference,
|
|
358
365
|
});
|
|
359
366
|
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { SORTED_JSON_V2, sha256ArtifactDigestV2 } from "../evidence/artifact-integrity.mjs";
|
|
2
2
|
import { ACQUISITION_DECISION_PACKET_SCHEMA, ACQUISITION_DECISION_PACKET_VERSION, } from "../contracts/investigation-portability.mjs";
|
|
3
|
-
import { isValidAsciiDomainName } from "
|
|
3
|
+
import { isValidAsciiDomainName } from "../contracts/domain-name.mjs";
|
|
4
|
+
import { parseBoundedJsonObject } from "../analysis/bounded-json.mjs";
|
|
5
|
+
import { MAX_INVESTIGATION_PORTABLE_BYTES } from "../contracts/investigation-portability.mjs";
|
|
6
|
+
import { array, boolean, domain, enumeration, exact, fail, iso, sameValues, strings, text as structureText, validateIntegrity } from "../evidence/artifact-structure.mjs";
|
|
4
7
|
export { ACQUISITION_DECISION_PACKET_SCHEMA, ACQUISITION_DECISION_PACKET_VERSION };
|
|
8
|
+
export { MAX_INVESTIGATION_PORTABLE_BYTES as MAX_ACQUISITION_DECISION_PACKET_BYTES };
|
|
5
9
|
export const ACQUISITION_DECISIONS = [
|
|
6
10
|
'unresolved',
|
|
7
11
|
'continue_manual_review',
|
|
@@ -18,6 +22,80 @@ export const ACQUISITION_MANUAL_CHECKS = [
|
|
|
18
22
|
const DECISION_SET = new Set(ACQUISITION_DECISIONS);
|
|
19
23
|
const CHECK_SET = new Set(ACQUISITION_MANUAL_CHECKS);
|
|
20
24
|
const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/gu;
|
|
25
|
+
function validateAcquisitionItem(value, label) {
|
|
26
|
+
const item = exact(value, ['id', 'label', 'state', 'detail', 'provenance'], label);
|
|
27
|
+
const id = enumeration(item.id, ['availability', 'contacts', 'lifecycle', 'mail', 'nameservers', 'operations', 'policy_eligibility', 'policy_lifecycle', 'policy_transfer', 'tls', 'transfer', 'web'], label);
|
|
28
|
+
enumeration(item.state, ['authoritative', 'observed', 'review', 'unavailable'], label);
|
|
29
|
+
structureText(item.label, label, 200);
|
|
30
|
+
structureText(item.detail, label, 2_000);
|
|
31
|
+
structureText(item.provenance, label, 300);
|
|
32
|
+
return id;
|
|
33
|
+
}
|
|
34
|
+
export function validateAcquisitionDecisionPacketStructure(value) {
|
|
35
|
+
const root = exact(value, ['schema', 'version', 'generatedAt', 'target', 'synthetic', 'evidenceObservedAt', 'analystReview', 'evidenceReview', 'limitations', 'integrity'], 'Acquisition decision packet');
|
|
36
|
+
if (root.schema !== ACQUISITION_DECISION_PACKET_SCHEMA)
|
|
37
|
+
fail('Acquisition decision packet schema');
|
|
38
|
+
iso(root.generatedAt, 'Acquisition decision packet generatedAt');
|
|
39
|
+
domain(root.target, 'Acquisition decision packet target');
|
|
40
|
+
boolean(root.synthetic, 'Acquisition decision packet synthetic');
|
|
41
|
+
iso(root.evidenceObservedAt, 'Acquisition decision packet evidenceObservedAt', true);
|
|
42
|
+
const review = exact(root.analystReview, ['decision', 'rationale', 'reviewedChecks', 'outstandingChecks', 'state'], 'Acquisition analyst review');
|
|
43
|
+
const decision = enumeration(review.decision, ACQUISITION_DECISIONS, 'Acquisition analyst review decision');
|
|
44
|
+
structureText(review.rationale, 'Acquisition analyst review rationale', 2_000, true);
|
|
45
|
+
const reviewed = strings(review.reviewedChecks, 'Acquisition reviewed checks', ACQUISITION_MANUAL_CHECKS.length, 40);
|
|
46
|
+
const outstanding = strings(review.outstandingChecks, 'Acquisition outstanding checks', ACQUISITION_MANUAL_CHECKS.length, 40);
|
|
47
|
+
const expectedReviewed = ACQUISITION_MANUAL_CHECKS.filter((item) => reviewed.includes(item));
|
|
48
|
+
const expectedOutstanding = ACQUISITION_MANUAL_CHECKS.filter((item) => !reviewed.includes(item));
|
|
49
|
+
if (!sameValues(reviewed, expectedReviewed) || !sameValues(outstanding, expectedOutstanding))
|
|
50
|
+
fail('Acquisition analyst review checks');
|
|
51
|
+
const expectedState = decision === 'unresolved' || reviewed.length < ACQUISITION_MANUAL_CHECKS.length ? 'draft' : 'reviewed';
|
|
52
|
+
if (review.state !== expectedState)
|
|
53
|
+
fail('Acquisition analyst review state');
|
|
54
|
+
const evidence = exact(root.evidenceReview, ['version', 'label', 'state', 'items', 'transitionDependencies', 'policyChecks', 'nextSteps', 'limitations'], 'Acquisition evidence review');
|
|
55
|
+
if (evidence.version !== 2)
|
|
56
|
+
fail('Acquisition evidence review');
|
|
57
|
+
structureText(evidence.label, 'Acquisition evidence review label', 200);
|
|
58
|
+
enumeration(evidence.state, ['incomplete', 'registered', 'review_transition', 'sale_signal', 'unregistered_observation'], 'Acquisition evidence review state');
|
|
59
|
+
const expectedEvidenceIds = {
|
|
60
|
+
items: ['availability', 'lifecycle', 'transfer', 'operations', 'contacts'],
|
|
61
|
+
transitionDependencies: ['nameservers', 'web', 'mail', 'tls'],
|
|
62
|
+
policyChecks: ['policy_eligibility', 'policy_lifecycle', 'policy_transfer'],
|
|
63
|
+
};
|
|
64
|
+
for (const key of ['items', 'transitionDependencies', 'policyChecks']) {
|
|
65
|
+
const expected = expectedEvidenceIds[key];
|
|
66
|
+
const values = array(evidence[key], `Acquisition evidence review ${key}`, expected.length, expected.length);
|
|
67
|
+
const ids = values.map((item, index) => validateAcquisitionItem(item, `Acquisition evidence review ${key}[${index}]`));
|
|
68
|
+
if (!sameValues(ids, expected))
|
|
69
|
+
fail(`Acquisition evidence review ${key}`);
|
|
70
|
+
}
|
|
71
|
+
strings(evidence.nextSteps, 'Acquisition next steps', 6, 600);
|
|
72
|
+
strings(evidence.limitations, 'Acquisition evidence limitations', 12, 600);
|
|
73
|
+
strings(root.limitations, 'Acquisition limitations', 8, 600);
|
|
74
|
+
validateIntegrity(root.integrity, 'Acquisition integrity', root.version, ACQUISITION_DECISION_PACKET_VERSION);
|
|
75
|
+
}
|
|
76
|
+
function freezePacket(value) {
|
|
77
|
+
if (value && typeof value === 'object') {
|
|
78
|
+
for (const item of Object.values(value))
|
|
79
|
+
freezePacket(item);
|
|
80
|
+
Object.freeze(value);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Validate the existing supported format and full canonical digest before preview. */
|
|
84
|
+
export async function readAcquisitionDecisionPacket(raw, expected) {
|
|
85
|
+
const value = parseBoundedJsonObject(raw, { label: 'Acquisition decision packet', maximumBytes: MAX_INVESTIGATION_PORTABLE_BYTES });
|
|
86
|
+
if (value.schema !== ACQUISITION_DECISION_PACKET_SCHEMA)
|
|
87
|
+
fail('Acquisition decision packet schema');
|
|
88
|
+
validateAcquisitionDecisionPacketStructure(value);
|
|
89
|
+
const { integrity, ...unsigned } = value;
|
|
90
|
+
if (await sha256ArtifactDigestV2(unsigned) !== integrity.digestSha256) {
|
|
91
|
+
throw new TypeError('The acquisition decision packet failed its integrity check.');
|
|
92
|
+
}
|
|
93
|
+
if (value.target !== target(expected.target) || value.synthetic !== expected.synthetic) {
|
|
94
|
+
throw new TypeError('This packet belongs to a different target or demonstration context. The current review is unchanged.');
|
|
95
|
+
}
|
|
96
|
+
freezePacket(value);
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
21
99
|
function text(value, maximum) {
|
|
22
100
|
return typeof value === 'string'
|
|
23
101
|
? value.replace(CONTROL_CHARACTERS, ' ').replace(/\s+/gu, ' ').trim().slice(0, maximum)
|
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import { SORTED_JSON_V2, sha256ArtifactDigestV2 } from "../evidence/artifact-integrity.mjs";
|
|
2
|
+
import { PASSIVE_MAIL_INTERPRETATION, PASSIVE_MAIL_LABELS } from "../contracts/passive-mail.mjs";
|
|
2
3
|
import { BULK_PROFILE_CONTEXT_MISMATCH_LIMITATION, normalizeBulkProfileContext, normalizeBulkSessionResult, } from "../workspace/bulk-session-model.mjs";
|
|
3
4
|
import { BULK_MAIL_EXPOSURE_EXPORT_VERSION, BULK_MAIL_EXPOSURE_SCHEMA, BULK_MAIL_EXPOSURE_VERSION, MAX_BULK_MAIL_EXPOSURE_ROWS, } from "../contracts/investigation-portability.mjs";
|
|
4
5
|
export { BULK_MAIL_EXPOSURE_EXPORT_VERSION, BULK_MAIL_EXPOSURE_SCHEMA, BULK_MAIL_EXPOSURE_VERSION, MAX_BULK_MAIL_EXPOSURE_ROWS };
|
|
5
|
-
const STATE_LABELS = {
|
|
6
|
-
authenticated_mail: 'Receiving mail with SPF and DMARC',
|
|
7
|
-
evidence_incomplete: 'Mail evidence incomplete',
|
|
8
|
-
mail_auth_gap: 'Receiving mail with an authentication gap',
|
|
9
|
-
mail_auth_incomplete: 'Receiving mail with incomplete authentication evidence',
|
|
10
|
-
no_explicit_mx: 'No explicit MX observed',
|
|
11
|
-
null_mx: 'Null MX observed',
|
|
12
|
-
};
|
|
13
6
|
function timestamp(value) {
|
|
14
7
|
if (typeof value !== 'string' || !Number.isFinite(Date.parse(value)))
|
|
15
8
|
return null;
|
|
@@ -51,16 +44,16 @@ function exposureState(row) {
|
|
|
51
44
|
}
|
|
52
45
|
function stateDetail(state) {
|
|
53
46
|
if (state === 'authenticated_mail') {
|
|
54
|
-
return '
|
|
47
|
+
return 'Explicit MX, SPF and DMARC publications were observed.';
|
|
55
48
|
}
|
|
56
49
|
if (state === 'null_mx') {
|
|
57
50
|
return 'A null MX explicitly declaring no inbound mail was observed.';
|
|
58
51
|
}
|
|
59
52
|
if (state === 'mail_auth_gap') {
|
|
60
|
-
return '
|
|
53
|
+
return 'An MX publication was observed while SPF or DMARC was observed absent.';
|
|
61
54
|
}
|
|
62
55
|
if (state === 'mail_auth_incomplete') {
|
|
63
|
-
return '
|
|
56
|
+
return 'An MX publication was observed, but SPF or DMARC collection was incomplete.';
|
|
64
57
|
}
|
|
65
58
|
if (state === 'no_explicit_mx') {
|
|
66
59
|
return 'DNS collection completed without an explicit MX. This is not proof that SMTP delivery is impossible.';
|
|
@@ -173,7 +166,7 @@ export function buildBulkMailExposureReport(rowsRaw, options = {}) {
|
|
|
173
166
|
return {
|
|
174
167
|
domain: item.domain,
|
|
175
168
|
state,
|
|
176
|
-
label:
|
|
169
|
+
label: PASSIVE_MAIL_LABELS[state],
|
|
177
170
|
detail: stateDetail(state),
|
|
178
171
|
...relation(state, profile, profileSourceState, profileContextComparable, profileContextLimitation),
|
|
179
172
|
mutationTypes: item.mutationTypes.slice(0, 40),
|
|
@@ -217,8 +210,9 @@ export function buildBulkMailExposureReport(rowsRaw, options = {}) {
|
|
|
217
210
|
counts,
|
|
218
211
|
profileContextUnevaluatedCount,
|
|
219
212
|
limitations: [
|
|
213
|
+
PASSIVE_MAIL_INTERPRETATION,
|
|
220
214
|
'This review uses compact Bulk evidence already collected and makes no additional request.',
|
|
221
|
-
'Null MX, no explicit MX,
|
|
215
|
+
'Null MX, no explicit MX, published mail policies, policy gaps, and incomplete evidence remain separate states.',
|
|
222
216
|
'SMTP delivery, mailbox existence, catch-all behaviour, and message acceptance were not tested.',
|
|
223
217
|
],
|
|
224
218
|
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { array, boolean, enumeration, exact, integer, text } from "../evidence/artifact-structure.mjs";
|
|
2
|
+
export const MAX_CAPTURE_CHANNEL_OBSERVATIONS = 500;
|
|
3
|
+
export const CAPTURE_REQUEST_CHANNELS = ['navigation', 'document', 'frame', 'script', 'stylesheet', 'image', 'font', 'media', 'xhr', 'fetch', 'beacon', 'event_source', 'other'];
|
|
4
|
+
export const CAPTURE_DISABLED_SURFACES = ['service_workers', 'dedicated_workers', 'shared_workers', 'websockets', 'webrtc', 'webtransport', 'downloads'];
|
|
5
|
+
export function emptyCaptureCoverage() {
|
|
6
|
+
return { attempts: [], omittedAttempts: 0, disabledSurfaces: [...CAPTURE_DISABLED_SURFACES], interactions: 'not_exercised', websocketRefusals: 0, directConnectionRefusals: 0 };
|
|
7
|
+
}
|
|
8
|
+
export function captureCoverageIsPartial(value) {
|
|
9
|
+
return Boolean(value.omittedAttempts || value.websocketRefusals || value.directConnectionRefusals || value.attempts.some(row => row.state !== 'observed'));
|
|
10
|
+
}
|
|
11
|
+
export function readCaptureCoverage(raw) {
|
|
12
|
+
const value = exact(raw, ['attempts', 'omittedAttempts', 'disabledSurfaces', 'interactions', 'websocketRefusals', 'directConnectionRefusals'], 'Capture coverage');
|
|
13
|
+
const disabledSurfaces = array(value.disabledSurfaces, 'Disabled capture surfaces', CAPTURE_DISABLED_SURFACES.length).map(value => enumeration(value, CAPTURE_DISABLED_SURFACES, 'Disabled capture surface'));
|
|
14
|
+
if (new Set(disabledSurfaces).size !== CAPTURE_DISABLED_SURFACES.length)
|
|
15
|
+
throw new TypeError('Capture coverage must identify every required disabled surface.');
|
|
16
|
+
const attempts = array(value.attempts, 'Capture request attempts', MAX_CAPTURE_CHANNEL_OBSERVATIONS).map(raw => {
|
|
17
|
+
const row = exact(raw, ['position', 'channel', 'method', 'origin', 'state', 'reason', 'collectionStarted'], 'Capture request attempt');
|
|
18
|
+
const state = enumeration(row.state, ['observed', 'refused', 'unavailable'], 'Capture request state');
|
|
19
|
+
const method = enumeration(row.method, ['read', 'non_read', 'unknown'], 'Capture method category');
|
|
20
|
+
const reason = row.reason === null ? null : enumeration(row.reason, ['method', 'request_bound', 'host_bound', 'response_bound', 'shutdown', 'address_or_transport'], 'Capture request qualification');
|
|
21
|
+
boolean(row.collectionStarted, 'Collector-started flag');
|
|
22
|
+
let origin = null;
|
|
23
|
+
if (row.origin !== null) {
|
|
24
|
+
const candidate = text(row.origin, 'Capture request origin', 500), parsed = new URL(candidate);
|
|
25
|
+
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.origin !== candidate || parsed.username || parsed.password)
|
|
26
|
+
throw new TypeError('Capture request coverage retains origins only.');
|
|
27
|
+
origin = candidate;
|
|
28
|
+
}
|
|
29
|
+
if (state === 'observed' && (reason !== null || method !== 'read' || !origin || row.collectionStarted !== true)
|
|
30
|
+
|| state !== 'observed' && reason === null
|
|
31
|
+
|| reason === 'method' && (method === 'read' || row.collectionStarted)
|
|
32
|
+
|| ['request_bound', 'host_bound', 'shutdown'].includes(reason ?? '') && row.collectionStarted
|
|
33
|
+
|| state === 'unavailable' && reason !== 'address_or_transport')
|
|
34
|
+
throw new TypeError('Capture request coverage has contradictory collection states.');
|
|
35
|
+
return { position: integer(row.position, 'Capture request position', 1, 1_000_000), channel: enumeration(row.channel, CAPTURE_REQUEST_CHANNELS, 'Capture request channel'),
|
|
36
|
+
method, origin, state, reason, collectionStarted: row.collectionStarted };
|
|
37
|
+
});
|
|
38
|
+
if (attempts.some((row, index) => index > 0 && row.position <= attempts[index - 1].position))
|
|
39
|
+
throw new TypeError('Capture request positions must be unique and ordered.');
|
|
40
|
+
return { attempts, omittedAttempts: integer(value.omittedAttempts, 'Omitted capture attempts', 0, 1_000_000), disabledSurfaces: [...CAPTURE_DISABLED_SURFACES],
|
|
41
|
+
interactions: enumeration(value.interactions, ['not_exercised'], 'Capture interaction coverage'),
|
|
42
|
+
websocketRefusals: integer(value.websocketRefusals, 'WebSocket refusals', 0, 1_000_000), directConnectionRefusals: integer(value.directConnectionRefusals, 'Direct-connection refusals', 0, 1_000_000) };
|
|
43
|
+
}
|
|
44
|
+
export function captureAttemptDescription(value) {
|
|
45
|
+
const status = value.state === 'observed' ? 'Response supplied' : value.state === 'refused' ? 'Response refused' : 'Response unavailable';
|
|
46
|
+
const reason = { method: 'non-read method', request_bound: 'request limit', host_bound: 'host limit', response_bound: 'response byte limit', shutdown: 'capture closing', address_or_transport: 'address or transport check' };
|
|
47
|
+
return `${status}${value.reason ? ` · ${reason[value.reason]}` : ''} · collector ${value.collectionStarted ? 'started' : 'not started'}`;
|
|
48
|
+
}
|
|
49
|
+
/** Unseen in a bounded capture is not the same as disabled or absent. */
|
|
50
|
+
export function captureChannelSummary(value) {
|
|
51
|
+
return CAPTURE_REQUEST_CHANNELS.map(channel => {
|
|
52
|
+
const rows = value.attempts.filter(row => row.channel === channel);
|
|
53
|
+
return { channel, observed: rows.filter(row => row.state === 'observed').length, refused: rows.filter(row => row.state === 'refused').length,
|
|
54
|
+
unavailable: rows.filter(row => row.state === 'unavailable').length,
|
|
55
|
+
coverage: rows.length ? 'recorded' : value.omittedAttempts ? 'unknown' : 'not_observed' };
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { array, exact, iso, record, text } from "../evidence/artifact-structure.mjs";
|
|
2
|
+
import { pageObservationOrigin } from "./page-behaviour.mjs";
|
|
3
|
+
import { CONTEXT_REVIEW_SCHEMA, CONTEXT_REVIEW_VERSION, MAX_CONTEXT_RECORDS, MAX_CONTEXT_INPUT_BYTES } from "../contracts/context-review.mjs";
|
|
4
|
+
import { parseBoundedJson } from "../analysis/bounded-json.mjs";
|
|
5
|
+
export { CONNECTOR_INPUT_SCHEMA, CONNECTOR_INPUT_VERSION } from "../contracts/context-review.mjs";
|
|
6
|
+
const KNOWN_FIELDS = new Set(['url', 'serverUrl', 'command', 'args', 'env', 'headers', 'type', 'transport', 'disabled', 'enabled', 'capabilities', 'package', 'oauth', 'auth']);
|
|
7
|
+
const CAPABILITIES = ['tools', 'resources', 'prompts', 'logging', 'completions'];
|
|
8
|
+
const PACKAGE = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*(?:@[a-zA-Z0-9][a-zA-Z0-9.+_-]*)?$/u;
|
|
9
|
+
function countObject(value, label) {
|
|
10
|
+
if (value === undefined)
|
|
11
|
+
return 0;
|
|
12
|
+
const keys = Object.keys(record(value, label));
|
|
13
|
+
if (keys.length > MAX_CONTEXT_RECORDS)
|
|
14
|
+
throw new TypeError(`${label} exceeds the metadata bound.`);
|
|
15
|
+
return keys.length;
|
|
16
|
+
}
|
|
17
|
+
/** Configuration is data only. Values of arguments, environment, headers and auth are never returned. */
|
|
18
|
+
export function readConnectorConfiguration(raw) {
|
|
19
|
+
const root = record(raw, 'Connector configuration');
|
|
20
|
+
const roots = ['mcpServers', 'servers'].filter(key => Object.hasOwn(root, key));
|
|
21
|
+
if (roots.length !== 1)
|
|
22
|
+
throw new TypeError('Select a configuration with exactly one mcpServers or servers object.');
|
|
23
|
+
if (Object.keys(root).some(key => !roots.includes(key)))
|
|
24
|
+
throw new TypeError('Select only the connector configuration section; unrelated settings are not reviewed.');
|
|
25
|
+
const entries = Object.entries(record(root[roots[0]], 'Connector entries'));
|
|
26
|
+
if (entries.length > MAX_CONTEXT_RECORDS)
|
|
27
|
+
throw new TypeError('Connector count exceeds the review bound.');
|
|
28
|
+
return entries.map(([name, value]) => {
|
|
29
|
+
text(name, 'Connector name', 200);
|
|
30
|
+
const item = record(value, 'Connector definition');
|
|
31
|
+
if (Object.keys(item).length > MAX_CONTEXT_RECORDS)
|
|
32
|
+
throw new TypeError('Connector definition exceeds the field bound.');
|
|
33
|
+
if (item.enabled !== undefined && typeof item.enabled !== 'boolean' || item.disabled !== undefined && typeof item.disabled !== 'boolean'
|
|
34
|
+
|| item.enabled !== undefined && item.disabled !== undefined && item.enabled === item.disabled)
|
|
35
|
+
throw new TypeError('Connector enablement declarations are invalid or conflicting.');
|
|
36
|
+
const suppliedUrl = item.url ?? item.serverUrl;
|
|
37
|
+
if (item.url !== undefined && item.serverUrl !== undefined && item.url !== item.serverUrl)
|
|
38
|
+
throw new TypeError('Conflicting connector URL declarations require a deliberate selection.');
|
|
39
|
+
const origin = suppliedUrl === undefined ? null : pageObservationOrigin(suppliedUrl);
|
|
40
|
+
if (suppliedUrl !== undefined && !origin)
|
|
41
|
+
throw new TypeError('Remote connector URL must be bounded HTTP(S) without credentials.');
|
|
42
|
+
const command = item.command === undefined ? null : text(item.command, 'Local connector command', 2_000);
|
|
43
|
+
const basename = command?.split(/[\\/]/u).at(-1) ?? null;
|
|
44
|
+
const executable = basename && /^[A-Za-z0-9._-]{1,120}$/u.test(basename) ? basename : null;
|
|
45
|
+
const args = item.args === undefined ? [] : array(item.args, 'Connector arguments', MAX_CONTEXT_RECORDS).map(value => text(value, 'Connector argument', 8_192, true));
|
|
46
|
+
let packageIdentity = item.package === undefined ? null : text(item.package, 'Declared connector package', 240);
|
|
47
|
+
if (packageIdentity !== null && !PACKAGE.test(packageIdentity))
|
|
48
|
+
throw new TypeError('Declared package must be a package identity, not an executable expression or URL.');
|
|
49
|
+
if (packageIdentity === null && ['npx', 'npx.cmd', 'uvx'].includes(executable ?? '')) {
|
|
50
|
+
// Only a simple first package operand is interpreted. Complex invocations remain unknown.
|
|
51
|
+
const operands = args.filter(value => !['--yes', '-y', '--no-install'].includes(value));
|
|
52
|
+
if (operands[0] && PACKAGE.test(operands[0]))
|
|
53
|
+
packageIdentity = operands[0];
|
|
54
|
+
}
|
|
55
|
+
const capabilityObject = item.capabilities === undefined ? {} : record(item.capabilities, 'Declared capabilities');
|
|
56
|
+
if (Object.keys(capabilityObject).length > MAX_CONTEXT_RECORDS)
|
|
57
|
+
throw new TypeError('Declared capabilities exceed the bound.');
|
|
58
|
+
return { name, transport: origin && command ? 'ambiguous' : origin ? 'remote' : command ? 'local' : 'unknown', enabled: item.enabled === false || item.disabled === true ? 'disabled' : item.enabled === true || item.disabled === false ? 'enabled' : 'unknown', origin, executable, package: packageIdentity,
|
|
59
|
+
argumentCount: args.length, environmentCount: countObject(item.env, 'Environment metadata'), headerCount: countObject(item.headers, 'Header metadata'),
|
|
60
|
+
authenticationDeclared: item.auth !== undefined || item.oauth !== undefined,
|
|
61
|
+
declaredCapabilities: CAPABILITIES.filter(key => Object.hasOwn(capabilityObject, key) && capabilityObject[key] !== false && capabilityObject[key] !== null),
|
|
62
|
+
unsupportedFields: Object.keys(item).filter(key => !KNOWN_FIELDS.has(key)).length + Object.keys(capabilityObject).filter(key => !CAPABILITIES.includes(key)).length,
|
|
63
|
+
privateLocationOmitted: suppliedUrl !== undefined && new URL(suppliedUrl).href !== `${origin}/` || command !== null && command !== executable,
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export function connectorProvenancePresentation(raw, reviewedAt) {
|
|
68
|
+
iso(reviewedAt, 'Review time');
|
|
69
|
+
const input = exact(raw, ['current', 'previous'], 'Connector review');
|
|
70
|
+
const current = readConnectorConfiguration(input.current), previous = input.previous === null ? null : readConnectorConfiguration(input.previous);
|
|
71
|
+
const signature = (row) => JSON.stringify([row.transport, row.enabled, row.origin, row.executable, row.package, row.argumentCount, row.environmentCount, row.headerCount, row.authenticationDeclared, row.declaredCapabilities]);
|
|
72
|
+
const observations = current.map(row => {
|
|
73
|
+
const old = previous?.find(value => value.name === row.name);
|
|
74
|
+
const changed = old ? signature(old) !== signature(row) : false;
|
|
75
|
+
return { label: `${row.name} · ${row.transport} connector`, state: row.transport === 'unknown' || row.transport === 'ambiguous' || row.unsupportedFields ? 'partial' : changed ? 'changed' : 'reported',
|
|
76
|
+
detail: `Declared enablement: ${row.enabled}. Endpoint: ${row.origin ?? 'not supplied'}; executable: ${row.executable ?? 'not retained or unknown'}; package: ${row.package ?? 'not established'}. Arguments: ${row.argumentCount}; environment entries: ${row.environmentCount}; headers: ${row.headerCount}; auth declaration ${row.authenticationDeclared ? 'present' : 'not supplied'}. Capabilities: ${row.declaredCapabilities.join(', ') || 'not declared'}. ${previous ? old ? changed ? 'Retained metadata changed.' : 'Retained metadata matches; excluded values were not compared.' : 'Newly listed connector.' : 'No earlier configuration selected.'}${row.unsupportedFields ? ` Uninterpreted fields: ${row.unsupportedFields}.` : ''}${row.privateLocationOmitted ? ' Private location detail omitted.' : ''}`,
|
|
77
|
+
source: 'Analyst-selected local configuration; declarations were not negotiated with a server', observedAt: null, hostname: row.origin ? new URL(row.origin).hostname : null };
|
|
78
|
+
});
|
|
79
|
+
for (const row of previous ?? [])
|
|
80
|
+
if (!current.some(value => value.name === row.name))
|
|
81
|
+
observations.push({ label: `${row.name} · no longer listed`, state: 'changed', detail: 'Absent from the selected current configuration; this does not establish that a process or endpoint has stopped.', source: 'Selected configuration comparison', observedAt: null, hostname: row.origin ? new URL(row.origin).hostname : null });
|
|
82
|
+
const report = { schema: CONTEXT_REVIEW_SCHEMA, version: CONTEXT_REVIEW_VERSION, kind: 'connector', reviewedAt, title: 'Connector provenance', state: !current.length || observations.some(row => row.state === 'partial') ? 'partial' : 'reviewed',
|
|
83
|
+
summary: `Configured connectors: ${current.length}${previous ? `; earlier configuration entries: ${previous.length}` : ''}. Commands were not run and endpoints were not contacted.`, observations,
|
|
84
|
+
nextSteps: ['Verify package publisher, exact version, installation source and requested privileges through a trusted source before enabling a local connector.', 'For remote connectors, investigate the endpoint domain separately and review authentication and data-sharing scope before granting access.', 'Review tool, resource and prompt capabilities against the task. Configuration declarations are not evidence of negotiated capabilities or trustworthy behaviour.'],
|
|
85
|
+
limitations: ['Arguments, local paths, environment values, header values, auth material and URL paths or queries are excluded from this report. Names and package identities remain visible; review them before sharing.', 'Matching retained metadata does not mean excluded values are unchanged. This is not code execution, a server connection, a protocol conformance test or safety certification.'] };
|
|
86
|
+
return { report, connectors: current };
|
|
87
|
+
}
|
|
88
|
+
export function reviewConnectorProvenance(raw, reviewedAt) {
|
|
89
|
+
return connectorProvenancePresentation(raw, reviewedAt).report;
|
|
90
|
+
}
|
|
91
|
+
export function reviewConnectorConfigurationText(current, previous, reviewedAt) {
|
|
92
|
+
return connectorConfigurationPresentation(current, previous, reviewedAt).report;
|
|
93
|
+
}
|
|
94
|
+
/** Browser presentation receives only the same minimised metadata as the report. */
|
|
95
|
+
export function connectorConfigurationPresentation(current, previous, reviewedAt) {
|
|
96
|
+
if (typeof current !== 'string' || typeof previous !== 'string')
|
|
97
|
+
throw new TypeError('Select connector configuration text.');
|
|
98
|
+
const parse = (value) => parseBoundedJson(value, { label: 'Connector configuration', maximumBytes: MAX_CONTEXT_INPUT_BYTES });
|
|
99
|
+
return connectorProvenancePresentation({ current: parse(current), previous: previous.trim() ? parse(previous) : null }, reviewedAt);
|
|
100
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { parseBoundedJson } from "../analysis/bounded-json.mjs";
|
|
2
|
+
import { exact, text } from "../evidence/artifact-structure.mjs";
|
|
3
|
+
import { readEditableCaseExport } from "../cases/case-export-input.mjs";
|
|
4
|
+
import { MAX_CONTEXT_INPUT_BYTES, DOMAIN_HISTORY_LEGACY_INPUT_VERSION } from "../contracts/context-review.mjs";
|
|
5
|
+
import { DOMAIN_HISTORY_INPUT_SCHEMA, DOMAIN_HISTORY_INPUT_VERSION, reviewDomainHistory } from "./domain-history-review.mjs";
|
|
6
|
+
import { PLATFORM_CONTINUITY_INPUT_SCHEMA, PLATFORM_CONTINUITY_INPUT_VERSION, reviewPlatformContinuity } from "./platform-continuity-review.mjs";
|
|
7
|
+
import { STOREFRONT_INPUT_SCHEMA, STOREFRONT_INPUT_VERSION, reviewStorefront } from "./storefront-review.mjs";
|
|
8
|
+
import { CONNECTOR_INPUT_SCHEMA, CONNECTOR_INPUT_VERSION, reviewConnectorProvenance } from "./connector-provenance-review.mjs";
|
|
9
|
+
import { INCIDENT_SEQUENCE_INPUT_SCHEMA, INCIDENT_SEQUENCE_INPUT_VERSION, reviewIncidentSequence } from "./incident-sequence-review.mjs";
|
|
10
|
+
export { CONTEXT_INPUT_SCHEMAS } from "../contracts/context-review.mjs";
|
|
11
|
+
/** Adapters share the same explicit dispatch; each domain owns its validation. */
|
|
12
|
+
export function reviewContextInput(raw, reviewedAt) {
|
|
13
|
+
const input = exact(raw, ['schema', 'version', 'evidence'], 'Context review input');
|
|
14
|
+
if (input.schema === DOMAIN_HISTORY_INPUT_SCHEMA && (input.version === DOMAIN_HISTORY_INPUT_VERSION || input.version === DOMAIN_HISTORY_LEGACY_INPUT_VERSION)) {
|
|
15
|
+
const evidence = exact(input.evidence, ['caseExport', 'caseId', 'declarations'], 'Domain history input');
|
|
16
|
+
const caseId = text(evidence.caseId, 'Selected Case ID', 240);
|
|
17
|
+
const cases = readEditableCaseExport(JSON.stringify(evidence.caseExport));
|
|
18
|
+
const selected = cases.find(record => record.id === caseId);
|
|
19
|
+
if (!selected)
|
|
20
|
+
throw new TypeError('The selected Case is not present in this export.');
|
|
21
|
+
return reviewDomainHistory(selected, evidence.declarations, reviewedAt, input.version);
|
|
22
|
+
}
|
|
23
|
+
if (input.schema === PLATFORM_CONTINUITY_INPUT_SCHEMA && input.version === PLATFORM_CONTINUITY_INPUT_VERSION)
|
|
24
|
+
return reviewPlatformContinuity(input.evidence, reviewedAt);
|
|
25
|
+
if (input.schema === STOREFRONT_INPUT_SCHEMA && input.version === STOREFRONT_INPUT_VERSION)
|
|
26
|
+
return reviewStorefront(input.evidence, reviewedAt);
|
|
27
|
+
if (input.schema === CONNECTOR_INPUT_SCHEMA && input.version === CONNECTOR_INPUT_VERSION)
|
|
28
|
+
return reviewConnectorProvenance(input.evidence, reviewedAt);
|
|
29
|
+
if (input.schema === INCIDENT_SEQUENCE_INPUT_SCHEMA && input.version === INCIDENT_SEQUENCE_INPUT_VERSION)
|
|
30
|
+
return reviewIncidentSequence(input.evidence, reviewedAt);
|
|
31
|
+
throw new TypeError('Unsupported context review schema or version.');
|
|
32
|
+
}
|
|
33
|
+
export function parseContextInput(input) {
|
|
34
|
+
return parseBoundedJson(input, { label: 'Context review input', maximumBytes: MAX_CONTEXT_INPUT_BYTES });
|
|
35
|
+
}
|