@slicedearth/whoisleuth-cli 1.47.4 → 2.0.1
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/README.md +74 -177
- package/SECURITY.md +6 -3
- package/bin/whoisleuth.mjs +2 -1
- package/cli/archive-inspect.mjs +28 -10
- package/cli/arguments.mjs +204 -174
- package/cli/artifact-structure.mjs +837 -163
- package/cli/artifact-verify.mjs +90 -61
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +33 -20
- package/cli/bulk.mjs +9 -2
- package/cli/case-pack.mjs +180 -576
- package/cli/ci-report.mjs +6 -1
- package/cli/collection-preflight.mjs +2 -2
- package/cli/command-catalogue.mjs +12 -3
- package/cli/command-reference.mjs +910 -92
- package/cli/completion.mjs +504 -226
- package/cli/config-profile.mjs +13 -3
- package/cli/discover.mjs +7 -1
- package/cli/discovery-observation-snapshot.mjs +24 -28
- package/cli/discovery-scan-command-runner.mjs +9 -9
- package/cli/discovery-scan.mjs +17 -3
- package/cli/discovery-snapshot.mjs +15 -7
- package/cli/domain-control-monitor.mjs +203 -30
- package/cli/domain-control-observations.mjs +15 -21
- package/cli/errors.mjs +8 -3
- package/cli/evidence-signing.mjs +10 -20
- package/cli/external-observation-mapping.mjs +3 -3
- package/cli/formatters/json.mjs +20 -32
- package/cli/formatters/terminal.mjs +583 -22
- package/cli/input.mjs +7 -2
- package/cli/interactive-launcher.mjs +226 -0
- package/cli/interchange-report.mjs +19 -19
- package/cli/investigation-manifest.mjs +1 -1
- package/cli/investigation-plan.mjs +167 -16
- package/cli/investigation-run.mjs +9 -1
- package/cli/lookalike-calibration.mjs +3 -1
- package/cli/lookup-brief.mjs +13 -8
- package/cli/lookup-browser.mjs +844 -0
- package/cli/lookup-command-runner.mjs +94 -31
- package/cli/lookup-diff.mjs +2 -2
- package/cli/lookup-plan.mjs +1 -0
- package/cli/mail-review.mjs +206 -94
- package/cli/manual.mjs +5 -5
- package/cli/offline-evidence-review.mjs +18 -7
- package/cli/open-asset-model-bridge.mjs +1 -1
- package/cli/output-file.mjs +70 -14
- package/cli/page-compare.mjs +11 -6
- package/cli/progress-events.mjs +2 -1
- package/cli/progress.mjs +3 -3
- package/cli/registry-cohort.mjs +9 -51
- package/cli/registry-doctor.mjs +67 -28
- package/cli/retained-artifact-diff.mjs +7 -8
- package/cli/risk-calibration.mjs +85 -38
- package/cli/runner.mjs +189 -75
- package/cli/saved-lookup.mjs +424 -57
- package/cli/sharing-review.mjs +6 -1
- package/cli/source-reliability.mjs +75 -36
- package/cli/terminal-presentation.mjs +43 -18
- package/docs/cli-reference.md +125 -1708
- package/docs/cli.md +86 -254
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -145
- package/frontend/src/lib/analysis/domain-control-records.js +3 -88
- package/lib/availability.mjs +58 -40
- package/lib/bounded-contract-normalizers.mjs +2 -4
- package/lib/bounded-file.mjs +78 -4
- package/lib/bounded-json.mjs +51 -13
- package/lib/browser-library-profile.mjs +1 -1
- package/lib/classify.mjs +56 -5
- package/lib/client-behavior-profile.mjs +1 -1
- package/lib/credential-surface-profile.mjs +1 -1
- package/lib/cryptographic-assurance.mjs +35 -17
- package/lib/ct-query.mjs +3 -3
- package/lib/ct-search.mjs +37 -21
- package/lib/distributed-operation-budget.mjs +4 -2
- package/lib/dns-delegation-health.mjs +9 -6
- package/lib/dns-intelligence.mjs +1 -1
- package/lib/dnssec-chain-validation.mjs +2 -3
- package/lib/dnssec-evidence-validation.mjs +13 -7
- package/lib/domain-assurance.mjs +12 -12
- package/lib/domain-change-packet.mjs +1 -1
- package/lib/domain-control-flight-recorder.mjs +442 -78
- package/lib/domain-control-manifest.mjs +314 -59
- package/lib/domain-posture-analysis.mjs +5 -2
- package/lib/encrypted-dns-contract.mjs +2 -3
- package/lib/evidence-export.mjs +518 -36
- package/lib/external-intelligence-risk.mjs +26 -10
- package/lib/feature-policy.mjs +1 -1
- package/lib/homepage-metadata-contract.mjs +290 -0
- package/lib/hostname.mjs +1 -1
- package/lib/html-signals.mjs +89 -5
- package/lib/http-intelligence.mjs +331 -2
- package/lib/interchange-fidelity-registry.mjs +28 -26
- package/lib/lookup-cache.mjs +6 -4
- package/lib/lookup.mjs +25 -2
- package/lib/observed-network-context.mjs +1 -1
- package/lib/page-fingerprints.mjs +2 -2
- package/lib/page-role-profile.mjs +1 -1
- package/lib/portable-generator.mjs +8 -0
- package/lib/project-metadata.mjs +1 -1
- package/lib/provider-policy-admission.mjs +3 -1
- package/lib/rdap-bootstrap.mjs +76 -16
- package/lib/rdap-client.mjs +20 -12
- package/lib/rdap-normalization.mjs +306 -75
- package/lib/rdap-transport.mjs +2 -2
- package/lib/rdap-validation.mjs +65 -14
- package/lib/rdap.mjs +2 -2
- package/lib/registrable-domain.mjs +36 -0
- package/lib/registry-insights.mjs +7 -0
- package/lib/response-policy.mjs +27 -4
- package/lib/risk-calibration-summary.mjs +35 -33
- package/lib/risk-scoring.mjs +4 -3
- package/lib/safe-fetch.mjs +62 -40
- package/lib/security-txt.mjs +1 -1
- package/lib/smtp-transport-review.mjs +8 -5
- package/lib/sslbl-intelligence.mjs +6 -8
- package/lib/static-html-analysis.mjs +301 -4
- package/lib/structured-data-identity.mjs +51 -19
- package/lib/threat-intelligence-fixtures.mjs +2 -4
- package/lib/threat-intelligence-manifests.mjs +2 -7
- package/lib/threat-intelligence-runtime.mjs +3 -5
- package/lib/threat-intelligence-types.mjs +3 -0
- package/lib/threatfox-intelligence.mjs +7 -3
- package/lib/tls-intelligence.mjs +4 -4
- package/lib/urlhaus-intelligence.mjs +4 -2
- package/lib/urlscan-intelligence.mjs +6 -5
- package/lib/website-security-posture.mjs +1 -1
- package/lib/website-technology.mjs +1 -1
- package/lib/whois-authority.mjs +43 -8
- package/lib/whois-chain.mjs +25 -3
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-transport.mjs +12 -3
- package/lib/zone-intent-review.mjs +1 -1
- package/package.json +13 -5
- package/{frontend/src/lib/analysis/case-brand-profile-references.js → packages/cases/case-brand-profile-references.mjs} +2 -2
- package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +35 -14
- package/{frontend/src/lib/analysis/case-investigation-branch-model.js → packages/cases/case-investigation-branch-model.mjs} +10 -12
- package/packages/cases/case-migration-model.mjs +476 -0
- package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
- package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -21
- package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
- package/packages/cases/case-record-model.mjs +9 -0
- package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +72 -29
- package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +108 -14
- package/packages/cases/case-response-model.mjs +1604 -0
- package/packages/cases/case-response-packet.mjs +1312 -0
- package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
- package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
- package/{frontend/src/lib/analysis/comparison-ledger-bulk.js → packages/comparison/comparison-ledger-bulk.mjs} +2 -2
- package/{frontend/src/lib/analysis/comparison-ledger-contract.js → packages/comparison/comparison-ledger-contract.mjs} +4 -4
- package/{frontend/src/lib/analysis/comparison-ledger-json.js → packages/comparison/comparison-ledger-json.mjs} +1 -0
- package/{frontend/src/lib/analysis/comparison-ledger-serialization.js → packages/comparison/comparison-ledger-serialization.mjs} +1 -0
- package/packages/comparison/favicon-similarity.mjs +78 -0
- package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
- package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
- package/packages/contracts/analyst-review-state-contract.mjs +61 -0
- package/packages/contracts/analyst-review-state.mjs +50 -0
- package/packages/contracts/case-portability.mjs +1310 -0
- package/packages/contracts/cli-command-catalogue.mjs +2 -0
- package/packages/contracts/cli-lookup.mjs +394 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
- package/packages/contracts/domain-control-manifest.mjs +1027 -0
- package/packages/contracts/domain-control-monitor.mjs +310 -0
- package/packages/contracts/domain-control-review.mjs +828 -0
- package/packages/contracts/domain-name.mjs +4 -0
- package/packages/contracts/external-observation-interchange.mjs +224 -0
- package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
- package/packages/contracts/investigation-portability.mjs +331 -0
- package/packages/contracts/offline-comparison.mjs +160 -0
- package/packages/contracts/risk-calibration.mjs +1288 -0
- package/packages/contracts/schema-compatibility.mjs +71 -0
- package/packages/contracts/schema-lifecycle.mjs +1702 -0
- package/packages/contracts/sslbl-snapshot.mjs +17 -0
- package/packages/contracts/workspace-portability.mjs +1347 -0
- package/{frontend/src/lib/analysis/artifact-integrity.js → packages/evidence/artifact-integrity.mjs} +13 -11
- package/packages/evidence/decision-fact.mjs +735 -0
- package/packages/evidence/domain-control-runtime.mjs +547 -0
- package/packages/evidence/domain-name.mjs +41 -0
- package/packages/evidence/observation.mjs +384 -0
- package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +24 -22
- package/packages/interchange/retained-text.mjs +8 -0
- package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +9 -6
- package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +5 -6
- package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +4 -6
- package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +5 -5
- package/packages/investigation/bulk-review-policy.mjs +1 -0
- package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +30 -32
- package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
- package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
- package/{frontend/src/lib/analysis/lookup-claim-passport.js → packages/investigation/lookup-claim-passport.mjs} +6 -10
- package/{frontend/src/lib/analysis/lookup-claim-readiness.js → packages/investigation/lookup-claim-readiness.mjs} +9 -2
- package/packages/investigation/lookup-investigation-brief.mjs +172 -0
- package/packages/monitoring/analyst-review-state.mjs +680 -0
- package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +71 -48
- package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +13 -14
- package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +33 -59
- package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +8 -5
- package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
- package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
- package/packages/workspace/hostile-input.mjs +172 -0
- package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +8 -45
- package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +18 -21
- package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
- package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
- package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
- package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
- package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
- package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +13 -13
- package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
- package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +110 -89
- package/third-party-notices.txt +14 -14
- package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
- package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
- package/frontend/src/lib/analysis/case-migration-model.js +0 -305
- package/frontend/src/lib/analysis/case-record-model.js +0 -9
- package/frontend/src/lib/analysis/case-response-model.js +0 -728
- package/frontend/src/lib/analysis/case-response-packet.js +0 -583
- package/frontend/src/lib/analysis/lookup-decision-support.js +0 -542
- package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
- package/frontend/src/lib/analysis/lookup-presentation.js +0 -120
- package/frontend/src/lib/analysis/lookup-response.js +0 -2
- package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
- package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
- package/frontend/src/lib/analysis/scoring.js +0 -55
- package/frontend/src/lib/analysis/utils.js +0 -255
- package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
- package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
- package/frontend/src/lib/analysis/visualization-matrix.js +0 -131
- package/frontend/src/lib/analysis/visualization-models.js +0 -338
- package/frontend/src/lib/analysis/visualization-time-series.js +0 -185
- package/lib/bounded-json-response.mjs +0 -170
- package/lib/lookup-response-contract.mjs +0 -1034
- package/lib/observation.mjs +0 -115
- /package/{frontend/src/lib/analysis/opaque-reference-id.js → packages/cases/opaque-reference-id.mjs} +0 -0
- /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
- /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
- /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
package/cli/artifact-verify.mjs
CHANGED
|
@@ -2,22 +2,19 @@ import { Buffer } from 'node:buffer';
|
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { parseBoundedJsonObject } from "./bounded-json.mjs";
|
|
4
4
|
import { validateInvestigationCapsuleStructure, validateLookupEvidenceArtifactStructure, validateOfflineArtifactStructure, validateSignedDigestArtifactStructure, } from "./artifact-structure.mjs";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { LOOKUP_CLAIM_PASSPORT_SCHEMA, LOOKUP_CLAIM_PASSPORT_VERSION, } from "../
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { SAVED_LOOKUP_SCHEMA, SAVED_LOOKUP_SCHEMA_VERSION, parseSavedLookupDocument, } from "./saved-lookup.mjs";
|
|
15
|
-
import { DOMAIN_CONTROL_MANIFEST_SCHEMA, DOMAIN_CONTROL_MANIFEST_VERSION, verifyDomainControlManifest, } from "../lib/domain-control-manifest.mjs";
|
|
16
|
-
import { DOMAIN_CONTROL_PASSPORT_VERSION } from "../frontend/src/lib/analysis/domain-control-manifest-core.js";
|
|
5
|
+
import { verifyCaseResponsePacketIntegrity, } from "../packages/cases/case-response-packet.mjs";
|
|
6
|
+
import { CASE_PORTABILITY_VERIFIER_DISPATCH, CASE_RESPONSE_PACKET_VERSION, CASE_RESPONSE_PACKET_SCHEMA, CLI_CASE_PACK_VERSION, CLI_CASE_PACK_SCHEMA, } from "../packages/contracts/case-portability.mjs";
|
|
7
|
+
import { ACQUISITION_DECISION_PACKET_SCHEMA, BULK_DOMAIN_COMPARISON_SCHEMA, BULK_MAIL_EXPOSURE_SCHEMA, BULK_REVIEW_MANIFEST_SCHEMA, LOOKUP_CLAIM_PASSPORT_SCHEMA, LOOKUP_CLAIM_PASSPORT_VERSION, SUPPORTED_ACQUISITION_DECISION_PACKET_VERSIONS, SUPPORTED_BULK_DOMAIN_COMPARISON_EXPORT_VERSIONS, SUPPORTED_BULK_MAIL_EXPOSURE_EXPORT_VERSIONS, SUPPORTED_BULK_REVIEW_MANIFEST_VERSIONS, } from "../packages/contracts/investigation-portability.mjs";
|
|
8
|
+
import { decryptWorkspaceArchive, inspectEncryptedWorkspaceArchive, isEncryptedWorkspaceArchive, } from "../packages/workspace/workspace-archive-crypto.mjs";
|
|
9
|
+
import { WORKSPACE_ARCHIVE_SCHEMA, previewWorkspaceArchive, readWorkspaceArchive, } from "../packages/workspace/workspace-archive.mjs";
|
|
10
|
+
import { canonicalArtifactJsonFor, resolveArtifactCanonicalization, sha256ArtifactDigestFor, SORTED_JSON_V2, } from "../packages/evidence/artifact-integrity.mjs";
|
|
11
|
+
import { SAVED_LOOKUP_SCHEMA, SUPPORTED_SAVED_LOOKUP_SCHEMA_VERSIONS, parseCliLookupDocument, } from "./saved-lookup.mjs";
|
|
12
|
+
import { DOMAIN_CONTROL_MANIFEST_SCHEMA, verifyDomainControlManifest, } from "../lib/domain-control-manifest.mjs";
|
|
13
|
+
import { DOMAIN_CONTROL_MANIFEST_CANONICALIZATION_ROUTES, } from "../packages/contracts/domain-control-manifest.mjs";
|
|
17
14
|
import { DOMAIN_CHANGE_PACKET_SCHEMA, DOMAIN_CHANGE_PACKET_VERSION, } from "../lib/domain-change-packet.mjs";
|
|
18
|
-
import { INVESTIGATION_CAPSULE_SCHEMA,
|
|
15
|
+
import { INVESTIGATION_CAPSULE_SCHEMA, SUPPORTED_INVESTIGATION_CAPSULE_VERSIONS, verifyInvestigationCapsule, } from "../packages/investigation/investigation-capsule.mjs";
|
|
19
16
|
import { INVESTIGATION_MANIFEST_SCHEMA, INVESTIGATION_MANIFEST_VERSION, } from "./investigation-manifest.mjs";
|
|
20
|
-
import {
|
|
17
|
+
import { verifyCliCasePack } from "./case-pack.mjs";
|
|
21
18
|
import { LOOKUP_EVIDENCE_PORTABLE_MAX_BYTES, LOOKUP_EVIDENCE_SCHEMA, SUPPORTED_LOOKUP_EVIDENCE_SCHEMA_VERSIONS, } from "../lib/evidence-export.mjs";
|
|
22
19
|
export const OFFLINE_ARTIFACT_VERIFICATION_SCHEMA = 'whoisleuth.offline-artifact-verification';
|
|
23
20
|
export const OFFLINE_ARTIFACT_VERIFICATION_VERSION = 3;
|
|
@@ -30,31 +27,62 @@ export class UnsupportedOfflineArtifactError extends TypeError {
|
|
|
30
27
|
this.name = 'UnsupportedOfflineArtifactError';
|
|
31
28
|
}
|
|
32
29
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
[
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
[
|
|
45
|
-
[
|
|
46
|
-
[
|
|
47
|
-
[
|
|
48
|
-
[
|
|
49
|
-
[DOMAIN_CONTROL_MANIFEST_SCHEMA]: [{ version: DOMAIN_CONTROL_PASSPORT_VERSION, canonicalization: SORTED_JSON_V1, explicit: true }, { version: DOMAIN_CONTROL_MANIFEST_VERSION, canonicalization: SORTED_JSON_V2, explicit: true }],
|
|
50
|
-
[DOMAIN_CHANGE_PACKET_SCHEMA]: [{ version: 1, canonicalization: SORTED_JSON_V1, explicit: false }, { version: DOMAIN_CHANGE_PACKET_VERSION, canonicalization: SORTED_JSON_V2, explicit: true }],
|
|
51
|
-
[INVESTIGATION_MANIFEST_SCHEMA]: [{ version: 1, canonicalization: SORTED_JSON_V1, explicit: false }, { version: INVESTIGATION_MANIFEST_VERSION, canonicalization: SORTED_JSON_V2, explicit: true }],
|
|
30
|
+
function currentCanonicalizationRoutes(versions) {
|
|
31
|
+
return Object.freeze(versions.map((version) => Object.freeze({
|
|
32
|
+
version,
|
|
33
|
+
canonicalization: SORTED_JSON_V2,
|
|
34
|
+
explicit: true,
|
|
35
|
+
})));
|
|
36
|
+
}
|
|
37
|
+
const SIGNED_ARTIFACT_ROUTES = Object.freeze({
|
|
38
|
+
[ACQUISITION_DECISION_PACKET_SCHEMA]: currentCanonicalizationRoutes(SUPPORTED_ACQUISITION_DECISION_PACKET_VERSIONS),
|
|
39
|
+
[LOOKUP_CLAIM_PASSPORT_SCHEMA]: currentCanonicalizationRoutes([LOOKUP_CLAIM_PASSPORT_VERSION]),
|
|
40
|
+
[BULK_DOMAIN_COMPARISON_SCHEMA]: currentCanonicalizationRoutes(SUPPORTED_BULK_DOMAIN_COMPARISON_EXPORT_VERSIONS),
|
|
41
|
+
[BULK_MAIL_EXPOSURE_SCHEMA]: currentCanonicalizationRoutes(SUPPORTED_BULK_MAIL_EXPOSURE_EXPORT_VERSIONS),
|
|
42
|
+
[BULK_REVIEW_MANIFEST_SCHEMA]: currentCanonicalizationRoutes(SUPPORTED_BULK_REVIEW_MANIFEST_VERSIONS),
|
|
43
|
+
[DOMAIN_CONTROL_MANIFEST_SCHEMA]: DOMAIN_CONTROL_MANIFEST_CANONICALIZATION_ROUTES,
|
|
44
|
+
[DOMAIN_CHANGE_PACKET_SCHEMA]: currentCanonicalizationRoutes([DOMAIN_CHANGE_PACKET_VERSION]),
|
|
45
|
+
[INVESTIGATION_MANIFEST_SCHEMA]: currentCanonicalizationRoutes([INVESTIGATION_MANIFEST_VERSION]),
|
|
52
46
|
});
|
|
53
47
|
function record(value) {
|
|
54
48
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
55
49
|
? value
|
|
56
50
|
: null;
|
|
57
51
|
}
|
|
52
|
+
function selectCasePortabilityVerifier(value) {
|
|
53
|
+
const packet = record(value.packet);
|
|
54
|
+
for (const descriptor of CASE_PORTABILITY_VERIFIER_DISPATCH) {
|
|
55
|
+
if (descriptor.id === 'case-response-packet'
|
|
56
|
+
&& descriptor.discriminator === 'root_schema'
|
|
57
|
+
&& value.schema === CASE_RESPONSE_PACKET_SCHEMA)
|
|
58
|
+
return descriptor;
|
|
59
|
+
if (descriptor.id === 'cli-case-pack'
|
|
60
|
+
&& descriptor.discriminator === 'packet_schema'
|
|
61
|
+
&& packet?.schema === CLI_CASE_PACK_SCHEMA)
|
|
62
|
+
return descriptor;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
function casePortabilityVersion(descriptor, value) {
|
|
67
|
+
if (descriptor.versionField === 'schemaVersion')
|
|
68
|
+
return artifactVersion(value);
|
|
69
|
+
const packet = record(value.packet);
|
|
70
|
+
if (!packet)
|
|
71
|
+
throw new TypeError('The Case portability verifier discriminator is missing.');
|
|
72
|
+
return artifactVersion(packet);
|
|
73
|
+
}
|
|
74
|
+
function supportsCasePortabilityVersion(descriptor, version) {
|
|
75
|
+
return descriptor.supportedVersions.some((candidate) => candidate === version);
|
|
76
|
+
}
|
|
77
|
+
function rejectUnsupportedCasePortabilityVersion(label, version, currentVersion) {
|
|
78
|
+
if (Number.isSafeInteger(version) && version >= 1 && version < currentVersion) {
|
|
79
|
+
throw new UnsupportedOfflineArtifactError(`${label} version ${String(version)} is retired. Export it again as version ${String(currentVersion)} with the last broad-reader release; no data was changed.`);
|
|
80
|
+
}
|
|
81
|
+
if (Number.isSafeInteger(version) && version > currentVersion) {
|
|
82
|
+
throw new UnsupportedOfflineArtifactError(`${label} version ${String(version)} is newer than the supported version ${String(currentVersion)}; no data was changed.`);
|
|
83
|
+
}
|
|
84
|
+
throw new UnsupportedOfflineArtifactError(`This ${label} version is malformed or unsupported; no data was changed.`);
|
|
85
|
+
}
|
|
58
86
|
function parseJson(raw) {
|
|
59
87
|
return parseBoundedJsonObject(raw, { maximumBytes: MAX_OFFLINE_ARTIFACT_BYTES });
|
|
60
88
|
}
|
|
@@ -115,11 +143,11 @@ async function archiveReport(raw, source, archive, encrypted, ciphertextBytes) {
|
|
|
115
143
|
schema: WORKSPACE_ARCHIVE_SCHEMA,
|
|
116
144
|
version: archive.sourceVersion,
|
|
117
145
|
}),
|
|
118
|
-
state: 'verified',
|
|
146
|
+
state: encrypted ? 'verified' : 'integrity_valid',
|
|
119
147
|
checks: Object.freeze({
|
|
120
148
|
structure: 'verified',
|
|
121
149
|
contentIntegrity: 'verified',
|
|
122
|
-
contentIntegrityScope: 'whole_artifact',
|
|
150
|
+
contentIntegrityScope: encrypted ? 'whole_artifact' : 'embedded_projections',
|
|
123
151
|
authenticatedEncryption: encrypted ? 'verified' : 'not_applicable',
|
|
124
152
|
}),
|
|
125
153
|
summary: Object.freeze({
|
|
@@ -135,7 +163,9 @@ async function archiveReport(raw, source, archive, encrypted, ciphertextBytes) {
|
|
|
135
163
|
fullyImportable,
|
|
136
164
|
}),
|
|
137
165
|
limitations: Object.freeze([
|
|
138
|
-
|
|
166
|
+
...(encrypted
|
|
167
|
+
? ['Authenticated decryption and archive validation cover the complete encrypted workspace content; they do not establish that the original observations were accurate or remain current.']
|
|
168
|
+
: ['Section digest verification covers the archive projections, not root metadata such as generation time or archive limitations. It does not establish that the original observations were accurate or remain current.']),
|
|
139
169
|
...(!fullyImportable
|
|
140
170
|
? ['One or more integrity-valid archive sections cannot be imported completely by this version. Inspect the archive before selecting data to restore.']
|
|
141
171
|
: []),
|
|
@@ -143,8 +173,8 @@ async function archiveReport(raw, source, archive, encrypted, ciphertextBytes) {
|
|
|
143
173
|
});
|
|
144
174
|
}
|
|
145
175
|
async function verifySignedArtifact(raw, value, schema, version) {
|
|
146
|
-
const
|
|
147
|
-
if (!
|
|
176
|
+
const routes = SIGNED_ARTIFACT_ROUTES[schema] ?? [];
|
|
177
|
+
if (!routes.some((route) => route.version === version)) {
|
|
148
178
|
throw new UnsupportedOfflineArtifactError('This signed review-artifact schema or version is not supported.');
|
|
149
179
|
}
|
|
150
180
|
validateSignedDigestArtifactStructure(schema, value);
|
|
@@ -155,7 +185,7 @@ async function verifySignedArtifact(raw, value, schema, version) {
|
|
|
155
185
|
|| !/^sha256:[a-f0-9]{64}$/u.test(integrity.digestSha256)) {
|
|
156
186
|
throw new TypeError('The signed review artefact has a missing or malformed integrity envelope.');
|
|
157
187
|
}
|
|
158
|
-
const canonicalization = resolveArtifactCanonicalization(version, integrity.canonicalization,
|
|
188
|
+
const canonicalization = resolveArtifactCanonicalization(version, integrity.canonicalization, routes, 'Signed review artefact');
|
|
159
189
|
const { integrity: _integrity, ...unsigned } = value;
|
|
160
190
|
if (await sha256ArtifactDigestFor(unsigned, canonicalization) !== integrity.digestSha256) {
|
|
161
191
|
throw new TypeError('The signed review artefact failed its SHA-256 integrity check.');
|
|
@@ -186,8 +216,12 @@ async function verifySignedArtifact(raw, value, schema, version) {
|
|
|
186
216
|
}
|
|
187
217
|
async function verifyOfflineArtifactCore(raw, options = {}) {
|
|
188
218
|
const value = parseJson(raw);
|
|
189
|
-
const
|
|
190
|
-
if (
|
|
219
|
+
const casePortabilityVerifier = selectCasePortabilityVerifier(value);
|
|
220
|
+
if (casePortabilityVerifier?.id === 'cli-case-pack') {
|
|
221
|
+
const casePackVersion = casePortabilityVersion(casePortabilityVerifier, value);
|
|
222
|
+
if (!supportsCasePortabilityVersion(casePortabilityVerifier, casePackVersion)) {
|
|
223
|
+
rejectUnsupportedCasePortabilityVersion('CLI Case-pack', casePackVersion, CLI_CASE_PACK_VERSION);
|
|
224
|
+
}
|
|
191
225
|
const verified = verifyCliCasePack(value);
|
|
192
226
|
return Object.freeze({
|
|
193
227
|
schema: OFFLINE_ARTIFACT_VERIFICATION_SCHEMA,
|
|
@@ -195,7 +229,7 @@ async function verifyOfflineArtifactCore(raw, options = {}) {
|
|
|
195
229
|
artifact: Object.freeze({
|
|
196
230
|
kind: 'cli_case_pack',
|
|
197
231
|
schema: CLI_CASE_PACK_SCHEMA,
|
|
198
|
-
version:
|
|
232
|
+
version: casePackVersion,
|
|
199
233
|
}),
|
|
200
234
|
state: 'verified',
|
|
201
235
|
checks: Object.freeze({
|
|
@@ -254,11 +288,11 @@ async function verifyOfflineArtifactCore(raw, options = {}) {
|
|
|
254
288
|
if (schema === WORKSPACE_ARCHIVE_SCHEMA) {
|
|
255
289
|
return archiveReport(raw, value, await readWorkspaceArchive(value), false, null);
|
|
256
290
|
}
|
|
257
|
-
if (
|
|
258
|
-
if (
|
|
259
|
-
|
|
291
|
+
if (casePortabilityVerifier?.id === 'case-response-packet') {
|
|
292
|
+
if (!supportsCasePortabilityVersion(casePortabilityVerifier, version)) {
|
|
293
|
+
rejectUnsupportedCasePortabilityVersion('case-response packet', version, CASE_RESPONSE_PACKET_VERSION);
|
|
260
294
|
}
|
|
261
|
-
validateOfflineArtifactStructure(
|
|
295
|
+
validateOfflineArtifactStructure(CASE_RESPONSE_PACKET_SCHEMA, value);
|
|
262
296
|
const integrity = record(value.integrity);
|
|
263
297
|
if (!integrity || !await verifyCaseResponsePacketIntegrity(value)) {
|
|
264
298
|
throw new TypeError('The case-response packet failed its manifest integrity check.');
|
|
@@ -266,7 +300,7 @@ async function verifyOfflineArtifactCore(raw, options = {}) {
|
|
|
266
300
|
return Object.freeze({
|
|
267
301
|
schema: OFFLINE_ARTIFACT_VERIFICATION_SCHEMA,
|
|
268
302
|
version: OFFLINE_ARTIFACT_VERIFICATION_VERSION,
|
|
269
|
-
artifact: Object.freeze({ kind: 'case_response_packet', schema, version }),
|
|
303
|
+
artifact: Object.freeze({ kind: 'case_response_packet', schema: CASE_RESPONSE_PACKET_SCHEMA, version }),
|
|
270
304
|
state: 'verified',
|
|
271
305
|
checks: Object.freeze({
|
|
272
306
|
structure: 'verified',
|
|
@@ -286,23 +320,22 @@ async function verifyOfflineArtifactCore(raw, options = {}) {
|
|
|
286
320
|
});
|
|
287
321
|
}
|
|
288
322
|
if (schema === INVESTIGATION_CAPSULE_SCHEMA) {
|
|
289
|
-
if (
|
|
323
|
+
if (!SUPPORTED_INVESTIGATION_CAPSULE_VERSIONS.some((candidate) => candidate === version)) {
|
|
290
324
|
throw new UnsupportedOfflineArtifactError('This investigation-capsule version is not supported.');
|
|
291
325
|
}
|
|
292
326
|
validateInvestigationCapsuleStructure(value);
|
|
293
327
|
const verification = await verifyInvestigationCapsule(value);
|
|
294
328
|
if (!verification.valid)
|
|
295
329
|
throw new TypeError('The investigation capsule failed its embedded projection integrity checks.');
|
|
296
|
-
const current = version === INVESTIGATION_CAPSULE_VERSION;
|
|
297
330
|
return Object.freeze({
|
|
298
331
|
schema: OFFLINE_ARTIFACT_VERIFICATION_SCHEMA,
|
|
299
332
|
version: OFFLINE_ARTIFACT_VERIFICATION_VERSION,
|
|
300
333
|
artifact: Object.freeze({ kind: 'investigation_capsule', schema, version }),
|
|
301
|
-
state:
|
|
334
|
+
state: 'verified',
|
|
302
335
|
checks: Object.freeze({
|
|
303
336
|
structure: 'verified',
|
|
304
337
|
contentIntegrity: 'verified',
|
|
305
|
-
contentIntegrityScope:
|
|
338
|
+
contentIntegrityScope: 'whole_artifact',
|
|
306
339
|
authenticatedEncryption: 'not_applicable',
|
|
307
340
|
}),
|
|
308
341
|
summary: Object.freeze({
|
|
@@ -312,9 +345,7 @@ async function verifyOfflineArtifactCore(raw, options = {}) {
|
|
|
312
345
|
ciphertextBytes: null,
|
|
313
346
|
}),
|
|
314
347
|
limitations: Object.freeze([
|
|
315
|
-
|
|
316
|
-
? ['The whole capsule matches its declared digest, including metadata and the linked source-contract projection digests. The non-embedded Lookup evidence remains linked by digest and must be retained separately.']
|
|
317
|
-
: ['The embedded brief, graph, and optional analyst-record projections match their declared digests. Capsule metadata and the linked Lookup evidence are outside those projection digests and must not be treated as whole-file integrity verified.']),
|
|
348
|
+
'The whole capsule matches its declared digest, including metadata and the linked source-contract projection digests. The non-embedded Lookup evidence remains linked by digest and must be retained separately.',
|
|
318
349
|
'Digest verification detects changed content but does not authenticate the analyst, signer, collection source, or truth of retained observations and assertions.',
|
|
319
350
|
]),
|
|
320
351
|
});
|
|
@@ -352,10 +383,10 @@ async function verifyOfflineArtifactCore(raw, options = {}) {
|
|
|
352
383
|
});
|
|
353
384
|
}
|
|
354
385
|
if (schema === SAVED_LOOKUP_SCHEMA) {
|
|
355
|
-
if (
|
|
386
|
+
if (!SUPPORTED_SAVED_LOOKUP_SCHEMA_VERSIONS.some((supported) => supported === version)) {
|
|
356
387
|
throw new UnsupportedOfflineArtifactError('This saved Lookup document version is not supported.');
|
|
357
388
|
}
|
|
358
|
-
|
|
389
|
+
parseCliLookupDocument(raw, { label: 'Saved Lookup artefact' });
|
|
359
390
|
return Object.freeze({
|
|
360
391
|
schema: OFFLINE_ARTIFACT_VERIFICATION_SCHEMA,
|
|
361
392
|
version: OFFLINE_ARTIFACT_VERIFICATION_VERSION,
|
|
@@ -374,7 +405,7 @@ async function verifyOfflineArtifactCore(raw, options = {}) {
|
|
|
374
405
|
ciphertextBytes: null,
|
|
375
406
|
}),
|
|
376
407
|
limitations: Object.freeze([
|
|
377
|
-
|
|
408
|
+
'The saved Lookup matches its versioned structural contract, but this document format has no embedded checksum or signature. Structural validity does not prove that its evidence is accurate, current, or unchanged since collection.',
|
|
378
409
|
]),
|
|
379
410
|
});
|
|
380
411
|
}
|
|
@@ -409,15 +440,13 @@ async function verifyManifestIdentity(artifactRaw, manifestRaw, entryId) {
|
|
|
409
440
|
throw new TypeError('The requested investigation manifest entry was not found.');
|
|
410
441
|
const artifactValue = parseJson(artifactRaw);
|
|
411
442
|
const metadata = artifactMetadata(artifactValue);
|
|
412
|
-
const manifestVersion = artifactVersion(manifest);
|
|
413
|
-
const canonicalization = manifestVersion === 1 ? SORTED_JSON_V1 : SORTED_JSON_V2;
|
|
414
443
|
const actualByteLength = inputBytes(artifactRaw);
|
|
415
444
|
const expectedByteLength = Number(item.byteLength);
|
|
416
445
|
const checks = Object.freeze({
|
|
417
446
|
manifestIntegrity: 'verified',
|
|
418
447
|
byteLength: actualByteLength === expectedByteLength ? 'verified' : 'mismatch',
|
|
419
448
|
rawContentDigest: rawContentDigest(artifactRaw) === item.contentDigestSha256 ? 'verified' : 'mismatch',
|
|
420
|
-
canonicalDigest: rawContentDigest(canonicalArtifactJsonFor(artifactValue,
|
|
449
|
+
canonicalDigest: rawContentDigest(canonicalArtifactJsonFor(artifactValue, SORTED_JSON_V2)) === item.canonicalDigestSha256 ? 'verified' : 'mismatch',
|
|
421
450
|
schema: metadata.schema === item.schema ? 'verified' : 'mismatch',
|
|
422
451
|
version: metadata.version === item.version ? 'verified' : 'mismatch',
|
|
423
452
|
});
|
|
@@ -437,7 +466,7 @@ async function verifyManifestIdentity(artifactRaw, manifestRaw, entryId) {
|
|
|
437
466
|
: 'mismatch';
|
|
438
467
|
return Object.freeze({
|
|
439
468
|
state,
|
|
440
|
-
manifest: Object.freeze({ schema: INVESTIGATION_MANIFEST_SCHEMA, version:
|
|
469
|
+
manifest: Object.freeze({ schema: INVESTIGATION_MANIFEST_SCHEMA, version: INVESTIGATION_MANIFEST_VERSION, entryId }),
|
|
441
470
|
checks,
|
|
442
471
|
expectedByteLength,
|
|
443
472
|
actualByteLength,
|
package/cli/bounded-json.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { MAX_BOUNDED_JSON_DEPTH, MAX_BOUNDED_JSON_KEYS, MAX_BOUNDED_JSON_VALUES, parseBoundedJsonObject, scanBoundedJson, } from "../lib/bounded-json.mjs";
|
|
1
|
+
export { MAX_BOUNDED_JSON_DEPTH, MAX_BOUNDED_JSON_KEYS, MAX_BOUNDED_JSON_VALUES, parseBoundedJson, parseBoundedJsonObject, scanBoundedJson, } from "../lib/bounded-json.mjs";
|
package/cli/bulk-checkpoint.mjs
CHANGED
|
@@ -2,11 +2,12 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { lstat } from 'node:fs/promises';
|
|
3
3
|
import { recordOrNull } from "../lib/bounded-contract-normalizers.mjs";
|
|
4
4
|
import { readBoundedRegularTextFile } from "../lib/bounded-file.mjs";
|
|
5
|
+
import { scanBoundedJson } from "../lib/bounded-json.mjs";
|
|
6
|
+
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
5
7
|
import { boundedCliInputError, CliUsageError } from "./errors.mjs";
|
|
6
8
|
import { writePrivateFile } from "./output-file.mjs";
|
|
7
9
|
export const CLI_BULK_CHECKPOINT_SCHEMA = 'whoisleuth.cli.bulk-checkpoint';
|
|
8
10
|
export const CLI_BULK_CHECKPOINT_VERSION = 2;
|
|
9
|
-
const SUPPORTED_BULK_CHECKPOINT_VERSIONS = new Set([1, CLI_BULK_CHECKPOINT_VERSION]);
|
|
10
11
|
export const MAX_BULK_CHECKPOINT_BYTES = 16 * 1024 * 1024;
|
|
11
12
|
const MAX_CHECKPOINT_JSON_DEPTH = 12;
|
|
12
13
|
const MAX_CHECKPOINT_OBJECT_KEYS = 256;
|
|
@@ -19,8 +20,8 @@ function checkpointDigest(queries, deep) {
|
|
|
19
20
|
.update(queries.join('\n'))
|
|
20
21
|
.digest('hex');
|
|
21
22
|
}
|
|
22
|
-
function
|
|
23
|
-
return
|
|
23
|
+
function normalizedTimestamp(value) {
|
|
24
|
+
return normalizeExplicitIsoTimestamp(value);
|
|
24
25
|
}
|
|
25
26
|
function isBoundedCheckpointJson(value, depth = 0) {
|
|
26
27
|
if (depth > MAX_CHECKPOINT_JSON_DEPTH)
|
|
@@ -53,7 +54,7 @@ function normalizeCompactResult(value) {
|
|
|
53
54
|
return null;
|
|
54
55
|
return result;
|
|
55
56
|
}
|
|
56
|
-
function normalizeCheckpointResult(value, queries, classifyQuery
|
|
57
|
+
function normalizeCheckpointResult(value, queries, classifyQuery) {
|
|
57
58
|
const item = recordOrNull(value);
|
|
58
59
|
const index = item?.index;
|
|
59
60
|
if (!Number.isSafeInteger(index) || Number(index) < 0 || Number(index) >= queries.length)
|
|
@@ -61,11 +62,9 @@ function normalizeCheckpointResult(value, queries, classifyQuery, version = CLI_
|
|
|
61
62
|
const query = queries[Number(index)];
|
|
62
63
|
if (typeof item?.query !== 'string' || item.query !== query)
|
|
63
64
|
return null;
|
|
64
|
-
const observedAt =
|
|
65
|
-
?
|
|
66
|
-
|
|
67
|
-
: validTimestamp(item.observedAt) ? new Date(item.observedAt).toISOString() : undefined
|
|
68
|
-
: null;
|
|
65
|
+
const observedAt = item.observedAt === null
|
|
66
|
+
? null
|
|
67
|
+
: normalizedTimestamp(item.observedAt) ?? undefined;
|
|
69
68
|
if (observedAt === undefined)
|
|
70
69
|
return null;
|
|
71
70
|
if (item.ok === false) {
|
|
@@ -86,33 +85,37 @@ function parseBulkCheckpoint(text, options) {
|
|
|
86
85
|
if (typeof text !== 'string' || Buffer.byteLength(text, 'utf8') > MAX_BULK_CHECKPOINT_BYTES) {
|
|
87
86
|
throw new CliUsageError(`Bulk checkpoint input is limited to ${MAX_BULK_CHECKPOINT_BYTES} bytes.`);
|
|
88
87
|
}
|
|
88
|
+
const normalized = text.replace(/^\uFEFF/u, '');
|
|
89
89
|
let parsed;
|
|
90
90
|
try {
|
|
91
|
-
|
|
91
|
+
scanBoundedJson(normalized);
|
|
92
|
+
parsed = JSON.parse(normalized);
|
|
92
93
|
}
|
|
93
94
|
catch {
|
|
94
|
-
throw new CliUsageError('Bulk checkpoint must be valid JSON.');
|
|
95
|
+
throw new CliUsageError('Bulk checkpoint must be valid bounded JSON without duplicate keys.');
|
|
95
96
|
}
|
|
96
97
|
const document = recordOrNull(parsed);
|
|
97
98
|
const expectedDigest = checkpointDigest(options.queries, options.deep);
|
|
98
99
|
const version = typeof document?.version === 'number' && Number.isSafeInteger(document.version)
|
|
99
100
|
? document.version
|
|
100
101
|
: 0;
|
|
101
|
-
if (document?.schema !== CLI_BULK_CHECKPOINT_SCHEMA ||
|
|
102
|
-
throw new CliUsageError(`Bulk checkpoint must use ${CLI_BULK_CHECKPOINT_SCHEMA} version
|
|
102
|
+
if (document?.schema !== CLI_BULK_CHECKPOINT_SCHEMA || version !== CLI_BULK_CHECKPOINT_VERSION) {
|
|
103
|
+
throw new CliUsageError(`Bulk checkpoint must use ${CLI_BULK_CHECKPOINT_SCHEMA} version ${CLI_BULK_CHECKPOINT_VERSION}.`);
|
|
103
104
|
}
|
|
104
105
|
if (document.mode !== (options.deep ? 'deep' : 'fast')
|
|
105
106
|
|| document.inputDigestSha256 !== expectedDigest
|
|
106
107
|
|| document.queryCount !== options.queries.length) {
|
|
107
108
|
throw new CliUsageError('Bulk checkpoint does not match the current input or scan mode.');
|
|
108
109
|
}
|
|
109
|
-
|
|
110
|
+
const startedAt = normalizedTimestamp(document.startedAt);
|
|
111
|
+
const updatedAt = normalizedTimestamp(document.updatedAt);
|
|
112
|
+
if (!startedAt || !updatedAt || !Array.isArray(document.results)) {
|
|
110
113
|
throw new CliUsageError('Bulk checkpoint metadata is invalid.');
|
|
111
114
|
}
|
|
112
115
|
const results = [];
|
|
113
116
|
const seen = new Set();
|
|
114
117
|
for (const candidate of document.results.slice(0, options.queries.length + 1)) {
|
|
115
|
-
const result = normalizeCheckpointResult(candidate, options.queries, options.classifyQuery
|
|
118
|
+
const result = normalizeCheckpointResult(candidate, options.queries, options.classifyQuery);
|
|
116
119
|
if (!result || seen.has(result.index))
|
|
117
120
|
throw new CliUsageError('Bulk checkpoint contains an invalid or duplicate result.');
|
|
118
121
|
seen.add(result.index);
|
|
@@ -124,8 +127,8 @@ function parseBulkCheckpoint(text, options) {
|
|
|
124
127
|
mode: options.deep ? 'deep' : 'fast',
|
|
125
128
|
inputDigestSha256: expectedDigest,
|
|
126
129
|
queryCount: options.queries.length,
|
|
127
|
-
startedAt
|
|
128
|
-
updatedAt
|
|
130
|
+
startedAt,
|
|
131
|
+
updatedAt,
|
|
129
132
|
results: results.sort((left, right) => left.index - right.index),
|
|
130
133
|
};
|
|
131
134
|
}
|
|
@@ -164,14 +167,20 @@ async function createBulkCheckpointWriter(options) {
|
|
|
164
167
|
const resumed = options.resume
|
|
165
168
|
? parseBulkCheckpoint(await readCheckpointFile(options.path), options)
|
|
166
169
|
: null;
|
|
167
|
-
const
|
|
170
|
+
const currentTimestamp = () => {
|
|
171
|
+
const timestamp = normalizedTimestamp(now());
|
|
172
|
+
if (!timestamp)
|
|
173
|
+
throw new CliUsageError('Bulk checkpoint collection time is invalid.');
|
|
174
|
+
return timestamp;
|
|
175
|
+
};
|
|
176
|
+
const startedAt = resumed?.startedAt || currentTimestamp();
|
|
168
177
|
const results = new Map((resumed?.results || []).map((item) => [item.index, item]));
|
|
169
178
|
let activeWrite = null;
|
|
170
179
|
let dirty = false;
|
|
171
180
|
let writeFailure = null;
|
|
172
181
|
let created = resumed !== null;
|
|
173
182
|
function document() {
|
|
174
|
-
const updatedAt =
|
|
183
|
+
const updatedAt = currentTimestamp();
|
|
175
184
|
return {
|
|
176
185
|
schema: CLI_BULK_CHECKPOINT_SCHEMA,
|
|
177
186
|
version: CLI_BULK_CHECKPOINT_VERSION,
|
|
@@ -226,7 +235,11 @@ async function createBulkCheckpointWriter(options) {
|
|
|
226
235
|
return Object.freeze({
|
|
227
236
|
initialResults: Object.freeze([...results.values()].sort((left, right) => left.index - right.index)),
|
|
228
237
|
record(result) {
|
|
229
|
-
const observedAt =
|
|
238
|
+
const observedAt = normalizedTimestamp(result.observedAt) ?? normalizedTimestamp(now());
|
|
239
|
+
if (!observedAt) {
|
|
240
|
+
writeFailure ||= new CliUsageError('Bulk checkpoint collection time is invalid.');
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
230
243
|
const normalized = normalizeCheckpointResult({ ...result, observedAt }, options.queries, options.classifyQuery);
|
|
231
244
|
if (!normalized) {
|
|
232
245
|
writeFailure ||= new CliUsageError('Bulk checkpoint result is not a bounded compact lookup result.');
|
package/cli/bulk.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
2
|
import { abortable } from "../lib/abort.mjs";
|
|
3
|
+
import { decodeBoundedUtf8 } from "../lib/bounded-file.mjs";
|
|
3
4
|
import { CliUsageError } from "./arguments.mjs";
|
|
5
|
+
import { hasUnsafeCliText } from "./errors.mjs";
|
|
4
6
|
import { MAX_DEEP_BULK_QUERIES, MAX_FAST_BULK_QUERIES, } from "../lib/bulk-limits.mjs";
|
|
5
7
|
const MAX_BULK_INPUT_BYTES = 1024 * 1024;
|
|
6
8
|
async function readTextStreamBounded(stream, limit = MAX_BULK_INPUT_BYTES, signal) {
|
|
@@ -26,7 +28,12 @@ async function readTextStreamBounded(stream, limit = MAX_BULK_INPUT_BYTES, signa
|
|
|
26
28
|
finally {
|
|
27
29
|
signal?.removeEventListener('abort', abort);
|
|
28
30
|
}
|
|
29
|
-
|
|
31
|
+
try {
|
|
32
|
+
return decodeBoundedUtf8(Buffer.concat(chunks), 'Bulk input');
|
|
33
|
+
}
|
|
34
|
+
catch (cause) {
|
|
35
|
+
throw new CliUsageError(cause instanceof Error ? cause.message : 'Bulk input must contain valid UTF-8 text.');
|
|
36
|
+
}
|
|
30
37
|
}
|
|
31
38
|
function parseBulkQueries(text, { deep = false } = {}) {
|
|
32
39
|
if (typeof text !== 'string')
|
|
@@ -43,7 +50,7 @@ function parseBulkQueries(text, { deep = false } = {}) {
|
|
|
43
50
|
const query = line.trim();
|
|
44
51
|
if (!query)
|
|
45
52
|
continue;
|
|
46
|
-
if (query.length > 1024 ||
|
|
53
|
+
if (query.length > 1024 || hasUnsafeCliText(query)) {
|
|
47
54
|
throw new CliUsageError('Bulk input contains an overlong query or unsupported control character.');
|
|
48
55
|
}
|
|
49
56
|
const key = query.toLowerCase();
|