@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
package/cli/case-command.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { boundedJsonLimitsForBytes, parseBoundedJsonObject } from "../
|
|
2
|
+
import { boundedJsonLimitsForBytes, parseBoundedJsonObject } from "../packages/analysis/bounded-json.mjs";
|
|
3
3
|
import { canonicalArtifactJsonV2 } from "../packages/evidence/artifact-integrity.mjs";
|
|
4
4
|
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
5
5
|
import { MAX_CASE_CHECKPOINT_FACTS, MAX_CASE_OBJECTIVE_LENGTH, MAX_CASE_STORE_BYTES, MAX_EDITABLE_CASE_INPUT_BYTES, MAX_NOTE_LENGTH, MAX_RESPONSE_RATIONALE_LENGTH, MAX_RESPONSE_VALUE_LENGTH } from "../packages/contracts/case-portability.mjs";
|
|
@@ -8,6 +8,7 @@ import { createCaseIncident, openOrCreateCase, recordCaseConclusion, recordCaseR
|
|
|
8
8
|
import { buildCaseExport, serializeCaseStore } from "../packages/cases/case-storage-model.mjs";
|
|
9
9
|
import { appendCaseEvidencePin } from "../packages/cases/case-response-model.mjs";
|
|
10
10
|
import { readCaseRecheckAnswerContext } from "../packages/cases/case-recheck-model.mjs";
|
|
11
|
+
import { caseEvidenceLinkIssues, mergeCaseEvidenceLinks } from "../packages/cases/case-evidence-links.mjs";
|
|
11
12
|
import { dispositionLabel, statusLabel } from "../packages/cases/case-record-decisions.mjs";
|
|
12
13
|
import { normalizeDomain } from "../packages/cases/case-record-core.mjs";
|
|
13
14
|
import { safeTerminalValue } from "./formatters/terminal.mjs";
|
|
@@ -52,6 +53,10 @@ function pin(input, now) {
|
|
|
52
53
|
return raw;
|
|
53
54
|
}
|
|
54
55
|
function retainExistingEntries(before, after) {
|
|
56
|
+
if (canonicalArtifactJsonV2(mergeCaseEvidenceLinks(before.evidenceLinks, after.evidenceLinks) ?? [])
|
|
57
|
+
!== canonicalArtifactJsonV2(mergeCaseEvidenceLinks(undefined, after.evidenceLinks) ?? [])) {
|
|
58
|
+
throw new CliUsageError('The operation would remove retained evidence relationships. Nothing was written.');
|
|
59
|
+
}
|
|
55
60
|
const pairs = [
|
|
56
61
|
[before.notes, after.notes], [before.evidenceHistory, after.evidenceHistory], [before.evidencePins, after.evidencePins],
|
|
57
62
|
[before.decisions, after.decisions], [before.actions, after.actions], [before.assertions, after.assertions],
|
|
@@ -66,7 +71,7 @@ function retainExistingEntries(before, after) {
|
|
|
66
71
|
}
|
|
67
72
|
}
|
|
68
73
|
}
|
|
69
|
-
function
|
|
74
|
+
export function applyCliCaseOperation(cases, args, input, note, now) {
|
|
70
75
|
if (args.operation === 'open') {
|
|
71
76
|
const title = args.title === null ? undefined : prose(args.title, MAX_CASE_OBJECTIVE_LENGTH, 'Incident title');
|
|
72
77
|
const result = args.newIncident
|
|
@@ -84,6 +89,9 @@ function mutate(cases, args, input, note, now) {
|
|
|
84
89
|
else if (args.operation === 'pin') {
|
|
85
90
|
result = updateCase(cases, current.id, { evidencePin: pin(input, now) }, now);
|
|
86
91
|
}
|
|
92
|
+
else if (args.operation === 'link' || args.operation === 'withdraw-link') {
|
|
93
|
+
result = updateCase(cases, current.id, args.operation === 'link' ? { evidenceLink: input } : { evidenceLinkWithdrawal: input }, now);
|
|
94
|
+
}
|
|
87
95
|
else if (args.operation === 'assess') {
|
|
88
96
|
if (!input)
|
|
89
97
|
throw new CliUsageError('An assessment requires JSON input.');
|
|
@@ -149,6 +157,15 @@ function formatCases(cases, digest) {
|
|
|
149
157
|
lines.push(`Pin ${safeTerminalValue(pin.id)} · ${safeTerminalValue(pin.label)}`, safeTerminalValue(pin.value, '—', MAX_RESPONSE_VALUE_LENGTH), ` Source ${safeTerminalValue(pin.source)} · ${safeTerminalValue(pin.observedAt, 'time unavailable')} · ${pin.completeness}`);
|
|
150
158
|
for (const decision of record.decisions)
|
|
151
159
|
lines.push(`Assessment ${decision.createdAt}`, safeTerminalValue(decision.summary, '—', MAX_RESPONSE_VALUE_LENGTH), safeTerminalValue(decision.rationale, '—', MAX_RESPONSE_RATIONALE_LENGTH));
|
|
160
|
+
for (const { link, missingPinIds, cyclic } of caseEvidenceLinkIssues(record.evidenceLinks ?? [], record.evidencePins)) {
|
|
161
|
+
lines.push(`Evidence relationship ${safeTerminalValue(link.id)} · analyst declaration`, ` ${safeTerminalValue(link.fromPinId)} ${link.kind.replaceAll('_', ' ')} ${safeTerminalValue(link.toPinId)}`, ` ${safeTerminalValue(link.basis, '—', MAX_RESPONSE_RATIONALE_LENGTH)}`);
|
|
162
|
+
if (link.withdrawal)
|
|
163
|
+
lines.push(` Withdrawn ${link.withdrawal.at}: ${safeTerminalValue(link.withdrawal.reason, '—', MAX_RESPONSE_RATIONALE_LENGTH)}`);
|
|
164
|
+
if (missingPinIds.length)
|
|
165
|
+
lines.push(` Referenced pins not retained: ${missingPinIds.map(id => safeTerminalValue(id)).join(', ')}`);
|
|
166
|
+
if (cyclic)
|
|
167
|
+
lines.push(' Conflicting imported derivation cycle; no order is inferred.');
|
|
168
|
+
}
|
|
152
169
|
for (const review of record.observedEffects.reviews)
|
|
153
170
|
lines.push(`Recheck ${safeTerminalValue(review.observedAt, 'time unavailable')} · ${review.state} · ${review.completeness} · ${safeTerminalValue(review.source)}`);
|
|
154
171
|
lines.push(`Other retained records: ${record.evidenceHistory.length} snapshots, ${record.actions.length} actions, ${record.assertions.length} assertions, ${record.attachments?.length ?? 0} file references.`, '');
|
|
@@ -176,7 +193,7 @@ export async function runCaseCommand(args, dependencies, context) {
|
|
|
176
193
|
maximumBytes: MAX_EDITABLE_CASE_INPUT_BYTES, limits: boundedJsonLimitsForBytes(MAX_CASE_STORE_BYTES), label: 'Case operation input',
|
|
177
194
|
}) : null;
|
|
178
195
|
const note = args.noteSource ? await context.readInput(args.noteSource, MAX_NOTE_LENGTH * 4 + 4, 'Case note') : args.text;
|
|
179
|
-
const next =
|
|
196
|
+
const next = applyCliCaseOperation(cases, args, input, note, now);
|
|
180
197
|
if (Buffer.byteLength(serializeCaseStore(next)) > MAX_CASE_STORE_BYTES)
|
|
181
198
|
throw new CliUsageError('The updated Case store exceeds its byte budget. No evidence was pruned and nothing was written.');
|
|
182
199
|
const output = `${JSON.stringify(buildCaseExport(next, now))}\n`;
|
package/cli/case-pack.mjs
CHANGED
|
@@ -3,8 +3,12 @@ import { Buffer } from 'node:buffer';
|
|
|
3
3
|
import { canonicalArtifactJson, canonicalArtifactJsonV2, SORTED_JSON_V2, } from "../packages/evidence/artifact-integrity.mjs";
|
|
4
4
|
import { buildCaseReport, buildCaseReportVerificationProjection } from "../packages/cases/case-report.mjs";
|
|
5
5
|
import { WHOISLEUTH_APPLICATION_VERSION } from "../lib/application-version.mjs";
|
|
6
|
-
import { assertBoundedJsonStructure, scanBoundedJson } from "../
|
|
7
|
-
import { assertCaseBrandProfileIds
|
|
6
|
+
import { assertBoundedJsonStructure, scanBoundedJson } from "../packages/analysis/bounded-json.mjs";
|
|
7
|
+
import { assertCaseBrandProfileIds } from "../packages/cases/case-brand-profile-references.mjs";
|
|
8
|
+
import { CASE_AUDIENCE_SENSITIVE_FIELD_NAMES, caseAudienceExclusions, projectCaseForAudience, } from "../packages/cases/case-record-projection.mjs";
|
|
9
|
+
import { normalizeDomain } from "../packages/evidence/domain-name.mjs";
|
|
10
|
+
import { normalizeCaseStore } from "../packages/cases/case-migration-model.mjs";
|
|
11
|
+
import { safeId } from "../packages/cases/case-record-core.mjs";
|
|
8
12
|
import { CASE_REPORT_SCHEMA, CASE_IMPORT_VERSIONS, CASE_SCHEMA_VERSION, INCIDENT_CASE_SCHEMA_VERSION, CLI_CASE_PACK_SCHEMA, CLI_CASE_PACK_VERSION, CLI_CASE_PACK_INPUT_CASE_VERSIONS, CLI_CASE_PACK_CURRENT_REDACTION_KEYS, CLI_CASE_PACK_PUBLIC_REPORT_KEYS, CLI_CASE_PACK_INTEGRITY_KEYS as INTEGRITY_KEYS, CLI_CASE_PACK_LIMITATIONS as PACKET_LIMITATIONS, CLI_CASE_PACK_PACKET_KEYS as PACKET_KEYS, CLI_CASE_PACK_REPORT_KEYS as REPORT_KEYS, CLI_CASE_PACK_ROOT_KEYS as ROOT_KEYS, MAX_CASE_IMPORT_BYTES, MAX_CASE_PACK_CASES, MAX_CASE_PACK_INPUT_BYTES, PUBLIC_CASE_SCHEMA_VERSION, PUBLISHED_V2_3_CASE_SCHEMA_VERSION, caseReportVersionMatchesCase, } from "../packages/contracts/case-portability.mjs";
|
|
9
13
|
import { CliUsageError } from "./errors.mjs";
|
|
10
14
|
import { canonicalCaseExportProjection as canonicalPackProjection } from "../packages/cases/case-export-input.mjs";
|
|
@@ -62,6 +66,11 @@ function allowedSensitiveFieldPath(key, path) {
|
|
|
62
66
|
return topCasePath(path) || reportCasePath(path);
|
|
63
67
|
if (key === 'notes')
|
|
64
68
|
return topCasePath(path) || reportCasePath(path);
|
|
69
|
+
if (key === 'workflowMetadata')
|
|
70
|
+
return topCasePath(path) || analystResponsePath(path);
|
|
71
|
+
if (key === 'incidentTargets' || key === 'investigationContext') {
|
|
72
|
+
return path.at(-1) === 'workflowMetadata' && (topCasePath(path.slice(0, -1)) || analystResponsePath(path.slice(0, -1)));
|
|
73
|
+
}
|
|
65
74
|
if (key === 'actions' || key === 'assertions' || key === 'manualTrail' || key === 'observedEffects' || key === 'closures' || key === 'branches') {
|
|
66
75
|
return topCasePath(path) || analystResponsePath(path);
|
|
67
76
|
}
|
|
@@ -473,6 +482,7 @@ export function formatCliCasePack(document) {
|
|
|
473
482
|
`Cases ${document.cases.length}`,
|
|
474
483
|
`Reports ${document.packet.reports.length}`,
|
|
475
484
|
`Digest ${document.integrity.digestSha256}`,
|
|
485
|
+
'Included for every audience: domain evidence, Case, pin and decision identifiers, tags, decision summaries and rationale. Review these before sharing.',
|
|
476
486
|
'',
|
|
477
487
|
...document.packet.redactionManifest.excluded.map((item) => `Excluded: ${item}`),
|
|
478
488
|
'',
|
package/cli/ci-report.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { WHOISLEUTH_REQUEST_POLICY_URL } from "../
|
|
1
|
+
import { WHOISLEUTH_REQUEST_POLICY_URL } from "../packages/analysis/project-metadata.mjs";
|
|
2
2
|
import { CLI_DOMAIN_CONTROL_MONITOR_SCHEMA } from "../packages/contracts/domain-control-monitor.mjs";
|
|
3
3
|
function record(value) {
|
|
4
4
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
@@ -45,9 +45,12 @@ function junitCases(document) {
|
|
|
45
45
|
return [{ name: 'WHOISleuth result', failure: 'Unsupported document for JUnit conversion.' }];
|
|
46
46
|
}
|
|
47
47
|
export function formatCliJunit(document) {
|
|
48
|
-
const
|
|
48
|
+
const root = record(document);
|
|
49
|
+
const cases = junitCases(root);
|
|
49
50
|
const failures = cases.filter((item) => item.failure).length;
|
|
50
|
-
|
|
51
|
+
const reminders = root.schema === CLI_DOMAIN_CONTROL_MONITOR_SCHEMA
|
|
52
|
+
? `\n <system-out>${xml(`Renewal reviews due: ${Number(record(record(root.review).counts).due ?? 0)}`)}</system-out>` : '';
|
|
53
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<testsuite name="WHOISleuth" tests="${cases.length}" failures="${failures}">\n${cases.map((item) => ` <testcase classname="whoisleuth" name="${xml(item.name)}">${item.failure ? `<failure message="${xml(item.failure)}" />` : ''}</testcase>`).join('\n')}${reminders}\n</testsuite>\n`;
|
|
51
54
|
}
|
|
52
55
|
export function buildPostureSarif(document) {
|
|
53
56
|
const root = record(document);
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import {} from "./command-argument-grammar.mjs";
|
|
2
|
+
import {} from "./fail-policy.mjs";
|
|
3
|
+
import { commandDefaultNumber, commandDefaultText } from "./command-reference.mjs";
|
|
4
|
+
import { COLLECTION_COMMAND_DEFINITIONS } from "./collection-command-definitions.mjs";
|
|
5
|
+
import { terminalOptions, normalizedLabel, failPolicies, parseOutput, } from "./argument-values.mjs";
|
|
6
|
+
function parseLookupArguments(parsed) {
|
|
7
|
+
const output = parseOutput(parsed, COLLECTION_COMMAND_DEFINITIONS.lookup);
|
|
8
|
+
const selectedFailPolicies = failPolicies(parsed, 'lookup');
|
|
9
|
+
return {
|
|
10
|
+
action: 'lookup',
|
|
11
|
+
query: parsed.positionalValue('target'),
|
|
12
|
+
output,
|
|
13
|
+
deep: parsed.hasOption('--deep'),
|
|
14
|
+
detail: parsed.hasOption('--summary')
|
|
15
|
+
? 'summary'
|
|
16
|
+
: parsed.hasOption('--verbose')
|
|
17
|
+
? 'verbose'
|
|
18
|
+
: 'standard',
|
|
19
|
+
strictExit: parsed.hasOption('--strict-exit'),
|
|
20
|
+
events: parsed.hasOption('--events'),
|
|
21
|
+
plan: parsed.hasOption('--plan'),
|
|
22
|
+
includeAttribution: !parsed.hasOption('--no-attribution'),
|
|
23
|
+
observerLabel: normalizedLabel(parsed, '--observer', 80),
|
|
24
|
+
vantageLabel: normalizedLabel(parsed, '--vantage', 80),
|
|
25
|
+
...terminalOptions(parsed),
|
|
26
|
+
...(parsed.hasOption('--browse') ? { browse: true } : {}),
|
|
27
|
+
...(parsed.hasOption('--exact-url') ? { exactUrl: true } : {}),
|
|
28
|
+
...(parsed.optionValue('--save-lookup')
|
|
29
|
+
? { saveLookup: parsed.optionValue('--save-lookup') }
|
|
30
|
+
: {}),
|
|
31
|
+
...(selectedFailPolicies ? { failOn: selectedFailPolicies } : {}),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function parseBulkArguments(parsed) {
|
|
35
|
+
const deep = parsed.hasOption('--deep');
|
|
36
|
+
const selectedFailPolicies = failPolicies(parsed, 'bulk');
|
|
37
|
+
const output = parseOutput(parsed, COLLECTION_COMMAND_DEFINITIONS.bulk);
|
|
38
|
+
const filter = parsed.hasOption('--registered-only')
|
|
39
|
+
? 'registered'
|
|
40
|
+
: parsed.hasOption('--inconclusive-only')
|
|
41
|
+
? 'inconclusive'
|
|
42
|
+
: parsed.hasOption('--errors-only')
|
|
43
|
+
? 'errors'
|
|
44
|
+
: 'all';
|
|
45
|
+
return {
|
|
46
|
+
action: 'bulk',
|
|
47
|
+
source: parsed.positionalValue('source'),
|
|
48
|
+
output,
|
|
49
|
+
deep,
|
|
50
|
+
concurrency: parsed.integerOption('--concurrency') ?? commandDefaultNumber('bulk', '--concurrency', deep),
|
|
51
|
+
checkpoint: parsed.optionValue('--checkpoint'),
|
|
52
|
+
resume: parsed.hasOption('--resume'),
|
|
53
|
+
events: parsed.hasOption('--events'),
|
|
54
|
+
plan: parsed.hasOption('--plan'),
|
|
55
|
+
filter,
|
|
56
|
+
...terminalOptions(parsed),
|
|
57
|
+
...(selectedFailPolicies ? { failOn: selectedFailPolicies } : {}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function parseDiscoverArguments(parsed) {
|
|
61
|
+
return {
|
|
62
|
+
action: 'discover',
|
|
63
|
+
seed: parsed.positionalValue('subject'),
|
|
64
|
+
output: parseOutput(parsed, COLLECTION_COMMAND_DEFINITIONS.discover),
|
|
65
|
+
...discoveryValues(parsed),
|
|
66
|
+
snapshotSource: parsed.optionValue('--snapshot'),
|
|
67
|
+
...terminalOptions(parsed),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function discoveryValues(parsed) {
|
|
71
|
+
const familyText = parsed.optionValue('--families');
|
|
72
|
+
return {
|
|
73
|
+
preset: (familyText
|
|
74
|
+
? 'custom'
|
|
75
|
+
: (parsed.optionValue('--preset') ?? commandDefaultText('discover', '--preset'))),
|
|
76
|
+
keyboardLayout: (parsed.optionValue('--keyboard') ??
|
|
77
|
+
commandDefaultText('discover', '--keyboard')),
|
|
78
|
+
tldText: parsed.optionValue('--tlds'),
|
|
79
|
+
dictionarySource: parsed.optionValue('--dictionary'),
|
|
80
|
+
familyText,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function parseDiscoverScanArguments(parsed) {
|
|
84
|
+
const deep = parsed.hasOption('--deep');
|
|
85
|
+
const selectedFailPolicies = failPolicies(parsed, 'discover-scan');
|
|
86
|
+
const filter = parsed.hasOption('--registered-only')
|
|
87
|
+
? 'registered'
|
|
88
|
+
: parsed.hasOption('--inconclusive-only')
|
|
89
|
+
? 'inconclusive'
|
|
90
|
+
: parsed.hasOption('--acquisition-only')
|
|
91
|
+
? 'acquisition'
|
|
92
|
+
: parsed.hasOption('--suppressed-only')
|
|
93
|
+
? 'suppressed'
|
|
94
|
+
: 'all';
|
|
95
|
+
return {
|
|
96
|
+
action: 'discover-scan',
|
|
97
|
+
seed: parsed.positionalValue('subject'),
|
|
98
|
+
output: parseOutput(parsed, COLLECTION_COMMAND_DEFINITIONS['discover-scan']),
|
|
99
|
+
...discoveryValues(parsed),
|
|
100
|
+
deep,
|
|
101
|
+
scanLimit: parsed.integerOption('--scan-limit') ??
|
|
102
|
+
commandDefaultNumber('discover-scan', '--scan-limit', deep),
|
|
103
|
+
chunkSize: parsed.integerOption('--chunk-size') ?? commandDefaultNumber('discover-scan', '--chunk-size'),
|
|
104
|
+
concurrency: parsed.integerOption('--concurrency') ??
|
|
105
|
+
commandDefaultNumber('discover-scan', '--concurrency', deep),
|
|
106
|
+
checkpoint: parsed.optionValue('--checkpoint'),
|
|
107
|
+
resume: parsed.hasOption('--resume'),
|
|
108
|
+
resolverText: parsed.optionValue('--resolver'),
|
|
109
|
+
observationSnapshot: parsed.optionValue('--observation-snapshot'),
|
|
110
|
+
allowlistSource: parsed.optionValue('--allowlist'),
|
|
111
|
+
filter,
|
|
112
|
+
events: parsed.hasOption('--events'),
|
|
113
|
+
plan: parsed.hasOption('--plan'),
|
|
114
|
+
...terminalOptions(parsed),
|
|
115
|
+
...(selectedFailPolicies ? { failOn: selectedFailPolicies } : {}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export const COLLECTION_ARGUMENT_PARSERS = Object.freeze({
|
|
119
|
+
lookup: parseLookupArguments,
|
|
120
|
+
bulk: parseBulkArguments,
|
|
121
|
+
discover: parseDiscoverArguments,
|
|
122
|
+
'discover-scan': parseDiscoverScanArguments,
|
|
123
|
+
});
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { positional, OPTIONAL_FILE_POSITIONAL, OPTIONAL_TEXT_POSITIONAL, constraint, commandSeed, } from "./command-definition.mjs";
|
|
2
|
+
import { bulkQueryLimit, bulkConcurrencyLimit } from "../lib/bulk-limits.mjs";
|
|
3
|
+
const COLLECTION_CEILINGS = `${bulkQueryLimit('fast')} fast or ${bulkQueryLimit('deep')} deep`;
|
|
4
|
+
const CONCURRENCY_CEILINGS = `${bulkConcurrencyLimit('fast')} fast or ${bulkConcurrencyLimit('deep')} deep`;
|
|
5
|
+
export const COLLECTION_COMMAND_DEFINITIONS = Object.freeze({
|
|
6
|
+
lookup: commandSeed({
|
|
7
|
+
reference: {
|
|
8
|
+
description: 'Collect registration evidence for one domain, IP, or ASN.',
|
|
9
|
+
example: 'whoisleuth lookup example.test --deep --browse',
|
|
10
|
+
boundary: 'Fast is the default. An ICANN-recognised public domain, reserved documentation domain, IP, or ASN may occupy command position as shorthand; it delegates to this same parser and URL-like input requires the explicit lookup command. Deep mode adds bounded WHOIS, DNS, HTTP, TLS, technology, posture, and network context where applicable. --deep --exact-url explicitly sends the input URL path and query to the website, without its fragment; ordinary URL input sends only the hostname. --plan remains offline and omits the selected URL. Retained paths and page-derived text require review before sharing. A full Deep homepage observation can derive fixed publication and delivery/cache summaries from the same response without retaining raw metadata values or making another request. --browse opens before collection, shows aggregate Fast progress or independently settled planned Deep sources, and then navigates allowlisted retained fields in the completed document. Press ? for help and / to search rendered panel text only. Closing during collection cancels without a partial document. --save-lookup writes the exact completed private JSON only after a normal browser close; it can contain normalised evidence omitted from panels and refuses an existing path.',
|
|
11
|
+
},
|
|
12
|
+
collection: {
|
|
13
|
+
scope: 'Accepts one target. Fast is the default; deep collection must be selected explicitly.',
|
|
14
|
+
},
|
|
15
|
+
summary: 'Collect one domain, IP, or ASN',
|
|
16
|
+
options: [
|
|
17
|
+
'--json',
|
|
18
|
+
'--junit',
|
|
19
|
+
'--markdown',
|
|
20
|
+
'--html',
|
|
21
|
+
'--no-attribution',
|
|
22
|
+
'--fast',
|
|
23
|
+
'--deep',
|
|
24
|
+
'--exact-url',
|
|
25
|
+
'--observer',
|
|
26
|
+
'--vantage',
|
|
27
|
+
'--plan',
|
|
28
|
+
'--summary',
|
|
29
|
+
'--verbose',
|
|
30
|
+
'--browse',
|
|
31
|
+
'--save-lookup',
|
|
32
|
+
'--strict-exit',
|
|
33
|
+
'--fail-on',
|
|
34
|
+
'--events',
|
|
35
|
+
'--quiet',
|
|
36
|
+
'--no-color',
|
|
37
|
+
],
|
|
38
|
+
positionals: Object.freeze([
|
|
39
|
+
positional('target', 'text', 0, 1, [], 'argv_or_stdin', ['--browse']),
|
|
40
|
+
]),
|
|
41
|
+
constraints: Object.freeze([
|
|
42
|
+
constraint({
|
|
43
|
+
kind: 'mutually_exclusive',
|
|
44
|
+
options: ['--json', '--junit', '--markdown', '--html'],
|
|
45
|
+
}),
|
|
46
|
+
constraint({ kind: 'mutually_exclusive', options: ['--fast', '--deep'] }),
|
|
47
|
+
constraint({ kind: 'mutually_exclusive', options: ['--summary', '--verbose'] }),
|
|
48
|
+
constraint({ kind: 'requires_all', option: '--save-lookup', requiredOptions: ['--browse'] }),
|
|
49
|
+
constraint({ kind: 'requires_all', option: '--exact-url', requiredOptions: ['--deep'] }),
|
|
50
|
+
constraint({
|
|
51
|
+
kind: 'excludes_all',
|
|
52
|
+
option: '--summary',
|
|
53
|
+
excludedOptions: ['--json', '--junit', '--markdown', '--html'],
|
|
54
|
+
}),
|
|
55
|
+
constraint({
|
|
56
|
+
kind: 'excludes_all',
|
|
57
|
+
option: '--verbose',
|
|
58
|
+
excludedOptions: ['--json', '--junit', '--markdown', '--html'],
|
|
59
|
+
}),
|
|
60
|
+
constraint({
|
|
61
|
+
kind: 'excludes_all',
|
|
62
|
+
option: '--browse',
|
|
63
|
+
excludedOptions: [
|
|
64
|
+
'--json',
|
|
65
|
+
'--junit',
|
|
66
|
+
'--markdown',
|
|
67
|
+
'--html',
|
|
68
|
+
'--summary',
|
|
69
|
+
'--verbose',
|
|
70
|
+
'--events',
|
|
71
|
+
'--plan',
|
|
72
|
+
'--quiet',
|
|
73
|
+
],
|
|
74
|
+
}),
|
|
75
|
+
constraint({
|
|
76
|
+
kind: 'requires_any',
|
|
77
|
+
option: '--no-attribution',
|
|
78
|
+
requiredOptions: ['--markdown', '--html'],
|
|
79
|
+
}),
|
|
80
|
+
constraint({
|
|
81
|
+
kind: 'excludes_all',
|
|
82
|
+
option: '--plan',
|
|
83
|
+
excludedOptions: [
|
|
84
|
+
'--junit',
|
|
85
|
+
'--markdown',
|
|
86
|
+
'--html',
|
|
87
|
+
'--summary',
|
|
88
|
+
'--verbose',
|
|
89
|
+
'--strict-exit',
|
|
90
|
+
'--events',
|
|
91
|
+
'--quiet',
|
|
92
|
+
'--fail-on',
|
|
93
|
+
],
|
|
94
|
+
}),
|
|
95
|
+
]),
|
|
96
|
+
handlerOwner: 'lookup',
|
|
97
|
+
networkEffect: 'conditional_network',
|
|
98
|
+
common: true,
|
|
99
|
+
schemaIdentifiers: Object.freeze(['whoisleuth.cli.lookup', 'whoisleuth.cli.lookup-plan']),
|
|
100
|
+
primaryArtefacts: Object.freeze(['Source-qualified Lookup', 'Lookup request plan']),
|
|
101
|
+
planSupport: true,
|
|
102
|
+
additionalOutputFormats: Object.freeze([]),
|
|
103
|
+
bootstrapProfile: 'allowed',
|
|
104
|
+
}),
|
|
105
|
+
bulk: commandSeed({
|
|
106
|
+
reference: {
|
|
107
|
+
description: 'Triage newline-delimited domains, IPs, or ASNs with bounded concurrency.',
|
|
108
|
+
example: 'cat domains.txt | whoisleuth bulk --jsonl',
|
|
109
|
+
boundary: 'Fast and deep jobs use separate concurrency ceilings. Filters affect output only; collection failures and inconclusive authority states remain explicit in JSON, JSONL, and CSV. --csv-with-metadata adds source versions, separate observation and report times, collection origin and diagnostic states; --csv retains the compact columns.',
|
|
110
|
+
},
|
|
111
|
+
collection: {
|
|
112
|
+
scope: `Accepts at most ${COLLECTION_CEILINGS} targets, with concurrency capped at ${CONCURRENCY_CEILINGS}.`,
|
|
113
|
+
},
|
|
114
|
+
summary: 'Run bounded multi-target collection',
|
|
115
|
+
options: [
|
|
116
|
+
'--json',
|
|
117
|
+
'--jsonl',
|
|
118
|
+
'--junit',
|
|
119
|
+
'--csv',
|
|
120
|
+
'--csv-with-metadata',
|
|
121
|
+
'--domains',
|
|
122
|
+
'--queries',
|
|
123
|
+
'--registered-only',
|
|
124
|
+
'--inconclusive-only',
|
|
125
|
+
'--errors-only',
|
|
126
|
+
'--fast',
|
|
127
|
+
'--deep',
|
|
128
|
+
'--concurrency',
|
|
129
|
+
'--checkpoint',
|
|
130
|
+
'--resume',
|
|
131
|
+
'--events',
|
|
132
|
+
'--plan',
|
|
133
|
+
'--fail-on',
|
|
134
|
+
'--quiet',
|
|
135
|
+
'--no-color',
|
|
136
|
+
],
|
|
137
|
+
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
138
|
+
constraints: Object.freeze([
|
|
139
|
+
constraint({
|
|
140
|
+
kind: 'mutually_exclusive',
|
|
141
|
+
options: [
|
|
142
|
+
'--json',
|
|
143
|
+
'--jsonl',
|
|
144
|
+
'--junit',
|
|
145
|
+
'--csv',
|
|
146
|
+
'--csv-with-metadata',
|
|
147
|
+
'--domains',
|
|
148
|
+
'--queries',
|
|
149
|
+
],
|
|
150
|
+
}),
|
|
151
|
+
constraint({
|
|
152
|
+
kind: 'mutually_exclusive',
|
|
153
|
+
options: ['--registered-only', '--inconclusive-only', '--errors-only'],
|
|
154
|
+
}),
|
|
155
|
+
constraint({ kind: 'mutually_exclusive', options: ['--fast', '--deep'] }),
|
|
156
|
+
constraint({ kind: 'requires_all', option: '--resume', requiredOptions: ['--checkpoint'] }),
|
|
157
|
+
constraint({
|
|
158
|
+
kind: 'excludes_all',
|
|
159
|
+
option: '--plan',
|
|
160
|
+
excludedOptions: [
|
|
161
|
+
'--jsonl',
|
|
162
|
+
'--junit',
|
|
163
|
+
'--csv',
|
|
164
|
+
'--csv-with-metadata',
|
|
165
|
+
'--domains',
|
|
166
|
+
'--queries',
|
|
167
|
+
'--events',
|
|
168
|
+
'--checkpoint',
|
|
169
|
+
'--resume',
|
|
170
|
+
'--quiet',
|
|
171
|
+
'--fail-on',
|
|
172
|
+
],
|
|
173
|
+
}),
|
|
174
|
+
]),
|
|
175
|
+
handlerOwner: 'bulk',
|
|
176
|
+
networkEffect: 'conditional_network',
|
|
177
|
+
common: true,
|
|
178
|
+
schemaIdentifiers: Object.freeze([
|
|
179
|
+
'whoisleuth.cli.bulk',
|
|
180
|
+
'whoisleuth.cli.bulk.item',
|
|
181
|
+
'whoisleuth.cli.bulk-checkpoint',
|
|
182
|
+
]),
|
|
183
|
+
primaryArtefacts: Object.freeze(['Bulk result', 'Bulk checkpoint']),
|
|
184
|
+
planSupport: true,
|
|
185
|
+
additionalOutputFormats: Object.freeze([]),
|
|
186
|
+
bootstrapProfile: 'allowed',
|
|
187
|
+
}),
|
|
188
|
+
discover: commandSeed({
|
|
189
|
+
reference: {
|
|
190
|
+
description: 'Generate bounded lookalike-domain candidates from a brand or registrable domain, including multi-part public suffixes.',
|
|
191
|
+
example: 'whoisleuth discover example.test --preset common --jsonl',
|
|
192
|
+
boundary: 'Generation and optional local snapshot comparison are offline. Candidates are leads only and are not resolved, registered, or classified as malicious.',
|
|
193
|
+
},
|
|
194
|
+
collection: {
|
|
195
|
+
scope: 'Generates a bounded candidate set from local rules, dictionaries, and optional saved snapshots.',
|
|
196
|
+
},
|
|
197
|
+
summary: 'Generate lookalike candidates offline',
|
|
198
|
+
options: [
|
|
199
|
+
'--tlds',
|
|
200
|
+
'--preset',
|
|
201
|
+
'--families',
|
|
202
|
+
'--keyboard',
|
|
203
|
+
'--dictionary',
|
|
204
|
+
'--snapshot',
|
|
205
|
+
'--json',
|
|
206
|
+
'--jsonl',
|
|
207
|
+
'--domains',
|
|
208
|
+
'--quiet',
|
|
209
|
+
'--no-color',
|
|
210
|
+
],
|
|
211
|
+
positionals: OPTIONAL_TEXT_POSITIONAL,
|
|
212
|
+
constraints: Object.freeze([
|
|
213
|
+
constraint({ kind: 'mutually_exclusive', options: ['--json', '--jsonl', '--domains'] }),
|
|
214
|
+
constraint({ kind: 'mutually_exclusive', options: ['--preset', '--families'] }),
|
|
215
|
+
constraint({
|
|
216
|
+
kind: 'value_excludes',
|
|
217
|
+
option: '--preset',
|
|
218
|
+
value: 'common',
|
|
219
|
+
excludedOptions: ['--dictionary'],
|
|
220
|
+
}),
|
|
221
|
+
]),
|
|
222
|
+
handlerOwner: 'discovery',
|
|
223
|
+
networkEffect: 'offline',
|
|
224
|
+
common: true,
|
|
225
|
+
schemaIdentifiers: Object.freeze([
|
|
226
|
+
'whoisleuth.cli.discover',
|
|
227
|
+
'whoisleuth.cli.discover.item',
|
|
228
|
+
'whoisleuth.cli.discovery-snapshot',
|
|
229
|
+
]),
|
|
230
|
+
primaryArtefacts: Object.freeze(['Candidate set', 'Discovery snapshot']),
|
|
231
|
+
planSupport: false,
|
|
232
|
+
additionalOutputFormats: Object.freeze([]),
|
|
233
|
+
bootstrapProfile: 'allowed',
|
|
234
|
+
}),
|
|
235
|
+
'discover-scan': commandSeed({
|
|
236
|
+
reference: {
|
|
237
|
+
description: 'Generate a bounded candidate set, collect a selected subset, and produce a supervised review queue.',
|
|
238
|
+
example: 'whoisleuth discover-scan example.test --scan-limit 50 --checkpoint scan.json --json',
|
|
239
|
+
boundary: `This command performs network collection. Fast compact lookup is the default; deep mode is capped at ${bulkQueryLimit('deep')} candidates. Allowlisting changes review priority only and shared infrastructure remains a lead, not attribution. --csv-with-metadata adds source versions, separate observation and report times, collection origin and diagnostic states; --csv retains the compact columns.`,
|
|
240
|
+
},
|
|
241
|
+
collection: {
|
|
242
|
+
scope: `Scans at most ${COLLECTION_CEILINGS} candidates, with concurrency capped at ${CONCURRENCY_CEILINGS}.`,
|
|
243
|
+
},
|
|
244
|
+
summary: 'Collect a supervised candidate review queue',
|
|
245
|
+
options: [
|
|
246
|
+
'--tlds',
|
|
247
|
+
'--preset',
|
|
248
|
+
'--families',
|
|
249
|
+
'--keyboard',
|
|
250
|
+
'--dictionary',
|
|
251
|
+
'--fast',
|
|
252
|
+
'--deep',
|
|
253
|
+
'--scan-limit',
|
|
254
|
+
'--chunk-size',
|
|
255
|
+
'--concurrency',
|
|
256
|
+
'--resolver',
|
|
257
|
+
'--allowlist',
|
|
258
|
+
'--checkpoint',
|
|
259
|
+
'--resume',
|
|
260
|
+
'--observation-snapshot',
|
|
261
|
+
'--registered-only',
|
|
262
|
+
'--inconclusive-only',
|
|
263
|
+
'--acquisition-only',
|
|
264
|
+
'--suppressed-only',
|
|
265
|
+
'--events',
|
|
266
|
+
'--plan',
|
|
267
|
+
'--fail-on',
|
|
268
|
+
'--json',
|
|
269
|
+
'--jsonl',
|
|
270
|
+
'--csv',
|
|
271
|
+
'--csv-with-metadata',
|
|
272
|
+
'--domains',
|
|
273
|
+
'--quiet',
|
|
274
|
+
'--no-color',
|
|
275
|
+
],
|
|
276
|
+
positionals: OPTIONAL_TEXT_POSITIONAL,
|
|
277
|
+
constraints: Object.freeze([
|
|
278
|
+
constraint({
|
|
279
|
+
kind: 'mutually_exclusive',
|
|
280
|
+
options: ['--json', '--jsonl', '--csv', '--csv-with-metadata', '--domains'],
|
|
281
|
+
}),
|
|
282
|
+
constraint({ kind: 'mutually_exclusive', options: ['--preset', '--families'] }),
|
|
283
|
+
constraint({ kind: 'mutually_exclusive', options: ['--fast', '--deep'] }),
|
|
284
|
+
constraint({
|
|
285
|
+
kind: 'mutually_exclusive',
|
|
286
|
+
options: [
|
|
287
|
+
'--registered-only',
|
|
288
|
+
'--inconclusive-only',
|
|
289
|
+
'--acquisition-only',
|
|
290
|
+
'--suppressed-only',
|
|
291
|
+
],
|
|
292
|
+
}),
|
|
293
|
+
constraint({ kind: 'requires_all', option: '--resume', requiredOptions: ['--checkpoint'] }),
|
|
294
|
+
constraint({
|
|
295
|
+
kind: 'value_excludes',
|
|
296
|
+
option: '--preset',
|
|
297
|
+
value: 'common',
|
|
298
|
+
excludedOptions: ['--dictionary'],
|
|
299
|
+
}),
|
|
300
|
+
constraint({
|
|
301
|
+
kind: 'excludes_all',
|
|
302
|
+
option: '--plan',
|
|
303
|
+
excludedOptions: [
|
|
304
|
+
'--jsonl',
|
|
305
|
+
'--csv',
|
|
306
|
+
'--csv-with-metadata',
|
|
307
|
+
'--domains',
|
|
308
|
+
'--events',
|
|
309
|
+
'--checkpoint',
|
|
310
|
+
'--resume',
|
|
311
|
+
'--observation-snapshot',
|
|
312
|
+
'--quiet',
|
|
313
|
+
'--fail-on',
|
|
314
|
+
],
|
|
315
|
+
}),
|
|
316
|
+
]),
|
|
317
|
+
handlerOwner: 'discovery_scan',
|
|
318
|
+
networkEffect: 'conditional_network',
|
|
319
|
+
common: true,
|
|
320
|
+
schemaIdentifiers: Object.freeze([
|
|
321
|
+
'whoisleuth.cli.discovery-scan',
|
|
322
|
+
'whoisleuth.cli.discovery-scan.item',
|
|
323
|
+
'whoisleuth.cli.discovery-observation-snapshot',
|
|
324
|
+
]),
|
|
325
|
+
primaryArtefacts: Object.freeze(['Reviewed candidate queue', 'Observation snapshot']),
|
|
326
|
+
planSupport: true,
|
|
327
|
+
additionalOutputFormats: Object.freeze([]),
|
|
328
|
+
bootstrapProfile: 'allowed',
|
|
329
|
+
}),
|
|
330
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bulkQueryLimit } from "../lib/bulk-limits.mjs";
|
|
1
2
|
export const CLI_COLLECTION_PREFLIGHT_SCHEMA = 'whoisleuth.cli.collection-preflight';
|
|
2
3
|
export const CLI_COLLECTION_PREFLIGHT_VERSION = 1;
|
|
3
4
|
export function buildCollectionPreflight(options) {
|
|
@@ -12,7 +13,7 @@ export function buildCollectionPreflight(options) {
|
|
|
12
13
|
networkRequestsMade: false,
|
|
13
14
|
scope: Object.freeze({
|
|
14
15
|
selectedTargets: options.targetCount,
|
|
15
|
-
commandTargetLimit: options.
|
|
16
|
+
commandTargetLimit: bulkQueryLimit(options.deep ? 'deep' : 'fast'),
|
|
16
17
|
concurrency: options.concurrency,
|
|
17
18
|
sourceFamilies: Object.freeze(sourceFamilies),
|
|
18
19
|
exactRequestCountKnown: false,
|
|
@@ -53,6 +54,12 @@ export function formatCollectionPreflight(document) {
|
|
|
53
54
|
...document.scope.sourceFamilies.map((source) => ` ${source}`),
|
|
54
55
|
'',
|
|
55
56
|
...document.disclosure.map((item) => `Disclosure: ${item}`),
|
|
57
|
+
'',
|
|
58
|
+
`Output format: ${document.persistence.output}`,
|
|
59
|
+
`Checkpoint: ${document.persistence.checkpoint ? 'enabled' : 'not written'}. ${document.persistence.note}`,
|
|
60
|
+
...(document.review.allowlistAffectsPriorityOnly ? ['Allowlist: affects review priority only; it does not exclude targets from collection.'] : []),
|
|
61
|
+
'Enforcement: none. This plan does not report, block or remove a target.',
|
|
62
|
+
'',
|
|
56
63
|
...document.limitations.map((item) => `Limitation: ${item}`),
|
|
57
64
|
'',
|
|
58
65
|
].join('\n');
|