@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,33 @@
|
|
|
1
|
+
import { buildManagedIndicatorRevision, managedIndicatorState, parseManagedIndicatorJson, readManagedIndicatorSet } from "../packages/interchange/managed-indicator-set.mjs";
|
|
2
|
+
import { exportManagedIndicators } from "../packages/interchange/managed-indicator-export.mjs";
|
|
3
|
+
import { MAX_MANAGED_INDICATOR_PLAN_BYTES, MAX_MANAGED_INDICATOR_SET_BYTES } from "../packages/contracts/analyst-interchange.mjs";
|
|
4
|
+
import { formatJsonDocument } from "./formatters/json.mjs";
|
|
5
|
+
import { safeTerminalValue } from "./formatters/terminal.mjs";
|
|
6
|
+
import EXIT_CODES from "./exit-codes.mjs";
|
|
7
|
+
export async function runIndicatorSetCommand(args, dependencies, context) {
|
|
8
|
+
context.setFailureLabel('Indicator revision');
|
|
9
|
+
const raw = dependencies.readArtifactInput ? await dependencies.readArtifactInput(args.source)
|
|
10
|
+
: await context.readInput(args.source, args.operation === 'revise' ? MAX_MANAGED_INDICATOR_PLAN_BYTES : MAX_MANAGED_INDICATOR_SET_BYTES, 'Indicator input');
|
|
11
|
+
const input = parseManagedIndicatorJson(raw, args.operation === 'revise');
|
|
12
|
+
const now = context.now();
|
|
13
|
+
const revision = args.operation === 'revise' ? await buildManagedIndicatorRevision(input, now) : null;
|
|
14
|
+
const manifest = revision?.manifest ?? await readManagedIndicatorSet(input);
|
|
15
|
+
dependencies.signal?.throwIfAborted();
|
|
16
|
+
if (args.quiet)
|
|
17
|
+
return EXIT_CODES.SUCCESS;
|
|
18
|
+
if (args.operation === 'stix' || args.operation === 'misp')
|
|
19
|
+
context.writeStdout((await exportManagedIndicators(manifest, args.operation)).content);
|
|
20
|
+
else if (args.output === 'json')
|
|
21
|
+
context.writeStdout(formatJsonDocument(manifest));
|
|
22
|
+
else {
|
|
23
|
+
const lines = [`${safeTerminalValue(manifest.name)} · revision ${manifest.revision}`, `Set ${manifest.id}`, `Content digest ${manifest.integrity.digestSha256}`,
|
|
24
|
+
'Content verified; authorship and earlier revisions are not authenticated.'];
|
|
25
|
+
if (revision)
|
|
26
|
+
lines.push(`Changes: ${revision.changes.length} · unchanged: ${revision.unchanged} · excluded candidates: ${revision.exclusions.length}`);
|
|
27
|
+
for (const entry of manifest.entries)
|
|
28
|
+
lines.push(`${entry.domain} · ${managedIndicatorState(entry, now)} · ${entry.id}`, ` Review expires ${entry.expiresAt} · source observed ${entry.observation.observedAt ?? 'time unavailable'}`, ` ${safeTerminalValue(entry.reviewBasis)}`);
|
|
29
|
+
lines.push('Save the manifest for the next revision. Nothing was submitted or applied.');
|
|
30
|
+
context.writeStdout(context.terminal(`${lines.join('\n')}\n`, args.color));
|
|
31
|
+
}
|
|
32
|
+
return EXIT_CODES.SUCCESS;
|
|
33
|
+
}
|
|
@@ -1,47 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
'registry-doctor',
|
|
8
|
-
'registry-cohort',
|
|
9
|
-
'registry-scaffold',
|
|
10
|
-
'risk-calibrate',
|
|
11
|
-
'lookalike-calibrate',
|
|
12
|
-
]);
|
|
13
|
-
const REVIEW_INLINE_COMMANDS = Object.freeze([
|
|
14
|
-
'verify-artifact',
|
|
15
|
-
'interchange-report',
|
|
16
|
-
'source-report',
|
|
17
|
-
'compare',
|
|
18
|
-
'page-compare',
|
|
19
|
-
'mail-review',
|
|
20
|
-
'mail-headers',
|
|
21
|
-
'review-evidence',
|
|
22
|
-
'brief',
|
|
23
|
-
'case',
|
|
24
|
-
'case-pack',
|
|
25
|
-
]);
|
|
26
|
-
const ASSURANCE_INLINE_COMMANDS = Object.freeze([
|
|
27
|
-
'manifest',
|
|
28
|
-
'map-observations',
|
|
29
|
-
'oam-export',
|
|
30
|
-
'domain-control',
|
|
31
|
-
'assurance',
|
|
32
|
-
'change-packet',
|
|
33
|
-
'sharing-review',
|
|
34
|
-
'ct-intake',
|
|
35
|
-
]);
|
|
36
|
-
const WORKFLOW_INLINE_COMMANDS = Object.freeze([
|
|
37
|
-
'monitor-once',
|
|
38
|
-
'workflow-plan',
|
|
39
|
-
'workflow-run',
|
|
40
|
-
]);
|
|
41
|
-
const HISTORY_INLINE_COMMANDS = Object.freeze([
|
|
42
|
-
'diff',
|
|
43
|
-
'reconcile',
|
|
44
|
-
'timeline',
|
|
45
|
-
'export',
|
|
46
|
-
]);
|
|
47
|
-
export { ASSURANCE_INLINE_COMMANDS, HISTORY_INLINE_COMMANDS, REVIEW_INLINE_COMMANDS, SUPPORT_INLINE_COMMANDS, WORKFLOW_INLINE_COMMANDS, };
|
|
1
|
+
import { inlineCommandsFor } from "./command-reference.mjs";
|
|
2
|
+
export const SUPPORT_INLINE_COMMANDS = inlineCommandsFor('support');
|
|
3
|
+
export const REVIEW_INLINE_COMMANDS = inlineCommandsFor('review');
|
|
4
|
+
export const ASSURANCE_INLINE_COMMANDS = inlineCommandsFor('assurance');
|
|
5
|
+
export const WORKFLOW_INLINE_COMMANDS = inlineCommandsFor('workflow');
|
|
6
|
+
export const HISTORY_INLINE_COMMANDS = inlineCommandsFor('history');
|
|
@@ -1,35 +1,8 @@
|
|
|
1
|
-
import { CLI_COMMANDS,
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
Object.freeze({ family: 'support', commands: SUPPORT_INLINE_COMMANDS }),
|
|
5
|
-
Object.freeze({ family: 'review', commands: REVIEW_INLINE_COMMANDS }),
|
|
6
|
-
Object.freeze({ family: 'assurance', commands: ASSURANCE_INLINE_COMMANDS }),
|
|
7
|
-
Object.freeze({ family: 'workflow', commands: WORKFLOW_INLINE_COMMANDS }),
|
|
8
|
-
Object.freeze({ family: 'history', commands: HISTORY_INLINE_COMMANDS }),
|
|
9
|
-
]);
|
|
10
|
-
function buildInlineCommandOwnership() {
|
|
11
|
-
const ownership = new Map();
|
|
12
|
-
for (const { family, commands } of FAMILY_COMMANDS) {
|
|
13
|
-
for (const command of commands) {
|
|
14
|
-
if (ownership.has(command))
|
|
15
|
-
throw new Error(`Inline CLI command ${command} has more than one command-family owner.`);
|
|
16
|
-
ownership.set(command, family);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
return ownership;
|
|
20
|
-
}
|
|
21
|
-
const INLINE_COMMAND_OWNERSHIP = buildInlineCommandOwnership();
|
|
22
|
-
const INLINE_CLI_COMMANDS = Object.freeze(CLI_COMMANDS.filter((command) => commandDefinition(command).execution.handlerOwner === 'inline'));
|
|
23
|
-
function validateInlineCommandOwnership() {
|
|
24
|
-
const missing = INLINE_CLI_COMMANDS.filter((command) => !INLINE_COMMAND_OWNERSHIP.has(command));
|
|
25
|
-
const unexpected = [...INLINE_COMMAND_OWNERSHIP.keys()].filter((command) => !INLINE_CLI_COMMANDS.includes(command));
|
|
26
|
-
if (missing.length || unexpected.length) {
|
|
27
|
-
throw new Error(`Inline CLI command-family ownership is inconsistent (missing: ${missing.join(', ') || 'none'}; unexpected: ${unexpected.join(', ') || 'none'}).`);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
validateInlineCommandOwnership();
|
|
1
|
+
import { CLI_COMMANDS, INLINE_COMMAND_FAMILIES, inlineCommandFamily, inlineCommandsFor, } from "./command-reference.mjs";
|
|
2
|
+
const FAMILY_COMMANDS = Object.freeze(INLINE_COMMAND_FAMILIES.map(family => Object.freeze({ family, commands: inlineCommandsFor(family) })));
|
|
3
|
+
const INLINE_CLI_COMMANDS = Object.freeze(CLI_COMMANDS.filter((command) => inlineCommandFamily(command) !== null));
|
|
31
4
|
async function runInlineCommand(args, dependencies, context) {
|
|
32
|
-
const family =
|
|
5
|
+
const family = inlineCommandFamily(args.action);
|
|
33
6
|
if (family === 'support') {
|
|
34
7
|
const { runSupportCommand } = await import("./support-command-runner.mjs");
|
|
35
8
|
return runSupportCommand(args, dependencies, context);
|
package/cli/input.mjs
CHANGED
|
@@ -64,7 +64,7 @@ export async function readCliTextInput(source, stdin, options) {
|
|
|
64
64
|
if (error instanceof CliUsageError)
|
|
65
65
|
throw error;
|
|
66
66
|
const message = error instanceof Error ? error.message : `${options.label} could not be read.`;
|
|
67
|
-
throw new CliUsageError(message);
|
|
67
|
+
throw new CliUsageError(message, error instanceof TypeError ? 'invalid_input' : 'input_unavailable');
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
if (!stdin || stdin.isTTY)
|
|
@@ -142,7 +142,7 @@ export async function readCliHeaderInput(source, stdin, options) {
|
|
|
142
142
|
if (error instanceof CliUsageError || options.signal?.aborted)
|
|
143
143
|
throw error;
|
|
144
144
|
const message = error instanceof Error ? error.message : `${options.label} could not be read.`;
|
|
145
|
-
throw new CliUsageError(message);
|
|
145
|
+
throw new CliUsageError(message, error instanceof TypeError ? 'invalid_input' : 'input_unavailable');
|
|
146
146
|
}
|
|
147
147
|
finally {
|
|
148
148
|
await handle?.close();
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { readBoundedRegularFile } from "../lib/bounded-file.mjs";
|
|
2
|
+
import { MAX_MESSAGE_INTAKE_BYTES, MESSAGE_INTAKE_INPUTS } from "../packages/contracts/message-intake.mjs";
|
|
3
|
+
import { reviewSelectedInputInWorker } from "./selected-input-worker.mjs";
|
|
4
|
+
import { reviewIdentityIncident } from "../packages/investigation/identity-incident-review.mjs";
|
|
5
|
+
import { selectReceiverTrust, authenticationHeaderLabel } from "../packages/investigation/mail-authentication-review.mjs";
|
|
6
|
+
import { CliUsageError } from "./errors.mjs";
|
|
7
|
+
import { formatJsonDocument } from "./formatters/json.mjs";
|
|
8
|
+
import EXIT_CODES from "./exit-codes.mjs";
|
|
9
|
+
export function formatMessageIntake(report) {
|
|
10
|
+
const lines = [`Offline ${report.source.kind} intake`, `Review: ${report.coverage.state} · Extracted links: ${report.links.length} · Reviewed parts: ${report.coverage.reviewedParts}`,
|
|
11
|
+
`Source: ${report.source.digestSha256} · ${report.source.byteLength} bytes`];
|
|
12
|
+
for (const identity of report.identities)
|
|
13
|
+
lines.push(`Part ${identity.part} ${identity.role.replaceAll('_', ' ')}: ${identity.domain}`);
|
|
14
|
+
for (const header of report.authenticationReview.headers) {
|
|
15
|
+
lines.push(`${authenticationHeaderLabel(header)} · ${header.state} · receiver trust: ${header.receiverTrust.replaceAll('_', ' ')}`);
|
|
16
|
+
for (const claim of header.claims)
|
|
17
|
+
lines.push(` ${claim.method}/${claim.methodVersion}: ${claim.result} (${claim.state})${claim.domains.map(value => ` · ${value.property}=${value.domain}`).join('')}${claim.duplicateProperties.length ? ` · ambiguous properties: ${claim.duplicateProperties.join(', ')}` : ''}`);
|
|
18
|
+
if (header.duplicateOf)
|
|
19
|
+
lines.push(` Duplicate of header ${header.duplicateOf} in this part.`);
|
|
20
|
+
lines.push(...header.issues.map(issue => ` ${issue}`));
|
|
21
|
+
}
|
|
22
|
+
for (const link of report.links) {
|
|
23
|
+
lines.push(`${link.id}: ${link.origin} · ${link.source.replaceAll('_', ' ')}${link.parentId ? ` · supplied inside ${link.parentId}` : ''}`);
|
|
24
|
+
if (link.displayedHostname)
|
|
25
|
+
lines.push(` Displayed: ${link.displayedHostname} · ${link.displayedDestination.replaceAll('_', ' ')}`);
|
|
26
|
+
if (link.authorisation) {
|
|
27
|
+
lines.push(` Authorisation: ${link.authorisation.kind.replaceAll('_', ' ')} · client ${link.authorisation.clientId ?? 'not retained or ambiguous'}`);
|
|
28
|
+
if (link.authorisation.scopes.length)
|
|
29
|
+
lines.push(` Requested scopes: ${link.authorisation.scopes.join(', ')}`);
|
|
30
|
+
if (link.authorisation.redirectOrigin)
|
|
31
|
+
lines.push(` Supplied return origin: ${link.authorisation.redirectOrigin}`);
|
|
32
|
+
if (link.authorisation.duplicateParameters.length)
|
|
33
|
+
lines.push(` Ambiguous parameters: ${link.authorisation.duplicateParameters.join(', ')}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (report.actionHints.length)
|
|
37
|
+
lines.push(`Review wording: ${report.actionHints.map(value => value.replaceAll('_', ' ')).join(', ')}`);
|
|
38
|
+
if (report.documentReview) {
|
|
39
|
+
lines.push(`Document coverage: ${report.documentReview.state} · pages ${report.documentReview.reviewedPages}/${report.documentReview.pageCount ?? 'not a paginated review'}`);
|
|
40
|
+
for (const part of report.documentReview.parts)
|
|
41
|
+
lines.push(` ${part.id}${part.page ? ` · page ${part.page}` : ''} · ${part.kind} · ${part.identity} · ${part.digestSha256}`);
|
|
42
|
+
lines.push(...report.documentReview.notes);
|
|
43
|
+
}
|
|
44
|
+
if (report.harReview) {
|
|
45
|
+
lines.push('Recorded HTTP sequence (not replayed)');
|
|
46
|
+
for (const entry of report.harReview.entries)
|
|
47
|
+
lines.push(` ${entry.sequence}. ${entry.startedAt ?? 'time unavailable'} · ${entry.method} ${entry.origin ?? 'origin unavailable'} · status ${entry.status ?? 'unavailable'} · ${entry.mimeCategory} · ${entry.durationMs === null ? 'duration unavailable' : `${entry.durationMs} ms`}`);
|
|
48
|
+
if (report.harReview.invalidEntries)
|
|
49
|
+
lines.push(`Invalid request records omitted: ${report.harReview.invalidEntries}`);
|
|
50
|
+
}
|
|
51
|
+
if (report.identityEventReview) {
|
|
52
|
+
const review = report.identityEventReview;
|
|
53
|
+
const matches = new Map(review.comparison?.events.map(event => [event.sequence, event]));
|
|
54
|
+
lines.push(`Selected identity events: ${review.provider} · ${review.events.length} records · ${review.invalidEvents} invalid records omitted`);
|
|
55
|
+
for (const event of review.events) {
|
|
56
|
+
const match = matches.get(event.sequence);
|
|
57
|
+
lines.push(` ${event.sequence}. ${event.occurredAt ?? 'time unavailable'} · ${event.actorLabel ?? 'actor unavailable'} · ${event.kind} · ${event.result}`, ` applications ${event.applicationIds.join(', ') || 'unavailable'} · resource tenant ${event.tenantId ?? 'unavailable'} · protocol ${event.protocol}`);
|
|
58
|
+
if (match)
|
|
59
|
+
lines.push(` comparison ${match.state}: application ${match.application}, tenant ${match.tenant}, actor ${match.actor}, time ${match.time}`);
|
|
60
|
+
}
|
|
61
|
+
lines.push('Actor labels are file-local. Matching supplied fields does not establish account compromise.');
|
|
62
|
+
}
|
|
63
|
+
for (const step of report.identityRecovery.nextSteps)
|
|
64
|
+
lines.push(`Account response — ${step.title}: ${step.detail}`);
|
|
65
|
+
if (report.coverage.unreviewedAttachments)
|
|
66
|
+
lines.push(`Unreviewed attachments: ${report.coverage.unreviewedAttachments}`);
|
|
67
|
+
if (report.coverage.rejectedLinks)
|
|
68
|
+
lines.push(`Unsupported link values: ${report.coverage.rejectedLinks}`);
|
|
69
|
+
if (report.coverage.boundsReached.length)
|
|
70
|
+
lines.push(`Partial analysis: ${report.coverage.boundsReached.join('; ')}`);
|
|
71
|
+
if (report.source.kind === 'qr' && !report.coverage.reviewedParts)
|
|
72
|
+
lines.push('No QR text was decoded. This does not establish that the image contains no QR symbol.');
|
|
73
|
+
lines.push('Exact paths, queries, fragments and original message content are not included. Link relationships and authorisation parameters are supplied claims, not proof of a completed redirect or account compromise.');
|
|
74
|
+
return `${lines.join('\n')}\n`;
|
|
75
|
+
}
|
|
76
|
+
export async function runIntakeCommand(args, dependencies, context) {
|
|
77
|
+
context.setFailureLabel('Message intake');
|
|
78
|
+
if (MESSAGE_INTAKE_INPUTS[args.kind].binary && (!args.source || args.source === '-'))
|
|
79
|
+
throw new CliUsageError('Binary intake requires a selected file; binary stdin is not accepted.');
|
|
80
|
+
let bytes;
|
|
81
|
+
if (dependencies.readBinaryArtifactInput && args.source && args.source !== '-')
|
|
82
|
+
bytes = await dependencies.readBinaryArtifactInput(args.source);
|
|
83
|
+
else if (args.source && args.source !== '-')
|
|
84
|
+
bytes = await readBoundedRegularFile(args.source, { maximumBytes: MAX_MESSAGE_INTAKE_BYTES, minimumBytes: 1, label: 'Selected input', ...(dependencies.signal ? { signal: dependencies.signal } : {}) });
|
|
85
|
+
else
|
|
86
|
+
bytes = new TextEncoder().encode(await context.readInput(args.source, MAX_MESSAGE_INTAKE_BYTES, 'Selected input'));
|
|
87
|
+
try {
|
|
88
|
+
const result = await reviewSelectedInputInWorker(bytes, args.kind, context.now(), dependencies.signal);
|
|
89
|
+
let authenticationReview;
|
|
90
|
+
try {
|
|
91
|
+
authenticationReview = selectReceiverTrust(result.report.authenticationReview, args.trustedAuthHeaders ?? []);
|
|
92
|
+
}
|
|
93
|
+
catch (cause) {
|
|
94
|
+
if (cause instanceof TypeError)
|
|
95
|
+
throw new CliUsageError(cause.message);
|
|
96
|
+
throw cause;
|
|
97
|
+
}
|
|
98
|
+
const report = { ...result.report, authenticationReview, identityRecovery: reviewIdentityIncident({ reportedActions: args.reportedActions }) };
|
|
99
|
+
dependencies.signal?.throwIfAborted();
|
|
100
|
+
if (!args.quiet)
|
|
101
|
+
context.writeStdout(args.output === 'json' ? formatJsonDocument(report) : context.terminal(formatMessageIntake(report), args.color));
|
|
102
|
+
return args.strictExit && report.coverage.state === 'partial' ? EXIT_CODES.PARTIAL_FAILURE : EXIT_CODES.SUCCESS;
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
bytes.fill(0);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -89,6 +89,7 @@ export async function buildInterchangeFidelityReport(raw, options = {}) {
|
|
|
89
89
|
}),
|
|
90
90
|
limitations: Object.freeze([
|
|
91
91
|
'The file did not match a registered interchange contract. Its supplied schema text and contents are not echoed.',
|
|
92
|
+
'This is an import/export compatibility catalogue, not the complete verifier inventory. verify-artifact may support this file independently.',
|
|
92
93
|
'This report makes no request and does not establish that evidence is accurate, current, complete, or safe to share.',
|
|
93
94
|
]),
|
|
94
95
|
});
|
|
@@ -185,6 +186,7 @@ export async function buildInterchangeFidelityReport(raw, options = {}) {
|
|
|
185
186
|
blockedSectionCount, prunedRecordCount,
|
|
186
187
|
}),
|
|
187
188
|
limitations: Object.freeze([
|
|
189
|
+
...(contract.id === 'lookup_evidence' ? ['CLI read support means offline verification and packaging. brief, export, compare, registry-doctor and source-report require a saved CLI Lookup, not this browser evidence export.'] : []),
|
|
188
190
|
'Compatibility describes declared field groups and supported operations, not byte-for-byte equality or evidence truth.',
|
|
189
191
|
'The report emits only registered metadata, counts, and fixed field-group identifiers. Targets, notes, contacts, passphrases, and evidence values are not included.',
|
|
190
192
|
'A verified integrity envelope detects file changes but does not establish author identity, source accuracy, currency, completeness, or sharing authority.',
|
|
@@ -195,7 +197,7 @@ export function formatInterchangeFidelityReport(report) {
|
|
|
195
197
|
const browser = report.compatibility.browser;
|
|
196
198
|
const cli = report.compatibility.cli;
|
|
197
199
|
const nextAction = !report.recognised
|
|
198
|
-
? '
|
|
200
|
+
? 'Run verify-artifact for independent verification support. Import/export compatibility is not registered here.'
|
|
199
201
|
: !report.artifact.versionSupported
|
|
200
202
|
? 'Use a WHOISleuth release that supports this exact version, or re-export it with its last supported release.'
|
|
201
203
|
: !report.verification.assuranceSatisfied
|
|
@@ -209,7 +211,7 @@ export function formatInterchangeFidelityReport(report) {
|
|
|
209
211
|
: 'Retain the verification result and use only the operations declared above.';
|
|
210
212
|
return [
|
|
211
213
|
'Interchange fidelity report',
|
|
212
|
-
`
|
|
214
|
+
`Interchange contract ${report.recognised ? 'registered' : 'not registered'}`,
|
|
213
215
|
`Artifact ${report.artifact.id ?? 'unrecognised'}`,
|
|
214
216
|
`Version ${report.artifact.version ?? 'unavailable'}${report.artifact.versionSupported ? '' : ' (unsupported)'}`,
|
|
215
217
|
`Verification ${report.verification.state}`,
|
|
@@ -9,7 +9,7 @@ import { OFFLINE_ARTIFACT_VERIFICATION_SCHEMA, OFFLINE_ARTIFACT_VERIFICATION_VER
|
|
|
9
9
|
import { validateLookupEvidenceArtifactStructure } from "./artifact-validation/lookup-evidence.mjs";
|
|
10
10
|
import { CLI_DISCOVERY_SCAN_SCHEMA, CLI_DISCOVERY_SCAN_VERSION } from "./discovery-scan.mjs";
|
|
11
11
|
import { CLI_DISCOVER_SCHEMA, CLI_DISCOVER_SCHEMA_VERSION, CLI_POSTURE_SCHEMA, CLI_POSTURE_SCHEMA_VERSION, CLI_CT_SEARCH_SCHEMA, CLI_CT_SEARCH_SCHEMA_VERSION, CLI_COMPARE_SCHEMA, CLI_COMPARE_SCHEMA_VERSION, } from "./formatters/json.mjs";
|
|
12
|
-
import { CLI_LOOKUP_BRIEF_SCHEMA, CLI_LOOKUP_BRIEF_VERSION } from "
|
|
12
|
+
import { CLI_LOOKUP_BRIEF_SCHEMA, CLI_LOOKUP_BRIEF_VERSION } from "../packages/contracts/cli-lookup-brief.mjs";
|
|
13
13
|
import { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, parseExternalFindingsDocument } from "../packages/interchange/external-findings-import.mjs";
|
|
14
14
|
import { SOURCE_RELIABILITY_REPORT_SCHEMA, SOURCE_RELIABILITY_REPORT_VERSION } from "./source-reliability.mjs";
|
|
15
15
|
import { CLI_CASE_PACK_SCHEMA, CLI_CASE_PACK_VERSION, verifyCliCasePack } from "./case-pack.mjs";
|
|
@@ -1,156 +1,20 @@
|
|
|
1
1
|
import { domainToASCII } from 'node:url';
|
|
2
2
|
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { EXTERNAL_FINDINGS_SCHEMA } from "../packages/interchange/external-findings-import.mjs";
|
|
6
|
-
import { CLI_LOOKUP_BRIEF_SCHEMA } from "./lookup-brief.mjs";
|
|
7
|
-
import { INVESTIGATION_PLAN_RECIPES, RUNNABLE_INVESTIGATION_PLAN_RECIPES, cliInvocationNetworkEffect, } from "./command-reference.mjs";
|
|
3
|
+
import { cliInvocationNetworkEffect } from "./command-reference.mjs";
|
|
4
|
+
import { INVESTIGATION_RECIPE_DEFINITIONS, INVESTIGATION_PLAN_RECIPES, RUNNABLE_INVESTIGATION_PLAN_RECIPES, } from "./investigation-recipes.mjs";
|
|
8
5
|
export const CLI_INVESTIGATION_PLAN_SCHEMA = 'whoisleuth.cli.investigation-plan';
|
|
9
6
|
export const CLI_INVESTIGATION_PLAN_VERSION = 1;
|
|
10
7
|
export const CLI_WORKFLOW_RECIPE_CATALOGUE_SCHEMA = 'whoisleuth.cli.workflow-recipe-catalogue';
|
|
11
8
|
export const CLI_WORKFLOW_RECIPE_CATALOGUE_VERSION = 1;
|
|
12
9
|
export { INVESTIGATION_PLAN_RECIPES, RUNNABLE_INVESTIGATION_PLAN_RECIPES };
|
|
13
|
-
const RECIPES = Object.freeze(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
{ stepId: 'verify', input: 1, sourceStepId: 'export' },
|
|
20
|
-
]),
|
|
21
|
-
subjectRequirement: 'domain',
|
|
22
|
-
objective: 'Collect and preserve separately attributed registration, DNS, HTTP, TLS, page, and network-context evidence.',
|
|
23
|
-
limitations: Object.freeze([
|
|
24
|
-
'Collection remains analyst-triggered and source limitations remain explicit.',
|
|
25
|
-
'Disposition, reviewed response actions, monitoring, and closure continue in the saved Case workspace; this CLI recipe does not submit reports.',
|
|
26
|
-
]),
|
|
27
|
-
steps: (domain) => Object.freeze([
|
|
28
|
-
step('collect', 'Collect a Deep lookup', 'lookup', [domain, '--deep', '--json'], 'network', 'network_disclosure', 'whoisleuth.cli.lookup', 'Review source health and limitations before using missing fields.'),
|
|
29
|
-
step('export', 'Create a portable evidence report', 'export', ['<saved-lookup.json>'], 'offline', 'analyst_selection', 'whoisleuth.lookup-evidence', 'Reuse the collected Lookup by default, or select a different reviewed Lookup file.'),
|
|
30
|
-
step('verify', 'Verify the exported artefact', 'verify-artifact', ['<evidence.json>', '--json'], 'offline', 'analyst_selection', 'whoisleuth.offline-artifact-verification', 'Keep verification distinct from a claim that the observations are correct or current.'),
|
|
31
|
-
]),
|
|
32
|
-
}),
|
|
33
|
-
'lookalike-review': Object.freeze({
|
|
34
|
-
id: 'lookalike-review',
|
|
35
|
-
label: 'Lookalike candidate review',
|
|
36
|
-
subjectRequirement: 'brand_or_domain',
|
|
37
|
-
objective: 'Generate a bounded candidate queue, collect only the selected scope, and retain a reviewed candidate lookup.',
|
|
38
|
-
limitations: Object.freeze([
|
|
39
|
-
'Candidate generation does not establish registration, control, intent, or maliciousness.',
|
|
40
|
-
'Official-reference collection and page comparison require analyst-selected saved evidence; use page-compare after retaining the reference and candidate observations.',
|
|
41
|
-
]),
|
|
42
|
-
steps: (subject) => Object.freeze([
|
|
43
|
-
step('generate', 'Generate candidates offline', 'discover', [subject, '--preset', 'all', '--json'], 'offline', 'none', 'whoisleuth.cli.discover', 'Review mutation families and suppressions before collection.'),
|
|
44
|
-
step('scan', 'Collect a bounded candidate queue', 'discover-scan', [subject, '--fast', '--scan-limit', '50', '--json'], 'network', 'network_disclosure', 'whoisleuth.cli.discovery-scan', 'Fast collection is a triage boundary; partial or inconclusive authority evidence remains explicit.'),
|
|
45
|
-
step('inspect', 'Deep-review one selected candidate', 'lookup', ['<selected-domain>', '--deep', '--json'], 'network', 'analyst_selection', 'whoisleuth.cli.lookup', 'Select a candidate deliberately; generation does not prove registration, control, intent, or maliciousness.'),
|
|
46
|
-
]),
|
|
47
|
-
}),
|
|
48
|
-
'owned-domain-review': Object.freeze({
|
|
49
|
-
id: 'owned-domain-review',
|
|
50
|
-
label: 'Owned domain posture review',
|
|
51
|
-
subjectRequirement: 'domain',
|
|
52
|
-
objective: 'Review current passive posture and compare supplied observations with an analyst-authored control manifest.',
|
|
53
|
-
limitations: Object.freeze(['Use only for a domain the analyst owns or is authorised to review.']),
|
|
54
|
-
steps: (domain) => Object.freeze([
|
|
55
|
-
step('posture', 'Collect bounded DNS posture', 'posture', [domain, '--json'], 'network', 'network_disclosure', 'whoisleuth.cli.posture', 'Review mail profile and delegation evidence before interpreting missing records.'),
|
|
56
|
-
step('lookup', 'Collect supporting Deep evidence', 'lookup', [domain, '--deep', '--json'], 'network', 'network_disclosure', 'whoisleuth.cli.lookup', 'Retain separately attributed registration, DNS, TLS, and page observations.'),
|
|
57
|
-
step('manifest', 'Review the domain control manifest', 'domain-control', ['<review-input.json>', '--json'], 'offline', 'analyst_selection', DOMAIN_CONTROL_REVIEW_SCHEMA, 'Only complete supplied observations may produce drift.'),
|
|
58
|
-
]),
|
|
59
|
-
}),
|
|
60
|
-
'historical-comparison': Object.freeze({
|
|
61
|
-
id: 'historical-comparison',
|
|
62
|
-
label: 'Historical observation comparison',
|
|
63
|
-
standardInputs: Object.freeze([
|
|
64
|
-
{ stepId: 'diff', input: 2, sourceStepId: 'current' },
|
|
65
|
-
{ stepId: 'timeline', input: 3, sourceStepId: 'current' },
|
|
66
|
-
]),
|
|
67
|
-
subjectRequirement: 'domain',
|
|
68
|
-
objective: 'Collect a current observation and compare it with analyst-selected saved observations without merging source states.',
|
|
69
|
-
limitations: Object.freeze(['A later observation does not retroactively refresh retained evidence.']),
|
|
70
|
-
steps: (domain) => Object.freeze([
|
|
71
|
-
step('current', 'Collect the current lookup', 'lookup', [domain, '--deep', '--json'], 'network', 'network_disclosure', 'whoisleuth.cli.lookup', 'A current request does not refresh or validate older provider-reported history.'),
|
|
72
|
-
step('diff', 'Compare two selected observations', 'diff', ['<previous.json>', '<current.json>', '--json'], 'offline', 'analyst_selection', 'whoisleuth.cli.lookup-diff', 'Equal, different, conflicting, and unavailable evidence remain separate.'),
|
|
73
|
-
step('timeline', 'Build a bounded local timeline', 'timeline', ['<oldest.json>', '<newer.json>', '<current.json>', '--json'], 'offline', 'analyst_selection', 'whoisleuth.cli.lookup-timeline', 'Choose two to twenty same-domain files in chronological scope.'),
|
|
74
|
-
]),
|
|
75
|
-
}),
|
|
76
|
-
'campaign-review': Object.freeze({
|
|
77
|
-
id: 'campaign-review',
|
|
78
|
-
label: 'Campaign candidate review',
|
|
79
|
-
subjectRequirement: 'brand_or_domain',
|
|
80
|
-
objective: 'Prepare a bounded candidate set, collect a deliberately selected queue, and review retained evidence without asserting campaign attribution.',
|
|
81
|
-
limitations: Object.freeze(['Grouping candidates is analyst triage and does not prove common ownership, control, infrastructure, or intent.']),
|
|
82
|
-
steps: (subject) => Object.freeze([
|
|
83
|
-
step('prepare', 'Prepare candidates offline', 'discover', [subject, '--preset', 'all', '--json'], 'offline', 'none', 'whoisleuth.cli.discover', 'Review mutation families and bounded omissions before selecting a collection scope.'),
|
|
84
|
-
step('collect', 'Collect the selected candidate queue', 'discover-scan', [subject, '--fast', '--scan-limit', '50', '--json'], 'network', 'network_disclosure', 'whoisleuth.cli.discovery-scan', 'Treat partial and inconclusive authority results as explicit outcomes.'),
|
|
85
|
-
step('review', 'Prepare a selected candidate brief', 'brief', ['<saved-lookup.json>', '--json'], 'offline', 'analyst_selection', CLI_LOOKUP_BRIEF_SCHEMA, 'Select a saved candidate Lookup; keep any campaign grouping analyst-authored.'),
|
|
86
|
-
]),
|
|
87
|
-
}),
|
|
88
|
-
'certificate-anomaly': Object.freeze({
|
|
89
|
-
id: 'certificate-anomaly',
|
|
90
|
-
label: 'Certificate anomaly review',
|
|
91
|
-
subjectRequirement: 'domain',
|
|
92
|
-
objective: 'Review bounded certificate observations alongside current source-qualified domain evidence without treating issuance as proof of control or intent.',
|
|
93
|
-
limitations: Object.freeze(['Certificate observations are separately attributed and do not establish current service control.']),
|
|
94
|
-
steps: (domain) => Object.freeze([
|
|
95
|
-
step('search', 'Collect bounded certificate observations', 'ct-search', [domain, '--json'], 'network', 'network_disclosure', 'whoisleuth.cli.ct-search', 'Review source availability, truncation, and observation timing.'),
|
|
96
|
-
step('intake', 'Normalise the selected observations', 'ct-intake', ['<certificate-events.json>', '--json'], 'offline', 'analyst_selection', EXTERNAL_FINDINGS_SCHEMA, 'Select a certificate-event batch; a certificate-search report is not interchangeable with that input.'),
|
|
97
|
-
step('corroborate', 'Collect supporting domain evidence', 'lookup', [domain, '--deep', '--json'], 'network', 'network_disclosure', 'whoisleuth.cli.lookup', 'Compare evidence families without collapsing certificate and registration identities.'),
|
|
98
|
-
]),
|
|
99
|
-
}),
|
|
100
|
-
'registry-disagreement': Object.freeze({
|
|
101
|
-
id: 'registry-disagreement',
|
|
102
|
-
label: 'Registry disagreement review',
|
|
103
|
-
standardInputs: Object.freeze([
|
|
104
|
-
{ stepId: 'compare', input: 1, sourceStepId: 'collect' },
|
|
105
|
-
{ stepId: 'report', input: 1, sourceStepId: 'collect' },
|
|
106
|
-
]),
|
|
107
|
-
subjectRequirement: 'domain',
|
|
108
|
-
objective: 'Collect separately attributed registration evidence and review conflicting publications without selecting an arbitrary source as truth.',
|
|
109
|
-
limitations: Object.freeze(['Only authority-aware registration evidence may decide availability; disagreement remains explicit.']),
|
|
110
|
-
steps: (domain) => Object.freeze([
|
|
111
|
-
step('collect', 'Collect source-qualified registration evidence', 'lookup', [domain, '--deep', '--json'], 'network', 'network_disclosure', 'whoisleuth.cli.lookup', 'Retain RDAP, registrar RDAP, WHOIS, and authority states separately.'),
|
|
112
|
-
step('compare', 'Compare registry publications offline', 'compare', ['<saved-lookup.json>', '--json'], 'offline', 'analyst_selection', 'whoisleuth.cli.compare', 'Do not convert conflicting or unavailable publications into equivalence.'),
|
|
113
|
-
step('report', 'Prepare a target-free source report', 'source-report', ['<saved-lookup.json>', '--json'], 'offline', 'analyst_selection', 'whoisleuth.source-reliability-report', 'The report describes source behaviour, not ownership, safety, or legal status.'),
|
|
114
|
-
]),
|
|
115
|
-
}),
|
|
116
|
-
'evidence-handoff': Object.freeze({
|
|
117
|
-
id: 'evidence-handoff',
|
|
118
|
-
label: 'Reviewed evidence handoff',
|
|
119
|
-
standardInputs: Object.freeze([{ stepId: 'lint', input: 1, sourceStepId: 'package' }]),
|
|
120
|
-
subjectRequirement: 'review_label',
|
|
121
|
-
objective: 'Verify, minimise, and package analyst-selected evidence for a deliberate handoff without transmitting or submitting it.',
|
|
122
|
-
limitations: Object.freeze(['The recipe prepares local material only; sharing remains a separate deliberate action.']),
|
|
123
|
-
steps: () => Object.freeze([
|
|
124
|
-
step('verify', 'Verify the selected artefact', 'verify-artifact', ['<evidence.json>', '--json', '--strict-exit'], 'offline', 'analyst_selection', 'whoisleuth.offline-artifact-verification', 'Verification checks structure and integrity, not the truth or currency of observations.'),
|
|
125
|
-
step('package', 'Build a reviewed public Case-pack', 'case-pack', ['<cases.json>', '--audience', 'public', '--reviewed', '--json'], 'offline', 'analyst_selection', 'whoisleuth.cli.case-pack', 'Review minimisation and audience projection before retaining the separate package.'),
|
|
126
|
-
step('lint', 'Review deliberate-sharing metadata', 'sharing-review', ['<package.json>', '--marking', 'clear', '--recipient-scope', 'public', '--purpose', 'reviewed evidence handoff', '--human-reviewed', '--personal-data-reviewed', '--redactions-confirmed', '--json'], 'offline', 'analyst_selection', 'whoisleuth.cli.sharing-review', 'A clear lint result does not send, upload, publish, or authorise the artefact.'),
|
|
127
|
-
]),
|
|
128
|
-
}),
|
|
129
|
-
'planned-domain-change': Object.freeze({
|
|
130
|
-
id: 'planned-domain-change',
|
|
131
|
-
label: 'Planned domain change',
|
|
132
|
-
subjectRequirement: 'domain',
|
|
133
|
-
objective: 'Review an analyst-authored desired state and prepare bounded change material without changing DNS, registry, mail, or hosted configuration.',
|
|
134
|
-
limitations: Object.freeze(['Planning and packaging never apply, submit, schedule, or enforce a change.']),
|
|
135
|
-
steps: () => Object.freeze([
|
|
136
|
-
step('control', 'Review the desired-state manifest', 'domain-control', ['<review-input.json>', '--json'], 'offline', 'analyst_selection', DOMAIN_CONTROL_REVIEW_SCHEMA, 'Only supplied complete observations may produce drift.'),
|
|
137
|
-
step('assure', 'Review change and recovery assumptions', 'assurance', ['<assurance-input.json>', '--json'], 'offline', 'analyst_selection', 'whoisleuth.domain-assurance', 'Record uncertainty, rollback dependencies, and unavailable evidence explicitly.'),
|
|
138
|
-
step('package', 'Build a reviewed change packet', 'change-packet', ['<change-packet-input.json>', '--json'], 'offline', 'analyst_selection', 'whoisleuth.domain-change-packet', 'The packet is local reviewed material and performs no submission or enforcement.'),
|
|
139
|
-
]),
|
|
140
|
-
}),
|
|
141
|
-
'post-change-verification': Object.freeze({
|
|
142
|
-
id: 'post-change-verification',
|
|
143
|
-
label: 'Post-change verification',
|
|
144
|
-
subjectRequirement: 'domain',
|
|
145
|
-
objective: 'Perform one explicit later observation and compare it with analyst-selected retained evidence after an authorised change.',
|
|
146
|
-
limitations: Object.freeze(['This is a one-time recheck, not monitoring setup or proof that every resolver or service has converged.']),
|
|
147
|
-
steps: () => Object.freeze([
|
|
148
|
-
step('recheck', 'Run one bounded retained-manifest review', 'monitor-once', ['<manifest.json>', '--limit', '1', '--json'], 'network', 'network_disclosure', CLI_DOMAIN_CONTROL_MONITOR_SCHEMA, 'One later observation may remain partial, unavailable, stale, or conflicting.'),
|
|
149
|
-
step('compare', 'Compare selected before and after evidence', 'diff', ['<before.json>', '<after.json>', '--json'], 'offline', 'analyst_selection', 'whoisleuth.cli.lookup-diff', 'Materiality is derived from compatible retained evidence and does not infer intent.'),
|
|
150
|
-
step('record', 'Record reviewed completion material', 'change-packet', ['<post-change-input.json>', '--json'], 'offline', 'analyst_selection', 'whoisleuth.domain-change-packet', 'Recording reviewed material does not submit it or start automatic monitoring.'),
|
|
151
|
-
]),
|
|
10
|
+
const RECIPES = Object.freeze(Object.fromEntries(INVESTIGATION_PLAN_RECIPES.map((id) => [
|
|
11
|
+
id,
|
|
12
|
+
Object.freeze({
|
|
13
|
+
...INVESTIGATION_RECIPE_DEFINITIONS[id],
|
|
14
|
+
id,
|
|
15
|
+
steps: (subject) => validatedSteps(INVESTIGATION_RECIPE_DEFINITIONS[id].steps(subject)),
|
|
152
16
|
}),
|
|
153
|
-
|
|
17
|
+
])));
|
|
154
18
|
export const INVESTIGATION_RECIPE_REGISTRY = RECIPES;
|
|
155
19
|
export function workflowStandardInputs(recipe) {
|
|
156
20
|
return RECIPES[recipe].standardInputs ?? [];
|
|
@@ -160,7 +24,12 @@ export function isRunnableInvestigationRecipe(value) {
|
|
|
160
24
|
return RUNNABLE_INVESTIGATION_PLAN_RECIPES.includes(value);
|
|
161
25
|
}
|
|
162
26
|
export function workflowReviewDeclarations(step) {
|
|
163
|
-
return step.arguments.filter(argument => [
|
|
27
|
+
return step.arguments.filter((argument) => [
|
|
28
|
+
'--reviewed',
|
|
29
|
+
'--human-reviewed',
|
|
30
|
+
'--personal-data-reviewed',
|
|
31
|
+
'--redactions-confirmed',
|
|
32
|
+
].includes(argument));
|
|
164
33
|
}
|
|
165
34
|
function recipeCatalogueEntry(recipe) {
|
|
166
35
|
const exampleSubject = recipe.subjectRequirement === 'domain' ? 'example.test' : 'Example Organisation';
|
|
@@ -215,15 +84,20 @@ export function formatWorkflowRecipeCatalogue(catalogue) {
|
|
|
215
84
|
'',
|
|
216
85
|
].join('\n');
|
|
217
86
|
}
|
|
218
|
-
function
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
87
|
+
function validatedSteps(steps) {
|
|
88
|
+
for (const step of steps) {
|
|
89
|
+
const invocationEffect = cliInvocationNetworkEffect(step.command, step.arguments);
|
|
90
|
+
if ((step.mode === 'network') !== (invocationEffect === 'network')) {
|
|
91
|
+
throw new Error(`Investigation step ${step.id} does not match the registered network effect for ${step.command}.`);
|
|
92
|
+
}
|
|
222
93
|
}
|
|
223
|
-
return
|
|
94
|
+
return steps;
|
|
224
95
|
}
|
|
225
96
|
function boundedSubject(value) {
|
|
226
|
-
if (typeof value !== 'string' ||
|
|
97
|
+
if (typeof value !== 'string' ||
|
|
98
|
+
!value.trim() ||
|
|
99
|
+
value.length > 253 ||
|
|
100
|
+
/[\u0000-\u001f\u007f]/u.test(value)) {
|
|
227
101
|
throw new TypeError('Investigation-plan subject must be bounded text without control characters.');
|
|
228
102
|
}
|
|
229
103
|
return value.replace(/\s+/gu, ' ').trim();
|
|
@@ -232,7 +106,9 @@ function normalizedDomain(value) {
|
|
|
232
106
|
const candidate = domainToASCII(value.toLowerCase().replace(/\.$/u, ''));
|
|
233
107
|
if (!candidate || candidate.length > 253 || !candidate.includes('.'))
|
|
234
108
|
return null;
|
|
235
|
-
return candidate
|
|
109
|
+
return candidate
|
|
110
|
+
.split('.')
|
|
111
|
+
.every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/u.test(label))
|
|
236
112
|
? candidate
|
|
237
113
|
: null;
|
|
238
114
|
}
|
|
@@ -241,7 +117,9 @@ export function buildInvestigationPlan(recipeId, subjectValue, generatedAtValue
|
|
|
241
117
|
if (!recipe)
|
|
242
118
|
throw new TypeError('Investigation-plan recipe is unsupported.');
|
|
243
119
|
const suppliedSubject = boundedSubject(subjectValue);
|
|
244
|
-
const subject = recipe.subjectRequirement === 'domain'
|
|
120
|
+
const subject = recipe.subjectRequirement === 'domain'
|
|
121
|
+
? normalizedDomain(suppliedSubject)
|
|
122
|
+
: suppliedSubject.toLowerCase();
|
|
245
123
|
if (!subject)
|
|
246
124
|
throw new TypeError('This investigation-plan recipe requires one valid domain.');
|
|
247
125
|
const generatedAt = normalizeExplicitIsoTimestamp(generatedAtValue);
|