@slicedearth/whoisleuth-cli 2.4.0 → 2.5.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/NOTICE +2 -2
- package/cli/argument-values.mjs +55 -0
- package/cli/arguments.mjs +24 -470
- package/cli/artifact-validation/case-response.mjs +21 -6
- package/cli/artifact-validation/lookup-evidence.mjs +3 -3
- package/cli/artifact-validation/signed-review.mjs +59 -13
- package/cli/artifact-verify.mjs +2 -0
- 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 +2 -2
- package/cli/bulk.mjs +2 -3
- package/cli/case-command.mjs +20 -3
- package/cli/case-pack.mjs +12 -2
- 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 +329 -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 +3 -3
- 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/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/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 +33 -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 +107 -0
- package/cli/interchange-report.mjs +4 -2
- package/cli/investigation-artifacts.mjs +1 -1
- 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 +197 -17
- package/cli/lookalike-calibration.mjs +1 -1
- package/cli/lookup-brief.mjs +2 -2
- package/cli/lookup-command-runner.mjs +18 -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 +39 -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 +131 -0
- package/cli/review-command-definitions.mjs +273 -0
- package/cli/review-command-runner.mjs +68 -158
- package/cli/risk-calibration.mjs +8 -8
- package/cli/runner.mjs +20 -32
- package/cli/saved-lookup.mjs +2 -2
- package/cli/selected-input-worker.mjs +63 -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 +2 -2
- package/cli/workflow-arguments.mjs +86 -0
- package/cli/workflow-command-definitions.mjs +78 -0
- package/cli/workflow-command-runner.mjs +8 -1
- package/docs/cli-reference.md +27 -0
- package/docs/cli.md +223 -2
- package/docs/contextual-reviews.md +155 -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/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 +12 -8
- package/lib/dns-inheritance-review.mjs +53 -63
- package/lib/dns-intelligence.mjs +9 -7
- 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 +22 -5
- package/lib/domain-posture.mjs +16 -15
- package/lib/domain-registration.mjs +392 -0
- package/lib/evidence-export-privacy.mjs +22 -15
- package/lib/evidence-export.mjs +3 -3
- package/lib/evidence-report.mjs +4 -1
- package/lib/external-intelligence-risk.mjs +2 -124
- 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 +7 -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-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 +69 -40
- package/lib/lookup-response.mjs +5 -15
- package/lib/lookup-source-progress.mjs +8 -22
- package/lib/lookup.mjs +1 -1
- 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 +27 -0
- package/lib/smtp-transport-review.mjs +48 -19
- package/lib/sslbl-certificates.generated.mjs +141 -139
- package/lib/static-html-analysis.mjs +1 -1
- package/lib/structured-data-identity.mjs +1 -1
- 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/typosquat-generator.mjs +11 -2
- package/lib/website-enrichment.mjs +388 -0
- package/lib/whois-authority.mjs +43 -5
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-parser.mjs +5 -3
- package/package.json +13 -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 +345 -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-evidence-links.mjs +135 -0
- package/packages/cases/case-evidence-model.mjs +78 -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-recheck-model.mjs +90 -32
- 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 +40 -112
- package/packages/cases/case-record-projection.mjs +8 -1
- package/packages/cases/case-record-version-input.mjs +19 -0
- package/packages/cases/case-report.mjs +63 -27
- package/packages/cases/case-requested-evidence.mjs +146 -0
- package/packages/cases/case-response-actions.mjs +103 -1
- package/packages/cases/case-response-model.mjs +9 -1
- package/packages/cases/case-response-outcomes.mjs +29 -8
- package/packages/cases/case-response-packet-render.mjs +13 -9
- package/packages/cases/case-response-packet.mjs +25 -11
- package/packages/cases/case-response-records.mjs +0 -1
- package/packages/cases/case-response-review-inputs.mjs +14 -3
- package/packages/cases/case-storage-model.mjs +1 -1
- package/packages/cases/case-workflow-metadata.mjs +225 -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 +28 -10
- package/packages/contracts/case-portability.mjs +118 -25
- package/packages/contracts/cli-command-semantics.mjs +2 -0
- package/packages/contracts/cli-grammar.mjs +6 -0
- package/packages/contracts/cli-lookup-brief.mjs +2 -0
- package/packages/contracts/context-review.mjs +24 -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/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/mail-authentication.mjs +5 -0
- package/packages/contracts/message-intake.mjs +31 -0
- package/packages/contracts/offline-comparison.mjs +4 -1
- package/packages/contracts/passive-mail.mjs +10 -0
- package/packages/contracts/web-capture.mjs +2 -2
- package/packages/contracts/workspace-portability.mjs +63 -17
- package/packages/evidence/collection-quality.mjs +67 -0
- package/packages/evidence/dns-query-outcome.mjs +72 -0
- package/packages/evidence/email-recipient.mjs +18 -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/interchange/defensive-indicator-export.mjs +264 -0
- package/packages/interchange/external-findings-import.mjs +4 -12
- package/packages/interchange/managed-indicator-export.mjs +35 -0
- package/packages/interchange/managed-indicator-set.mjs +210 -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 +1 -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 +81 -0
- package/packages/investigation/docx-intake.mjs +133 -0
- package/packages/investigation/domain-history-review.mjs +100 -0
- package/packages/investigation/domain-transition-review.mjs +47 -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/intake-report.mjs +19 -0
- package/packages/investigation/investigation-manifest.mjs +2 -2
- package/packages/investigation/link-intake.mjs +179 -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 +195 -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/workspace/brand-profile-model.mjs +2 -2
- package/packages/workspace/bulk-review-model.mjs +28 -26
- package/packages/workspace/bulk-session-model.mjs +23 -13
- package/packages/workspace/campaign-model.mjs +1 -1
- package/packages/workspace/detection-rule-model.mjs +1 -1
- 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 +14 -5
- package/packages/workspace/watchlist-history.mjs +28 -12
- package/packages/workspace/watchlist-store.mjs +17 -5
- 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,18 @@
|
|
|
1
|
+
import { SUPPORT_COMMAND_DEFINITIONS } from "./support-command-definitions.mjs";
|
|
2
|
+
import { ASSURANCE_COMMAND_DEFINITIONS } from "./assurance-command-definitions.mjs";
|
|
3
|
+
import { COLLECTION_COMMAND_DEFINITIONS } from "./collection-command-definitions.mjs";
|
|
4
|
+
import { NETWORK_COMMAND_DEFINITIONS } from "./network-command-definitions.mjs";
|
|
5
|
+
import { REVIEW_COMMAND_DEFINITIONS } from "./review-command-definitions.mjs";
|
|
6
|
+
import { EVIDENCE_COMMAND_DEFINITIONS } from "./evidence-command-definitions.mjs";
|
|
7
|
+
import { WORKFLOW_COMMAND_DEFINITIONS } from "./workflow-command-definitions.mjs";
|
|
8
|
+
import { HISTORY_COMMAND_DEFINITIONS } from "./history-command-definitions.mjs";
|
|
9
|
+
export const COMMAND_SEEDS = Object.freeze({
|
|
10
|
+
...SUPPORT_COMMAND_DEFINITIONS,
|
|
11
|
+
...ASSURANCE_COMMAND_DEFINITIONS,
|
|
12
|
+
...COLLECTION_COMMAND_DEFINITIONS,
|
|
13
|
+
...NETWORK_COMMAND_DEFINITIONS,
|
|
14
|
+
...REVIEW_COMMAND_DEFINITIONS,
|
|
15
|
+
...EVIDENCE_COMMAND_DEFINITIONS,
|
|
16
|
+
...WORKFLOW_COMMAND_DEFINITIONS,
|
|
17
|
+
...HISTORY_COMMAND_DEFINITIONS,
|
|
18
|
+
});
|
package/cli/compare.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CliUsageError } from "./
|
|
1
|
+
import { CliUsageError } from "./errors.mjs";
|
|
2
2
|
import { isRdapRegistryAccessProfile, isWhoisRegistryAccessProfile, } from "./registry-access.mjs";
|
|
3
3
|
import { MAX_SAVED_LOOKUP_INPUT_BYTES, MAX_SAVED_LOOKUP_STRING_LENGTH, SAVED_LOOKUP_SCHEMA, SAVED_LOOKUP_SCHEMA_VERSION, parseSavedLookupDocument, readSavedLookupInputBounded, } from "./saved-lookup.mjs";
|
|
4
4
|
const MAX_COMPARE_INPUT_BYTES = MAX_SAVED_LOOKUP_INPUT_BYTES;
|
package/cli/config-profile.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { join, resolve } from 'node:path';
|
|
|
4
4
|
import { CliUsageError } from "./errors.mjs";
|
|
5
5
|
import { cliInvocationOptionIndices, cliMetaActionForInvocation } from "./command-reference.mjs";
|
|
6
6
|
import { readBoundedRegularTextFile } from "../lib/bounded-file.mjs";
|
|
7
|
-
import { scanBoundedJson } from "../
|
|
7
|
+
import { scanBoundedJson } from "../packages/analysis/bounded-json.mjs";
|
|
8
8
|
export const CLI_CONFIG_SCHEMA = 'whoisleuth.cli.config';
|
|
9
9
|
export const CLI_CONFIG_VERSION = 1;
|
|
10
10
|
export const MAX_CLI_CONFIG_BYTES = 64 * 1024;
|
package/cli/ct-event-intake.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { domainToASCII } from 'node:url';
|
|
3
|
-
import { exactKeys, requireBoundedString, requireIsoTimestamp, requireRecord, } from "../lib/bounded-contract-normalizers.mjs";
|
|
3
|
+
import { compareCodeUnits, exactKeys, requireBoundedString, requireIsoTimestamp, requireRecord, } from "../lib/bounded-contract-normalizers.mjs";
|
|
4
4
|
import { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, MAX_EXTERNAL_FINDINGS, MAX_EXTERNAL_FINDING_DOMAINS, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, MAX_EXTERNAL_FINDING_LIMITATIONS, MAX_EXTERNAL_FINDING_LIMITATION_LENGTH, retainExternalFindingLimitations, } from "../packages/interchange/external-findings-import.mjs";
|
|
5
5
|
export const CT_EVENT_BATCH_SCHEMA = 'whoisleuth.ct-event-batch';
|
|
6
6
|
export const CT_EVENT_BATCH_VERSION = 1;
|
|
@@ -118,9 +118,9 @@ export function buildCtEventFindings(inputRaw) {
|
|
|
118
118
|
});
|
|
119
119
|
const seen = new Set();
|
|
120
120
|
const unique = candidates
|
|
121
|
-
.sort((left, right) => left.domain
|
|
122
|
-
|| left.observedAt
|
|
123
|
-
|| left.structuredObservation.value
|
|
121
|
+
.sort((left, right) => compareCodeUnits(left.domain, right.domain)
|
|
122
|
+
|| compareCodeUnits(left.observedAt, right.observedAt)
|
|
123
|
+
|| compareCodeUnits(left.structuredObservation.value, right.structuredObservation.value))
|
|
124
124
|
.filter((item) => {
|
|
125
125
|
const key = `${item.domain}\u0000${item.observedAt}\u0000${item.structuredObservation.value}\u0000${item.structuredObservation.eventId}`;
|
|
126
126
|
if (seen.has(key))
|
package/cli/discover.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
2
|
import { decodeBoundedUtf8 } from "../lib/bounded-file.mjs";
|
|
3
|
-
import { CliUsageError } from "./
|
|
4
|
-
import { normalizeDiscoverySuffix } from "../
|
|
3
|
+
import { CliUsageError } from "./errors.mjs";
|
|
4
|
+
import { normalizeDiscoverySuffix } from "../packages/analysis/registrable-domain.mjs";
|
|
5
5
|
const DEFAULT_DISCOVERY_TLDS = Object.freeze(['com', 'net', 'org']);
|
|
6
6
|
const MAX_DISCOVERY_TLD_TEXT_LENGTH = 1024;
|
|
7
7
|
const MAX_DISCOVERY_TLD_TOKENS_INSPECTED = 80;
|
|
@@ -2,9 +2,9 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { availabilityState, bulkDnsSummary } from "./bulk-output.mjs";
|
|
3
3
|
import { boundedCliInputError, CliUsageError } from "./errors.mjs";
|
|
4
4
|
import { writePrivateFile } from "./output-file.mjs";
|
|
5
|
-
import { isValidAsciiDomainName } from "../
|
|
5
|
+
import { isValidAsciiDomainName } from "../packages/contracts/domain-name.mjs";
|
|
6
6
|
import { readBoundedRegularTextFile } from "../lib/bounded-file.mjs";
|
|
7
|
-
import { scanBoundedJson } from "../
|
|
7
|
+
import { scanBoundedJson } from "../packages/analysis/bounded-json.mjs";
|
|
8
8
|
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
9
9
|
export const CLI_DISCOVERY_OBSERVATION_SCHEMA = 'whoisleuth.cli.discovery-observation-snapshot';
|
|
10
10
|
export const CLI_DISCOVERY_OBSERVATION_VERSION = 2;
|
|
@@ -23,7 +23,7 @@ async function readAllowlist(source, dependencies, context, classify) {
|
|
|
23
23
|
catch (error) {
|
|
24
24
|
if (error instanceof CliUsageError)
|
|
25
25
|
throw error;
|
|
26
|
-
throw new CliUsageError(`Could not read discovery scan allowlist: ${boundedCliErrorMessage(error, 'Input could not be read')}
|
|
26
|
+
throw new CliUsageError(`Could not read discovery scan allowlist: ${boundedCliErrorMessage(error, 'Input could not be read')}`, 'input_unavailable');
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
async function runDiscoveryScanCommand(args, dependencies, context) {
|
|
@@ -52,7 +52,7 @@ async function runDiscoveryScanCommand(args, dependencies, context) {
|
|
|
52
52
|
const queries = candidates.map((candidate) => String(candidate.domain));
|
|
53
53
|
if (args.plan) {
|
|
54
54
|
const document = buildCollectionPreflight({
|
|
55
|
-
command: 'discover-scan', targetCount: queries.length,
|
|
55
|
+
command: 'discover-scan', targetCount: queries.length,
|
|
56
56
|
deep: args.deep, concurrency: args.concurrency, output: args.output, checkpoint: false,
|
|
57
57
|
customResolvers: Boolean(args.resolverText), allowlist: Boolean(args.allowlistSource),
|
|
58
58
|
});
|
|
@@ -86,7 +86,7 @@ async function runDiscoveryScanCommand(args, dependencies, context) {
|
|
|
86
86
|
throw new CliUsageError('Bulk checkpoint collection context does not match the current DNS resolver selection.');
|
|
87
87
|
}
|
|
88
88
|
if (item.observedAt === null || item.observedAt === undefined) {
|
|
89
|
-
throw new CliUsageError('Discovery resume requires an observation time for every retained checkpoint result.');
|
|
89
|
+
throw new CliUsageError('Discovery resume requires an observation time for every retained checkpoint result.', 'missing_input');
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
const indicator = context.beginProgress(`Collecting 0 of ${queries.length} generated candidates`);
|
package/cli/discovery-scan.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { Buffer } from 'node:buffer';
|
|
3
3
|
import { decodeBoundedUtf8 } from "../lib/bounded-file.mjs";
|
|
4
|
-
import { MAX_DISCOVERY_SUFFIX_LENGTH } from "../
|
|
4
|
+
import { MAX_DISCOVERY_SUFFIX_LENGTH } from "../packages/analysis/registrable-domain.mjs";
|
|
5
5
|
import { runBulkLookups } from "./bulk.mjs";
|
|
6
6
|
import { REGISTERED_STATES, BULK_CSV_METADATA_COLUMNS, availabilityState, bulkCsvMetadataValues, bulkDnsSummary } from "./bulk-output.mjs";
|
|
7
7
|
import { cliCsvCell } from "./csv.mjs";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { boundedCliInputError, CliUsageError } from "./errors.mjs";
|
|
3
3
|
import { writePrivateFile } from "./output-file.mjs";
|
|
4
|
-
import { isValidAsciiDomainName } from "../
|
|
4
|
+
import { isValidAsciiDomainName } from "../packages/contracts/domain-name.mjs";
|
|
5
5
|
import { readBoundedRegularTextFile } from "../lib/bounded-file.mjs";
|
|
6
|
-
import { scanBoundedJson } from "../
|
|
6
|
+
import { scanBoundedJson } from "../packages/analysis/bounded-json.mjs";
|
|
7
7
|
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
8
8
|
export const CLI_DISCOVERY_SNAPSHOT_SCHEMA = 'whoisleuth.cli.discovery-snapshot';
|
|
9
9
|
export const CLI_DISCOVERY_SNAPSHOT_VERSION = 1;
|
|
@@ -4,7 +4,7 @@ import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
|
|
|
4
4
|
async function generateDiscoveryCandidates(args, dependencies, context) {
|
|
5
5
|
const seed = args.seed || await context.readSingleInput();
|
|
6
6
|
if (!seed)
|
|
7
|
-
throw new CliUsageError('discover requires one brand label or domain as an argument or on stdin.');
|
|
7
|
+
throw new CliUsageError('discover requires one brand label or domain as an argument or on stdin.', 'missing_input');
|
|
8
8
|
const loadGenerator = dependencies.loadTyposquatGenerator || (() => import("../lib/typosquat-generator.mjs"));
|
|
9
9
|
const generator = await loadGenerator();
|
|
10
10
|
const tlds = normalizeDiscoveryTlds(args.tldText || DEFAULT_DISCOVERY_TLDS.join(','), generator.MAX_GENERATION_TLDS);
|
|
@@ -23,7 +23,7 @@ async function generateDiscoveryCandidates(args, dependencies, context) {
|
|
|
23
23
|
if (args.preset === 'custom'
|
|
24
24
|
&& !mutationFamilies.includes('dictionary')
|
|
25
25
|
&& !mutationFamilies.includes('dictionary_token_replacement')) {
|
|
26
|
-
throw new CliUsageError('--dictionary requires a dictionary mutation family.');
|
|
26
|
+
throw new CliUsageError('--dictionary requires a dictionary mutation family.', 'conflicting_options');
|
|
27
27
|
}
|
|
28
28
|
try {
|
|
29
29
|
dictionaryText = dependencies.readDiscoveryDictionary
|
|
@@ -33,11 +33,11 @@ async function generateDiscoveryCandidates(args, dependencies, context) {
|
|
|
33
33
|
catch (error) {
|
|
34
34
|
if (error instanceof CliUsageError)
|
|
35
35
|
throw error;
|
|
36
|
-
throw new CliUsageError(`Could not read discovery dictionary: ${boundedCliErrorMessage(error, 'Input could not be read')}
|
|
36
|
+
throw new CliUsageError(`Could not read discovery dictionary: ${boundedCliErrorMessage(error, 'Input could not be read')}`, 'input_unavailable');
|
|
37
37
|
}
|
|
38
38
|
const normalizedDictionary = generator.normalizeCustomDictionaryTerms(dictionaryText);
|
|
39
39
|
if (!normalizedDictionary.values.length) {
|
|
40
|
-
throw new CliUsageError('The discovery dictionary did not contain any valid terms.');
|
|
40
|
+
throw new CliUsageError('The discovery dictionary did not contain any valid terms.', 'missing_input');
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
const result = generator.generateTyposquatCandidateSet(seed, tlds, {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { classifyQuery } from "../lib/classify.mjs";
|
|
2
|
-
import { scanBoundedJson } from "../
|
|
2
|
+
import { scanBoundedJson } from "../packages/analysis/bounded-json.mjs";
|
|
3
3
|
import { buildDomainControlFlightRecorder, validateDomainControlFlightRecorderDocument, normalizeDomainControlFlightRecorderObservations, } from "../lib/domain-control-flight-recorder.mjs";
|
|
4
|
-
import { DOMAIN_CONTROL_FLIGHT_RECORDER_SCHEMA, DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA, DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION,
|
|
5
|
-
import { DOMAIN_CONTROL_REVIEW_SCHEMA,
|
|
6
|
-
import { CLI_DOMAIN_CONTROL_MONITOR_SCHEMA, CLI_DOMAIN_CONTROL_MONITOR_VERSION, PUBLIC_CLI_DOMAIN_CONTROL_MONITOR_VERSION, SUPPORTED_CLI_DOMAIN_CONTROL_MONITOR_VERSIONS, DOMAIN_CONTROL_MONITOR_COLLECTION_KEYS, DOMAIN_CONTROL_MONITOR_FAILURE_CATEGORIES, DOMAIN_CONTROL_MONITOR_FAILURE_KEYS, DOMAIN_CONTROL_MONITOR_LIMITATIONS, DOMAIN_CONTROL_MONITOR_MANIFEST_KEYS, DOMAIN_CONTROL_MONITOR_ROOT_KEYS, MAX_DOMAIN_CONTROL_MONITOR_CONCURRENCY, MAX_DOMAIN_CONTROL_MONITOR_DOMAINS as CONTRACT_MAX_DOMAIN_CONTROL_MONITOR_DOMAINS, MAX_DOMAIN_CONTROL_MONITOR_ERROR_LENGTH, MAX_DOMAIN_CONTROL_MONITOR_FAILURES, MAX_DOMAIN_CONTROL_MONITOR_INPUT_BYTES, MAX_DOMAIN_CONTROL_MONITOR_JSON_CONTAINER_ITEMS, MAX_DOMAIN_CONTROL_MONITOR_JSON_DEPTH, MAX_DOMAIN_CONTROL_MONITOR_JSON_KEYS, MAX_DOMAIN_CONTROL_MONITOR_JSON_VALUES, MIN_DOMAIN_CONTROL_MONITOR_CONCURRENCY, MIN_DOMAIN_CONTROL_MONITOR_DOMAINS, } from "../packages/contracts/domain-control-monitor.mjs";
|
|
4
|
+
import { DOMAIN_CONTROL_FLIGHT_RECORDER_SCHEMA, DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA, DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION, } from "../packages/contracts/domain-control-flight-recorder.mjs";
|
|
5
|
+
import { DOMAIN_CONTROL_REVIEW_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_VERSION, } from "../packages/contracts/domain-control-review.mjs";
|
|
6
|
+
import { CLI_DOMAIN_CONTROL_MONITOR_SCHEMA, CLI_DOMAIN_CONTROL_MONITOR_VERSION, PUBLIC_CLI_DOMAIN_CONTROL_MONITOR_VERSION, SUPPORTED_CLI_DOMAIN_CONTROL_MONITOR_VERSIONS, DOMAIN_CONTROL_MONITOR_COLLECTION_KEYS, DOMAIN_CONTROL_MONITOR_EMBEDDED_VERSIONS, DOMAIN_CONTROL_MONITOR_FAILURE_CATEGORIES, DOMAIN_CONTROL_MONITOR_FAILURE_KEYS, DOMAIN_CONTROL_MONITOR_LIMITATIONS, DOMAIN_CONTROL_MONITOR_MANIFEST_KEYS, DOMAIN_CONTROL_MONITOR_ROOT_KEYS, MAX_DOMAIN_CONTROL_MONITOR_CONCURRENCY, MAX_DOMAIN_CONTROL_MONITOR_DOMAINS as CONTRACT_MAX_DOMAIN_CONTROL_MONITOR_DOMAINS, MAX_DOMAIN_CONTROL_MONITOR_ERROR_LENGTH, MAX_DOMAIN_CONTROL_MONITOR_FAILURES, MAX_DOMAIN_CONTROL_MONITOR_INPUT_BYTES, MAX_DOMAIN_CONTROL_MONITOR_JSON_CONTAINER_ITEMS, MAX_DOMAIN_CONTROL_MONITOR_JSON_DEPTH, MAX_DOMAIN_CONTROL_MONITOR_JSON_KEYS, MAX_DOMAIN_CONTROL_MONITOR_JSON_VALUES, MIN_DOMAIN_CONTROL_MONITOR_CONCURRENCY, MIN_DOMAIN_CONTROL_MONITOR_DOMAINS, } from "../packages/contracts/domain-control-monitor.mjs";
|
|
7
7
|
import { validateDomainControlReviewDocument, verifyDomainControlManifest, } from "../lib/domain-control-manifest.mjs";
|
|
8
8
|
import { requireDomainName, requireIsoTimestamp } from "../lib/bounded-contract-normalizers.mjs";
|
|
9
9
|
import { buildCliLookupDocument } from "./formatters/json.mjs";
|
|
@@ -62,8 +62,7 @@ function previousSnapshot(input, currentGeneratedAt) {
|
|
|
62
62
|
throw new CliUsageError(`Previous monitor snapshot must use ${CLI_DOMAIN_CONTROL_MONITOR_SCHEMA} supported version ${SUPPORTED_CLI_DOMAIN_CONTROL_MONITOR_VERSIONS.join(' or ')}.`);
|
|
63
63
|
}
|
|
64
64
|
const legacy = root.version === PUBLIC_CLI_DOMAIN_CONTROL_MONITOR_VERSION;
|
|
65
|
-
const reviewVersion
|
|
66
|
-
const flightVersion = legacy ? PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION : DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION;
|
|
65
|
+
const { review: reviewVersion, flightRecorder: flightVersion } = DOMAIN_CONTROL_MONITOR_EMBEDDED_VERSIONS[root.version];
|
|
67
66
|
let previousGeneratedAt;
|
|
68
67
|
try {
|
|
69
68
|
previousGeneratedAt = requireIsoTimestamp(root.generatedAt, 'Previous monitor snapshot generatedAt');
|
|
@@ -307,6 +306,7 @@ export function formatDomainControlMonitor(document) {
|
|
|
307
306
|
`Succeeded ${document.collection.succeeded}`,
|
|
308
307
|
`Failed ${document.collection.failed}`,
|
|
309
308
|
`Review ${document.review.state}`,
|
|
309
|
+
`Due ${document.review.counts.due ?? 0}`,
|
|
310
310
|
`Unexpected ${document.flightRecorder.summary.unexpectedChanges}`,
|
|
311
311
|
'',
|
|
312
312
|
].join('\n');
|
|
@@ -4,7 +4,7 @@ import { canonicalCaaRecord, canonicalDsRecord, canonicalMxRecord, canonicalDoma
|
|
|
4
4
|
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
5
5
|
import { postureTransferRestriction } from "../packages/evidence/domain-posture-context.mjs";
|
|
6
6
|
import { MAX_DOMAIN_CONTROL_INPUT_RECORDS, PUBLIC_DOMAIN_CONTROL_MANIFEST_VERSION } from "../packages/contracts/domain-control-manifest.mjs";
|
|
7
|
-
import { scanBoundedJson, } from "../
|
|
7
|
+
import { scanBoundedJson, } from "../packages/analysis/bounded-json.mjs";
|
|
8
8
|
import { CLI_DOMAIN_CONTROL_REVIEW_INPUT_KEYS, CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_LIMITATIONS, CLI_DOMAIN_CONTROL_REVIEW_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_VERSION, PUBLIC_CLI_DOMAIN_CONTROL_REVIEW_VERSION, SUPPORTED_CLI_DOMAIN_CONTROL_REVIEW_VERSIONS, DOMAIN_CONTROL_REVIEW_VERSION, MAX_DOMAIN_CONTROL_REVIEW_INPUT_BYTES, MAX_DOMAIN_CONTROL_REVIEW_JSON_DEPTH, MAX_DOMAIN_CONTROL_REVIEW_JSON_KEYS, MAX_DOMAIN_CONTROL_REVIEW_JSON_VALUES, MAX_DOMAIN_CONTROL_REVIEW_LOOKUPS, MIN_DOMAIN_CONTROL_REVIEW_LOOKUPS, } from "../packages/contracts/domain-control-review.mjs";
|
|
9
9
|
import { MAX_FLIGHT_RECORDER_VALUES, mergeConcurrentDomainControlFields, } from "../lib/domain-control-flight-recorder.mjs";
|
|
10
10
|
import { DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, reviewDomainControlManifest, verifyDomainControlManifest, } from "../lib/domain-control-manifest.mjs";
|
|
@@ -289,6 +289,7 @@ export function formatCliDomainControlReview(document) {
|
|
|
289
289
|
`Domains ${review.domains.length}`,
|
|
290
290
|
`Lookups ${document.input.lookupsReceived}`,
|
|
291
291
|
`Drift ${review.counts.drift ?? 0}`,
|
|
292
|
+
`Due ${review.counts.due ?? 0}`,
|
|
292
293
|
`Incomplete ${(review.counts.partial ?? 0) + (review.counts.unavailable ?? 0) + (review.counts.unsupported ?? 0)}`,
|
|
293
294
|
'',
|
|
294
295
|
...review.domains.map((item) => `${item.domain} ${item.state}`),
|
package/cli/errors.mjs
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { stripVTControlCharacters } from 'node:util';
|
|
2
|
+
import { hasUnsafeCliText } from "../packages/contracts/cli-grammar.mjs";
|
|
2
3
|
const MAX_CLI_ERROR_MESSAGE_LENGTH = 300;
|
|
3
4
|
const MAX_CLI_DIAGNOSTIC_BYTES = 4_096;
|
|
4
|
-
const CLI_DEFAULT_IGNORABLE_RE = /\p{Default_Ignorable_Code_Point}/u;
|
|
5
5
|
const CLI_DEFAULT_IGNORABLE_GLOBAL_RE = /\p{Default_Ignorable_Code_Point}/gu;
|
|
6
6
|
class CliUsageError extends Error {
|
|
7
|
-
|
|
7
|
+
reason;
|
|
8
|
+
constructor(message, reason = 'invalid_input') {
|
|
8
9
|
super(message);
|
|
9
10
|
this.name = 'CliUsageError';
|
|
11
|
+
this.reason = reason;
|
|
10
12
|
}
|
|
11
13
|
}
|
|
12
14
|
function errorMessage(error) {
|
|
@@ -22,9 +24,6 @@ function boundedCliErrorMessage(error, fallback = 'Unexpected command failure')
|
|
|
22
24
|
.trim()
|
|
23
25
|
.slice(0, MAX_CLI_ERROR_MESSAGE_LENGTH) || fallback;
|
|
24
26
|
}
|
|
25
|
-
function hasUnsafeCliText(value) {
|
|
26
|
-
return /[\x00-\x1f\x7f-\x9f]/u.test(value) || CLI_DEFAULT_IGNORABLE_RE.test(value);
|
|
27
|
-
}
|
|
28
27
|
// A transient stderr preview, never a checkpoint field. Bound before collecting
|
|
29
28
|
// and redact request URLs and credential-shaped fields before shortening text.
|
|
30
29
|
function createCliDiagnosticOutput() {
|
|
@@ -59,6 +58,6 @@ function boundedCliInputError(error, label) {
|
|
|
59
58
|
if (error instanceof CliUsageError)
|
|
60
59
|
return error;
|
|
61
60
|
const safeLabel = label.replace(/[^A-Za-z0-9 ()_-]+/gu, '').trim().slice(0, 80) || 'Input';
|
|
62
|
-
return new CliUsageError(`${safeLabel} could not be read as a bounded regular file
|
|
61
|
+
return new CliUsageError(`${safeLabel} could not be read as a bounded regular file.`, 'input_unavailable');
|
|
63
62
|
}
|
|
64
63
|
export { CliUsageError, MAX_CLI_ERROR_MESSAGE_LENGTH, boundedCliErrorMessage, boundedCliInputError, createCliDiagnosticOutput, hasUnsafeCliText, };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {} from "./command-argument-grammar.mjs";
|
|
2
|
+
import { CliUsageError } from "./errors.mjs";
|
|
3
|
+
import { parseArchiveContentDigest } from "./archive-content-digest.mjs";
|
|
4
|
+
import { terminalOptions } from "./argument-values.mjs";
|
|
5
|
+
function parseInspectArchiveArguments(parsed) {
|
|
6
|
+
const expectedContentDigest = parsed.optionValue('--expect-content-digest');
|
|
7
|
+
try {
|
|
8
|
+
parseArchiveContentDigest(expectedContentDigest);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
throw new CliUsageError('--expect-content-digest requires sha256:<64 lowercase hex> or sorted-json-v2:sha256:<64 lowercase hex>.');
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
action: 'inspect-archive',
|
|
15
|
+
source: parsed.positionalValue('source'),
|
|
16
|
+
passphraseSource: parsed.optionValue('--passphrase-file'),
|
|
17
|
+
search: parsed.optionValue('--search'),
|
|
18
|
+
reveal: parsed.hasOption('--reveal'),
|
|
19
|
+
requireMatch: parsed.hasOption('--require-match'),
|
|
20
|
+
expectedContentDigest,
|
|
21
|
+
output: parsed.hasOption('--json') ? 'json' : 'terminal',
|
|
22
|
+
...terminalOptions(parsed),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function parseSignArtifactArguments(parsed) {
|
|
26
|
+
return {
|
|
27
|
+
action: 'sign-artifact',
|
|
28
|
+
source: parsed.positionalValue('source'),
|
|
29
|
+
privateKeySource: parsed.optionValue('--private-key-file'),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function parseVerifySignatureArguments(parsed) {
|
|
33
|
+
return {
|
|
34
|
+
action: 'verify-signature',
|
|
35
|
+
source: parsed.positionalValue('source'),
|
|
36
|
+
publicKeySource: parsed.optionValue('--public-key-file'),
|
|
37
|
+
trustStoreSource: parsed.optionValue('--trust-store-file'),
|
|
38
|
+
output: parsed.hasOption('--json') ? 'json' : 'terminal',
|
|
39
|
+
...terminalOptions(parsed),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export const EVIDENCE_ARGUMENT_PARSERS = Object.freeze({
|
|
43
|
+
'inspect-archive': parseInspectArchiveArguments,
|
|
44
|
+
'sign-artifact': parseSignArtifactArguments,
|
|
45
|
+
'verify-signature': parseVerifySignatureArguments,
|
|
46
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { WORKSPACE_ARCHIVE_VERSION } from "../packages/contracts/case-portability.mjs";
|
|
2
|
+
import { LEGACY_WORKSPACE_ARCHIVE_DESCRIPTION, LEGACY_WORKSPACE_ARCHIVE_SCOPE, OPTIONAL_FILE_POSITIONAL, constraint, EMPTY_CONSTRAINTS, commandSeed } from "./command-definition.mjs";
|
|
3
|
+
export const EVIDENCE_COMMAND_DEFINITIONS = Object.freeze({
|
|
4
|
+
"inspect-archive": commandSeed({
|
|
5
|
+
reference: {
|
|
6
|
+
description: `Summarise or search one current version-${WORKSPACE_ARCHIVE_VERSION} workspace archive, with exact ${LEGACY_WORKSPACE_ARCHIVE_DESCRIPTION} support and redacted output by default.`,
|
|
7
|
+
example: 'whoisleuth inspect-archive workspace.json --search example.test --json',
|
|
8
|
+
boundary: 'Exact values require --reveal. New content comparisons use the reported sorted-json-v2:sha256 identity with --expect-content-digest; bare sha256 hashes retain their legacy locale-sensitive meaning. Retired and future archives are rejected. The archive is read locally and is never uploaded.',
|
|
9
|
+
},
|
|
10
|
+
collection: { scope: `Reads one selected bounded workspace archive v${WORKSPACE_ARCHIVE_VERSION}, retains exact ${LEGACY_WORKSPACE_ARCHIVE_SCOPE} compatibility, and redacts output by default.` },
|
|
11
|
+
summary: 'Inspect an archive locally',
|
|
12
|
+
options: ['--passphrase-file', '--search', '--require-match', '--reveal', '--expect-content-digest', '--json', '--quiet', '--no-color'],
|
|
13
|
+
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
14
|
+
constraints: Object.freeze([
|
|
15
|
+
constraint({ kind: 'requires_all', option: '--reveal', requiredOptions: ['--search'] }),
|
|
16
|
+
constraint({ kind: 'requires_all', option: '--require-match', requiredOptions: ['--search'] }),
|
|
17
|
+
]),
|
|
18
|
+
handlerOwner: 'evidence',
|
|
19
|
+
networkEffect: 'offline',
|
|
20
|
+
common: false,
|
|
21
|
+
schemaIdentifiers: Object.freeze(['whoisleuth.workspace-archive-inspection']),
|
|
22
|
+
primaryArtefacts: Object.freeze([]),
|
|
23
|
+
planSupport: false,
|
|
24
|
+
additionalOutputFormats: Object.freeze([]),
|
|
25
|
+
bootstrapProfile: 'allowed',
|
|
26
|
+
}),
|
|
27
|
+
"sign-artifact": commandSeed({
|
|
28
|
+
reference: {
|
|
29
|
+
description: 'Sign one reviewed response packet or supported manifest with a local private key.',
|
|
30
|
+
example: 'whoisleuth sign-artifact packet.json --private-key-file analyst-private.pem',
|
|
31
|
+
boundary: 'The command never creates, stores, or transmits keys. Key custody and signer identity remain the operator\'s responsibility.',
|
|
32
|
+
},
|
|
33
|
+
collection: { scope: 'Reads one selected artefact and one local private key without transmitting either.' },
|
|
34
|
+
summary: 'Sign a reviewed artefact locally',
|
|
35
|
+
options: ['--private-key-file'],
|
|
36
|
+
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
37
|
+
constraints: Object.freeze([
|
|
38
|
+
constraint({ kind: 'required', options: ['--private-key-file'] }),
|
|
39
|
+
]),
|
|
40
|
+
handlerOwner: 'evidence',
|
|
41
|
+
networkEffect: 'offline',
|
|
42
|
+
common: false,
|
|
43
|
+
schemaIdentifiers: Object.freeze(['whoisleuth.signed-evidence-package']),
|
|
44
|
+
primaryArtefacts: Object.freeze([]),
|
|
45
|
+
planSupport: false,
|
|
46
|
+
additionalOutputFormats: Object.freeze([]),
|
|
47
|
+
bootstrapProfile: 'allowed',
|
|
48
|
+
}),
|
|
49
|
+
"verify-signature": commandSeed({
|
|
50
|
+
reference: {
|
|
51
|
+
description: 'Verify one signed package and report embedded-artefact assurance separately. --trust-store-file also checks an explicit local fingerprint policy and emits a signer-trust report; unknown, retired, revoked or future-reviewed entries exit 4.',
|
|
52
|
+
example: 'whoisleuth verify-signature packet.signed.json --trust-store-file trust.json --json',
|
|
53
|
+
boundary: 'A valid signature proves package consistency for the embedded key, not identity, authority or evidence accuracy. With --trust-store-file, unknown, retired, revoked or future-reviewed entries exit 4 even if --public-key-file matches. Replacement fingerprints need their own trusted entry; no signing date overrides current revocation.',
|
|
54
|
+
},
|
|
55
|
+
collection: { scope: 'Reads one selected signed package, optional local public key and explicit fingerprint trust file. No automatic trust discovery or network requests.' },
|
|
56
|
+
summary: 'Verify a signed evidence package',
|
|
57
|
+
options: ['--public-key-file', '--trust-store-file', '--json', '--quiet', '--no-color'],
|
|
58
|
+
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
59
|
+
constraints: EMPTY_CONSTRAINTS,
|
|
60
|
+
handlerOwner: 'evidence',
|
|
61
|
+
networkEffect: 'offline',
|
|
62
|
+
common: false,
|
|
63
|
+
schemaIdentifiers: Object.freeze(['whoisleuth.evidence-signature-verification', 'whoisleuth.evidence-signer-trust-store', 'whoisleuth.evidence-signer-trust-report']),
|
|
64
|
+
primaryArtefacts: Object.freeze([]),
|
|
65
|
+
planSupport: false,
|
|
66
|
+
additionalOutputFormats: Object.freeze([]),
|
|
67
|
+
bootstrapProfile: 'allowed',
|
|
68
|
+
}),
|
|
69
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { MAX_OFFLINE_ARTIFACT_BYTES, MAX_OFFLINE_PASSPHRASE_FILE_BYTES, } from "./artifact-verify.mjs";
|
|
2
2
|
import { formatArchiveInspection, inspectWorkspaceArchive, } from "./archive-inspect.mjs";
|
|
3
3
|
import { MAX_SIGNING_KEY_FILE_BYTES, formatEvidenceSignatureVerification, signEvidencePackage, verifyEvidencePackageSignature, } from "./evidence-signing.mjs";
|
|
4
|
+
import { commandHasOwner } from "./command-reference.mjs";
|
|
4
5
|
import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
|
|
5
6
|
import EXIT_CODES from "./exit-codes.mjs";
|
|
6
7
|
import { formatJsonDocument } from "./formatters/json.mjs";
|
|
@@ -8,9 +9,7 @@ import { readCliTextInput } from "./input.mjs";
|
|
|
8
9
|
import { assessEvidenceSignerTrust, formatSignerTrustReport, MAX_SIGNER_TRUST_STORE_BYTES } from "./signer-trust.mjs";
|
|
9
10
|
import { runDiscriminatedCommandHandler } from "./discriminated-command-handlers.mjs";
|
|
10
11
|
export function isEvidenceCommand(args) {
|
|
11
|
-
return args.action
|
|
12
|
-
|| args.action === 'sign-artifact'
|
|
13
|
-
|| args.action === 'verify-signature';
|
|
12
|
+
return commandHasOwner(args.action, 'evidence');
|
|
14
13
|
}
|
|
15
14
|
export function evidenceCommandFailureLabel(action) {
|
|
16
15
|
if (action === 'inspect-archive')
|
package/cli/evidence-signing.mjs
CHANGED
|
@@ -169,7 +169,8 @@ export async function verifyEvidencePackageSignature(raw, trustedPublicKeyPem) {
|
|
|
169
169
|
}
|
|
170
170
|
catch { /* Signature validity remains independently reportable. */ }
|
|
171
171
|
let publicKeyMatched = null;
|
|
172
|
-
|
|
172
|
+
const trustedKeySupplied = trustedPublicKeyPem !== undefined && trustedPublicKeyPem !== null;
|
|
173
|
+
if (trustedKeySupplied) {
|
|
173
174
|
const trustedDer = spkiDer(importPublicKey(trustedPublicKeyPem, 'Trusted public key file'));
|
|
174
175
|
publicKeyMatched = trustedDer.equals(publicDer);
|
|
175
176
|
if (!publicKeyMatched)
|
|
@@ -181,7 +182,7 @@ export async function verifyEvidencePackageSignature(raw, trustedPublicKeyPem) {
|
|
|
181
182
|
state: 'signature_valid',
|
|
182
183
|
signature: Object.freeze({
|
|
183
184
|
state: 'valid',
|
|
184
|
-
signerTrust:
|
|
185
|
+
signerTrust: trustedKeySupplied ? 'trusted_key' : 'embedded_key_only',
|
|
185
186
|
signedAt,
|
|
186
187
|
keyIdSha256: signature.keyIdSha256,
|
|
187
188
|
publicKeyMatched,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { domainToASCII } from 'node:url';
|
|
2
|
-
import { exactKeys, requireBoundedString, requireIsoTimestamp, requireRecord, } from "../lib/bounded-contract-normalizers.mjs";
|
|
2
|
+
import { compareCodeUnits, exactKeys, requireBoundedString, requireIsoTimestamp, requireRecord, } from "../lib/bounded-contract-normalizers.mjs";
|
|
3
3
|
import { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, EXTERNAL_FINDING_CATEGORIES, MAX_EXTERNAL_FINDINGS, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, MAX_EXTERNAL_FINDING_DOMAINS, MAX_EXTERNAL_FINDING_LIMITATIONS, MAX_EXTERNAL_FINDING_LIMITATION_LENGTH, parseExternalFindingsDocument, retainExternalFindingLimitations, } from "../packages/interchange/external-findings-import.mjs";
|
|
4
4
|
export const EXTERNAL_OBSERVATION_MAPPING_SCHEMA = 'whoisleuth.external-observation-mapping';
|
|
5
5
|
export const EXTERNAL_OBSERVATION_MAPPING_VERSION = 1;
|
|
@@ -112,7 +112,7 @@ export function mapExternalObservations(inputRaw) {
|
|
|
112
112
|
: null,
|
|
113
113
|
structuredObservation: null,
|
|
114
114
|
});
|
|
115
|
-
}).sort((left, right) => left.domain
|
|
115
|
+
}).sort((left, right) => compareCodeUnits(left.domain, right.domain) || compareCodeUnits(left.observedAt, right.observedAt) || compareCodeUnits(left.summary, right.summary));
|
|
116
116
|
const selected = [];
|
|
117
117
|
const seen = new Set();
|
|
118
118
|
const domainCounts = new Map();
|
|
@@ -168,6 +168,12 @@ function formatTerminalPosture(document) {
|
|
|
168
168
|
`Dependencies ${dependencies.length} observed · ${dmarcAuthorizations.length} DMARC reporting destination${dmarcAuthorizations.length === 1 ? '' : 's'} checked`,
|
|
169
169
|
'',
|
|
170
170
|
];
|
|
171
|
+
for (const value of dmarcAuthorizations.slice(0, 10)) {
|
|
172
|
+
const item = terminalRecord(value);
|
|
173
|
+
lines.push(`Reporting ${safeTerminalValue(item.destination)} · ${safeTerminalValue(item.reportType)} · ${item.state === 'self' ? 'same organisational scope' : safeTerminalValue(item.state).replaceAll('_', ' ')}`);
|
|
174
|
+
if (item.error)
|
|
175
|
+
lines.push(` Detail ${safeTerminalValue(item.error)}`);
|
|
176
|
+
}
|
|
171
177
|
for (const value of checks) {
|
|
172
178
|
const item = terminalRecord(value);
|
|
173
179
|
lines.push(`[${safeTerminalValue(item.status, 'info').toUpperCase()}] ${safeTerminalValue(item.label)}: ${safeTerminalValue(item.summary)}`);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { registryAccessProfileLabel } from "../registry-access.mjs";
|
|
2
|
+
import { dnsAddressFamilyEvidence } from "../../packages/evidence/dns-query-outcome.mjs";
|
|
2
3
|
import { appendTechnologyLines } from "./terminal-technology.mjs";
|
|
3
4
|
import { appendDeliveryMetadataLines, appendPublicationMetadataLines } from "./terminal-metadata.mjs";
|
|
4
5
|
import { securityPostureReview, summarizeSecurityPostureReview } from "../../lib/website-security-posture.mjs";
|
|
@@ -319,6 +320,11 @@ function formatTerminalLookup(document, { detail = 'standard' } = {}) {
|
|
|
319
320
|
if (Object.keys(dns).length) {
|
|
320
321
|
dnsLines.push(`Evidence ${titleCase(dns.status)}`);
|
|
321
322
|
dnsLines.push(`Completeness ${dns.complete === true ? 'Complete' : 'Incomplete'}`);
|
|
323
|
+
for (const family of ['a', 'aaaa']) {
|
|
324
|
+
const evidence = dnsAddressFamilyEvidence(dns, family);
|
|
325
|
+
if (evidence.value)
|
|
326
|
+
dnsLines.push(`${family.toUpperCase().padEnd(14)} ${safeTerminalValue(evidence.value)}`);
|
|
327
|
+
}
|
|
322
328
|
const recordTypes = [
|
|
323
329
|
['a', 'A'], ['aaaa', 'AAAA'], ['cname', 'CNAME'], ['ns', 'NS'],
|
|
324
330
|
['mx', 'MX'], ['spf', 'SPF'], ['dmarc', 'DMARC'], ['caa', 'CAA'],
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {} from "./command-argument-grammar.mjs";
|
|
2
|
+
import { CliUsageError } from "./errors.mjs";
|
|
3
|
+
import { terminalOptions, jsonOutput, parseTwoFileComparisonArguments, uniqueSources, } from "./argument-values.mjs";
|
|
4
|
+
function parseDiffArguments(parsed) {
|
|
5
|
+
const compared = parseTwoFileComparisonArguments(parsed, 'diff');
|
|
6
|
+
const leftSessionId = parsed.optionValue('--left-session');
|
|
7
|
+
const rightSessionId = parsed.optionValue('--right-session');
|
|
8
|
+
for (const [option, value] of [
|
|
9
|
+
['--left-session', leftSessionId],
|
|
10
|
+
['--right-session', rightSessionId],
|
|
11
|
+
]) {
|
|
12
|
+
if (value !== null && !/^[A-Za-z0-9_-]{1,128}$/u.test(value)) {
|
|
13
|
+
throw new CliUsageError(`${option} requires one bounded saved-session ID.`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return { action: 'diff', ...compared, leftSessionId, rightSessionId };
|
|
17
|
+
}
|
|
18
|
+
export const HISTORY_ARGUMENT_PARSERS = Object.freeze({
|
|
19
|
+
diff: parseDiffArguments,
|
|
20
|
+
reconcile: (parsed) => ({
|
|
21
|
+
action: 'reconcile',
|
|
22
|
+
sources: uniqueSources(parsed, 'reconcile'),
|
|
23
|
+
output: jsonOutput(parsed),
|
|
24
|
+
...terminalOptions(parsed),
|
|
25
|
+
}),
|
|
26
|
+
timeline: (parsed) => ({
|
|
27
|
+
action: 'timeline',
|
|
28
|
+
sources: uniqueSources(parsed, 'timeline'),
|
|
29
|
+
output: jsonOutput(parsed),
|
|
30
|
+
...terminalOptions(parsed),
|
|
31
|
+
}),
|
|
32
|
+
export: (parsed) => ({
|
|
33
|
+
action: 'export',
|
|
34
|
+
source: parsed.positionalValue('source'),
|
|
35
|
+
format: parsed.hasOption('--markdown') ? 'markdown' : parsed.hasOption('--html') ? 'html' : 'json',
|
|
36
|
+
compact: parsed.hasOption('--compact'),
|
|
37
|
+
includeAttribution: !parsed.hasOption('--no-attribution'),
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { LOOKUP_EVIDENCE_SCHEMA_VERSION, V1_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION } from "../packages/contracts/lookup-evidence.mjs";
|
|
2
|
+
import { PUBLISHED_V2_LOOKUP_EVIDENCE_VERSIONS, positional, OPTIONAL_FILE_POSITIONAL, constraint, EMPTY_CONSTRAINTS, commandSeed } from "./command-definition.mjs";
|
|
3
|
+
export const HISTORY_COMMAND_DEFINITIONS = Object.freeze({
|
|
4
|
+
diff: commandSeed({
|
|
5
|
+
reference: {
|
|
6
|
+
description: 'Compare an earlier and later artefact from the same retained Lookup, Bulk-session, or domain-portfolio family.',
|
|
7
|
+
example: 'whoisleuth diff earlier.json later.json --json',
|
|
8
|
+
boundary: 'Comparison is offline: the left input is earlier and the right input is later. Inputs must belong to the same supported family. Saved Lookups can describe the same or different domains; same-domain comparisons preserve observation times and collection uncertainty. For a multi-session Bulk export, --left-session selects a session from the left file and --right-session selects one from the right; missing, unavailable, equal, and different evidence remain separate states.',
|
|
9
|
+
},
|
|
10
|
+
collection: { scope: 'Reads two compatible retained artefacts capped at 8 MiB each and retains no source paths.' },
|
|
11
|
+
summary: 'Compare two compatible retained artefacts',
|
|
12
|
+
options: ['--left-session', '--right-session', '--json', '--quiet', '--no-color'],
|
|
13
|
+
positionals: Object.freeze([positional('sources', 'file', 2, 2)]),
|
|
14
|
+
constraints: EMPTY_CONSTRAINTS,
|
|
15
|
+
handlerOwner: 'history',
|
|
16
|
+
networkEffect: 'offline',
|
|
17
|
+
common: true,
|
|
18
|
+
schemaIdentifiers: Object.freeze(['whoisleuth.cli.lookup-diff']),
|
|
19
|
+
primaryArtefacts: Object.freeze(['Retained-evidence comparison']),
|
|
20
|
+
planSupport: false,
|
|
21
|
+
additionalOutputFormats: Object.freeze([]),
|
|
22
|
+
bootstrapProfile: 'allowed',
|
|
23
|
+
}),
|
|
24
|
+
reconcile: commandSeed({
|
|
25
|
+
reference: {
|
|
26
|
+
description: 'Reconcile bounded values across independently labelled observations of one domain.',
|
|
27
|
+
example: 'whoisleuth reconcile office.json mobile.json external.json --json',
|
|
28
|
+
boundary: 'The command is offline, accepts 2 to 5 saved observations for one domain, and never treats labels as proof of network independence or majority agreement as truth.',
|
|
29
|
+
},
|
|
30
|
+
collection: { scope: 'Reads 2 to 5 saved observations for one domain, capped at 32 MiB in total.' },
|
|
31
|
+
summary: 'Reconcile independently labelled observations',
|
|
32
|
+
options: ['--json', '--quiet', '--no-color'],
|
|
33
|
+
positionals: Object.freeze([positional('sources', 'file', 2, 5)]),
|
|
34
|
+
constraints: EMPTY_CONSTRAINTS,
|
|
35
|
+
handlerOwner: 'history',
|
|
36
|
+
networkEffect: 'offline',
|
|
37
|
+
common: false,
|
|
38
|
+
schemaIdentifiers: Object.freeze(['whoisleuth.cli.lookup-reconciliation']),
|
|
39
|
+
primaryArtefacts: Object.freeze([]),
|
|
40
|
+
planSupport: false,
|
|
41
|
+
additionalOutputFormats: Object.freeze([]),
|
|
42
|
+
bootstrapProfile: 'allowed',
|
|
43
|
+
}),
|
|
44
|
+
timeline: commandSeed({
|
|
45
|
+
reference: {
|
|
46
|
+
description: 'Build an ordered same-domain history from saved Lookup observations.',
|
|
47
|
+
example: 'whoisleuth timeline first.json second.json latest.json --json',
|
|
48
|
+
boundary: 'The command is offline, accepts 2 to 20 bounded inputs for one domain, retains no filenames or raw registry payloads, and does not treat changed collection conditions as a domain change.',
|
|
49
|
+
},
|
|
50
|
+
collection: { scope: 'Reads 2 to 20 saved observations for one domain, capped at 32 MiB in total.' },
|
|
51
|
+
summary: 'Build same-domain history from saved lookups',
|
|
52
|
+
options: ['--json', '--quiet', '--no-color'],
|
|
53
|
+
positionals: Object.freeze([positional('sources', 'file', 2, 20)]),
|
|
54
|
+
constraints: EMPTY_CONSTRAINTS,
|
|
55
|
+
handlerOwner: 'history',
|
|
56
|
+
networkEffect: 'offline',
|
|
57
|
+
common: false,
|
|
58
|
+
schemaIdentifiers: Object.freeze(['whoisleuth.cli.lookup-timeline']),
|
|
59
|
+
primaryArtefacts: Object.freeze(['Bounded retained-observation timeline']),
|
|
60
|
+
planSupport: false,
|
|
61
|
+
additionalOutputFormats: Object.freeze([]),
|
|
62
|
+
bootstrapProfile: 'allowed',
|
|
63
|
+
}),
|
|
64
|
+
export: commandSeed({
|
|
65
|
+
reference: {
|
|
66
|
+
description: 'Convert one saved lookup into a versioned evidence report.',
|
|
67
|
+
example: 'whoisleuth export lookup.json --markdown',
|
|
68
|
+
boundary: `Saved Lookup versions 1 and 2 are capped at 8 MiB and scanned for duplicate keys, the prototype-sensitive __proto__ key, and bounded nesting, key, value, and per-container counts before parsing. Current schema-${LOOKUP_EVIDENCE_SCHEMA_VERSION} exports preserve evidence-source attribution and limitations; published v2 schemas ${PUBLISHED_V2_LOOKUP_EVIDENCE_VERSIONS} and exact v1 schema ${V1_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION} remain readable, while other historical and unreleased shapes are unsupported. Markdown and HTML include a presentation-only generator footer unless --no-attribution is selected; JSON retains bounded generator provenance. Compact output intentionally omits raw registry payloads.`,
|
|
69
|
+
},
|
|
70
|
+
collection: { scope: 'Reads one saved Lookup and writes one bounded report.' },
|
|
71
|
+
summary: 'Convert a lookup to an evidence report',
|
|
72
|
+
options: ['--markdown', '--html', '--compact', '--no-attribution'],
|
|
73
|
+
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
74
|
+
constraints: Object.freeze([
|
|
75
|
+
constraint({ kind: 'mutually_exclusive', options: ['--markdown', '--html'] }),
|
|
76
|
+
constraint({ kind: 'excludes_all', option: '--compact', excludedOptions: ['--markdown', '--html'] }),
|
|
77
|
+
constraint({ kind: 'requires_any', option: '--no-attribution', requiredOptions: ['--markdown', '--html'] }),
|
|
78
|
+
]),
|
|
79
|
+
handlerOwner: 'history',
|
|
80
|
+
networkEffect: 'offline',
|
|
81
|
+
common: true,
|
|
82
|
+
schemaIdentifiers: Object.freeze(['whoisleuth.lookup-evidence']),
|
|
83
|
+
primaryArtefacts: Object.freeze(['Portable evidence report']),
|
|
84
|
+
planSupport: false,
|
|
85
|
+
additionalOutputFormats: Object.freeze(['JSON']),
|
|
86
|
+
bootstrapProfile: 'allowed',
|
|
87
|
+
}),
|
|
88
|
+
});
|