@slicedearth/whoisleuth-cli 2.4.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DISCLOSURE +1 -1
- package/NOTICE +2 -2
- package/cli/argument-values.mjs +55 -0
- package/cli/arguments.mjs +24 -470
- package/cli/artifact-validation/case-response.mjs +67 -9
- package/cli/artifact-validation/lookup-evidence.mjs +18 -4
- package/cli/artifact-validation/signed-review.mjs +61 -64
- package/cli/artifact-verify.mjs +11 -2
- package/cli/assurance-arguments.mjs +56 -0
- package/cli/assurance-command-definitions.mjs +179 -0
- package/cli/assurance-command-runner.mjs +1 -1
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +1 -1
- package/cli/bulk-command-runner.mjs +6 -8
- package/cli/bulk.mjs +2 -3
- package/cli/case-command.mjs +99 -6
- package/cli/case-pack.mjs +19 -4
- package/cli/ci-report.mjs +6 -3
- package/cli/collection-arguments.mjs +123 -0
- package/cli/collection-command-definitions.mjs +330 -0
- package/cli/collection-preflight.mjs +8 -1
- package/cli/command-argument-grammar.mjs +8 -210
- package/cli/command-definition.mjs +330 -0
- package/cli/command-reference.mjs +55 -1388
- package/cli/command-seeds.mjs +18 -0
- package/cli/compare.mjs +1 -1
- package/cli/config-profile.mjs +1 -1
- package/cli/ct-event-intake.mjs +4 -4
- package/cli/discover.mjs +2 -2
- package/cli/discovery-observation-snapshot.mjs +2 -2
- package/cli/discovery-scan-command-runner.mjs +7 -9
- package/cli/discovery-scan.mjs +1 -1
- package/cli/discovery-snapshot.mjs +2 -2
- package/cli/discovery-workflow.mjs +4 -4
- package/cli/domain-control-monitor.mjs +6 -6
- package/cli/domain-control-observations.mjs +2 -1
- package/cli/domain-feed.mjs +61 -0
- package/cli/errors.mjs +5 -6
- package/cli/evidence-arguments.mjs +46 -0
- package/cli/evidence-command-definitions.mjs +69 -0
- package/cli/evidence-command-runner.mjs +2 -3
- package/cli/evidence-signing.mjs +3 -2
- package/cli/external-observation-mapping.mjs +2 -2
- package/cli/formatters/json.mjs +1 -13
- package/cli/formatters/terminal-command-formats.mjs +6 -0
- package/cli/formatters/terminal-lookup.mjs +6 -0
- package/cli/history-arguments.mjs +39 -0
- package/cli/history-command-definitions.mjs +88 -0
- package/cli/indicator-set-command.mjs +32 -0
- package/cli/inline-command-families.mjs +6 -47
- package/cli/inline-command-runner.mjs +4 -31
- package/cli/input.mjs +2 -2
- package/cli/intake-command.mjs +147 -0
- package/cli/interchange-report.mjs +4 -2
- package/cli/investigation-artifacts.mjs +3 -4
- package/cli/investigation-package-review.mjs +2 -0
- package/cli/investigation-plan.mjs +32 -154
- package/cli/investigation-recipes.mjs +177 -0
- package/cli/investigation-run.mjs +78 -12
- package/cli/local-mmdb-review.mjs +220 -17
- package/cli/lookalike-calibration.mjs +1 -1
- package/cli/lookup-brief.mjs +2 -2
- package/cli/lookup-command-runner.mjs +19 -35
- package/cli/lookup-diff.mjs +10 -9
- package/cli/lookup-plan.mjs +10 -14
- package/cli/lookup-reconcile.mjs +6 -5
- package/cli/lookup-timeline.mjs +7 -6
- package/cli/mail-header-review.mjs +24 -247
- package/cli/mail-review.mjs +9 -7
- package/cli/network-arguments.mjs +51 -0
- package/cli/network-command-definitions.mjs +130 -0
- package/cli/network-command-runner.mjs +43 -31
- package/cli/offline-evidence-review.mjs +85 -6
- package/cli/posture.mjs +1 -1
- package/cli/registry-fixture-scaffold.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +8 -7
- package/cli/review-arguments.mjs +132 -0
- package/cli/review-command-definitions.mjs +279 -0
- package/cli/review-command-runner.mjs +72 -159
- package/cli/risk-calibration.mjs +14 -12
- package/cli/runner.mjs +20 -32
- package/cli/saved-lookup.mjs +2 -2
- package/cli/selected-input-worker.mjs +73 -0
- package/cli/sharing-review.mjs +5 -64
- package/cli/source-reliability.mjs +2 -2
- package/cli/support-arguments.mjs +49 -0
- package/cli/support-command-definitions.mjs +209 -0
- package/cli/support-command-runner.mjs +4 -4
- package/cli/watchlist-review.mjs +43 -0
- package/cli/workflow-arguments.mjs +100 -0
- package/cli/workflow-command-definitions.mjs +103 -0
- package/cli/workflow-command-runner.mjs +40 -1
- package/docs/cli-reference.md +27 -0
- package/docs/cli.md +322 -4
- package/docs/contextual-reviews.md +178 -0
- package/docs/infrastructure-observations.md +126 -0
- package/docs/offline-intake-context.md +213 -0
- package/lib/analyst-taxonomy.mjs +2 -40
- package/lib/application-version.mjs +1 -1
- package/lib/availability.mjs +13 -789
- package/lib/bounded-contract-normalizers.mjs +4 -0
- package/lib/bounded-json.mjs +2 -330
- package/lib/browser-library-profile.mjs +62 -84
- package/lib/candidate-provenance-bounds.mjs +2 -0
- package/lib/classify.mjs +1 -1
- package/lib/credential-form-attribution.mjs +136 -0
- package/lib/credential-surface-profile.mjs +6 -2
- package/lib/ct-query.mjs +2 -27
- package/lib/ct-response-bounds.mjs +2 -9
- package/lib/ct-search.mjs +83 -47
- package/lib/dmarc-discovery.mjs +62 -0
- package/lib/dns-convergence-review.mjs +21 -8
- package/lib/dns-delegation-health.mjs +35 -15
- package/lib/dns-inheritance-review.mjs +53 -63
- package/lib/dns-intelligence.mjs +27 -11
- package/lib/dns-query-coverage.mjs +40 -0
- package/lib/domain-assurance.mjs +1 -1
- package/lib/domain-change-packet.mjs +12 -13
- package/lib/domain-change-review.mjs +32 -14
- package/lib/domain-control-flight-recorder.mjs +103 -27
- package/lib/domain-control-manifest.mjs +25 -17
- package/lib/domain-posture-analysis.mjs +75 -7
- package/lib/domain-posture-parsers.mjs +9 -0
- package/lib/domain-posture.mjs +45 -25
- package/lib/domain-registration.mjs +392 -0
- package/lib/evidence-export-privacy.mjs +27 -17
- package/lib/evidence-export.mjs +3 -3
- package/lib/evidence-report.mjs +5 -1
- package/lib/external-intelligence-risk.mjs +2 -124
- package/lib/favicon.mjs +13 -4
- package/lib/generated/cisa-kev-catalog.mjs +41 -3
- package/lib/generated/registrar-standing-catalogue.mjs +18 -10
- package/lib/generated/retire-browser-catalog.mjs +332 -19
- package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
- package/lib/html-signals.mjs +13 -2
- package/lib/http-evidence-bounds.mjs +2 -8
- package/lib/http-intelligence.mjs +13 -1
- package/lib/idn-confusable-policy.mjs +6 -4
- package/lib/idn-confusables.mjs +1 -1
- package/lib/interchange-fidelity-registry.mjs +10 -0
- package/lib/lookup-child-profile-contract.mjs +28 -11
- package/lib/lookup-contract-primitives.mjs +1 -1
- package/lib/lookup-diagnostics.mjs +1 -23
- package/lib/lookup-network-evidence-bounds.mjs +1 -10
- package/lib/lookup-progress.mjs +50 -1
- package/lib/lookup-response-contract.mjs +70 -40
- package/lib/lookup-response.mjs +5 -15
- package/lib/lookup-source-progress.mjs +8 -22
- package/lib/lookup.mjs +1 -1
- package/lib/operation-budget.mjs +4 -0
- package/lib/opportunity-scoring.mjs +2 -107
- package/lib/outbound-identity.mjs +1 -1
- package/lib/page-fingerprints.mjs +18 -1
- package/lib/perceptual-hash-comparison.mjs +2 -34
- package/lib/perceptual-hash.mjs +36 -10
- package/lib/portable-generator.mjs +2 -29
- package/lib/project-metadata.mjs +2 -8
- package/lib/rdap-client.mjs +11 -7
- package/lib/registrable-domain.mjs +2 -56
- package/lib/registrar-standing-contract.mjs +1 -1
- package/lib/registrar-standing.mjs +9 -13
- package/lib/registry-capabilities.mjs +2 -2
- package/lib/registry-capability-catalogue.mjs +27 -19
- package/lib/risk-calibration-summary.mjs +2 -352
- package/lib/risk-scoring.mjs +2 -345
- package/lib/scoring-evidence-quality.mjs +2 -107
- package/lib/semantic-version.mjs +2 -54
- package/lib/service-binding-dns.mjs +46 -2
- package/lib/smtp-transport-review.mjs +48 -19
- package/lib/sslbl-certificates.generated.mjs +142 -139
- package/lib/static-html-analysis.mjs +32 -4
- package/lib/structured-data-identity.mjs +1 -1
- package/lib/technology-evidence-role.mjs +1 -51
- package/lib/threat-intelligence-manifests.mjs +1 -1
- package/lib/threat-intelligence-runtime.mjs +1 -1
- package/lib/threat-intelligence-targets.mjs +1 -1
- package/lib/threat-intelligence-types.mjs +2 -75
- package/lib/tls-intelligence.mjs +30 -5
- package/lib/typosquat-generator.mjs +11 -2
- package/lib/website-enrichment.mjs +404 -0
- package/lib/whois-authority.mjs +43 -5
- package/lib/whois-chain.mjs +12 -3
- package/lib/whois-common-formats.mjs +3 -1
- package/lib/whois-contacts.mjs +13 -2
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-parser.mjs +5 -3
- package/package.json +15 -14
- package/packages/analysis/analyst-taxonomy.mjs +40 -0
- package/packages/analysis/bounded-json.mjs +330 -0
- package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
- package/packages/analysis/cli-argument-grammar.mjs +216 -0
- package/packages/analysis/ct-query.mjs +27 -0
- package/packages/analysis/ct-response-bounds.mjs +9 -0
- package/packages/analysis/external-intelligence-risk.mjs +124 -0
- package/packages/analysis/http-evidence-bounds.mjs +8 -0
- package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
- package/packages/analysis/opportunity-scoring.mjs +107 -0
- package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
- package/packages/analysis/portable-generator.mjs +29 -0
- package/packages/analysis/project-metadata.mjs +8 -0
- package/packages/analysis/registrable-domain.mjs +56 -0
- package/packages/analysis/risk-calibration-summary.mjs +352 -0
- package/packages/analysis/risk-scoring.mjs +348 -0
- package/packages/analysis/scoring-evidence-quality.mjs +107 -0
- package/packages/analysis/semantic-version.mjs +54 -0
- package/packages/analysis/threat-intelligence-types.mjs +75 -0
- package/packages/cases/case-containment-handoff.mjs +195 -0
- package/packages/cases/case-evidence-links.mjs +151 -0
- package/packages/cases/case-evidence-model.mjs +89 -12
- package/packages/cases/case-evidence-quality.mjs +5 -0
- package/packages/cases/case-export-input.mjs +7 -5
- package/packages/cases/case-incident-context.mjs +52 -0
- package/packages/cases/case-markdown.mjs +11 -0
- package/packages/cases/case-migration-model.mjs +40 -24
- package/packages/cases/case-object-coverage.mjs +33 -0
- package/packages/cases/case-packet-correction.mjs +106 -0
- package/packages/cases/case-recheck-model.mjs +124 -36
- package/packages/cases/case-record-contracts.mjs +1 -1
- package/packages/cases/case-record-core.mjs +1 -1
- package/packages/cases/case-record-operations.mjs +111 -118
- package/packages/cases/case-record-projection.mjs +23 -4
- package/packages/cases/case-record-version-input.mjs +41 -0
- package/packages/cases/case-report.mjs +93 -27
- package/packages/cases/case-requested-evidence.mjs +146 -0
- package/packages/cases/case-response-actions.mjs +159 -1
- package/packages/cases/case-response-model.mjs +16 -1
- package/packages/cases/case-response-object.mjs +119 -0
- package/packages/cases/case-response-outcomes.mjs +108 -20
- package/packages/cases/case-response-packet-render.mjs +35 -12
- package/packages/cases/case-response-packet.mjs +55 -13
- package/packages/cases/case-response-records.mjs +0 -1
- package/packages/cases/case-response-review-inputs.mjs +60 -8
- package/packages/cases/case-storage-model.mjs +1 -1
- package/packages/cases/case-workflow-metadata.mjs +217 -0
- package/packages/cases/case-workflow-migration.mjs +43 -0
- package/packages/cases/http-summary.mjs +6 -1
- package/packages/comparison/favicon-similarity.mjs +1 -1
- package/packages/comparison/page-similarity.mjs +1 -1
- package/packages/comparison/relationship-evidence.mjs +21 -9
- package/packages/comparison/relationship-provenance.mjs +7 -1
- package/packages/contracts/analyst-interchange.mjs +35 -15
- package/packages/contracts/candidate-watch-review.mjs +54 -0
- package/packages/contracts/case-portability.mjs +360 -49
- package/packages/contracts/cli-command-semantics.mjs +5 -0
- package/packages/contracts/cli-grammar.mjs +6 -0
- package/packages/contracts/cli-lookup-brief.mjs +2 -0
- package/packages/contracts/context-review.mjs +25 -0
- package/packages/contracts/document-intake.mjs +11 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +30 -14
- package/packages/contracts/domain-control-manifest.mjs +1 -1
- package/packages/contracts/domain-control-monitor.mjs +13 -2
- package/packages/contracts/domain-control-review.mjs +36 -15
- package/packages/contracts/external-observation-interchange.mjs +96 -28
- package/packages/contracts/har-review.mjs +2 -0
- package/packages/contracts/identity-events.mjs +4 -0
- package/packages/contracts/investigation-run.mjs +2 -0
- package/packages/contracts/ip-address.mjs +10 -0
- package/packages/contracts/lookup-evidence.mjs +3 -2
- package/packages/contracts/mail-authentication.mjs +5 -0
- package/packages/contracts/message-intake.mjs +37 -0
- package/packages/contracts/offline-comparison.mjs +4 -1
- package/packages/contracts/passive-mail.mjs +10 -0
- package/packages/contracts/risk-calibration.mjs +44 -29
- package/packages/contracts/schema-lifecycle.mjs +3 -1
- package/packages/contracts/web-capture.mjs +2 -2
- package/packages/contracts/workspace-portability.mjs +144 -28
- package/packages/evidence/collection-quality.mjs +67 -0
- package/packages/evidence/credential-form-attribution.mjs +60 -0
- package/packages/evidence/dns-query-outcome.mjs +72 -0
- package/packages/evidence/email-recipient.mjs +18 -0
- package/packages/evidence/image-regions.mjs +15 -0
- package/packages/evidence/lookup-target.mjs +5 -1
- package/packages/evidence/observation.mjs +2 -0
- package/packages/evidence/png-pixels.mjs +117 -0
- package/packages/evidence/public-address-policy.mjs +83 -0
- package/packages/evidence/record-identity.mjs +60 -16
- package/packages/evidence/technology-evidence-role.mjs +51 -0
- package/packages/interchange/defensive-indicator-export.mjs +264 -0
- package/packages/interchange/external-findings-import.mjs +36 -29
- package/packages/interchange/json-output.mjs +14 -0
- package/packages/interchange/managed-indicator-export.mjs +36 -0
- package/packages/interchange/managed-indicator-set.mjs +223 -0
- package/packages/interchange/misp-indicator-export.mjs +93 -0
- package/packages/interchange/sharing-policy.mjs +56 -0
- package/packages/interchange/stix-indicator-export.mjs +129 -0
- package/packages/interchange/web-capture-import.mjs +22 -15
- package/packages/investigation/acquisition-decision-packet.mjs +79 -1
- package/packages/investigation/bulk-mail-exposure.mjs +7 -13
- package/packages/investigation/capture-coverage.mjs +57 -0
- package/packages/investigation/connector-provenance-review.mjs +100 -0
- package/packages/investigation/context-review.mjs +35 -0
- package/packages/investigation/document-intake.mjs +87 -0
- package/packages/investigation/docx-intake.mjs +133 -0
- package/packages/investigation/domain-history-review.mjs +132 -0
- package/packages/investigation/domain-transition-review.mjs +49 -0
- package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
- package/packages/investigation/har-intake.mjs +68 -0
- package/packages/investigation/identity-event-intake.mjs +131 -0
- package/packages/investigation/identity-incident-review.mjs +39 -0
- package/packages/investigation/incident-sequence-review.mjs +58 -0
- package/packages/investigation/infrastructure-observation.mjs +234 -0
- package/packages/investigation/intake-context.mjs +284 -0
- package/packages/investigation/intake-indicators.mjs +116 -0
- package/packages/investigation/intake-phones.mjs +155 -0
- package/packages/investigation/intake-report.mjs +20 -0
- package/packages/investigation/investigation-manifest.mjs +35 -9
- package/packages/investigation/link-intake.mjs +209 -0
- package/packages/investigation/lookup-asset-graph.mjs +1 -1
- package/packages/investigation/mail-authentication-review.mjs +222 -0
- package/packages/investigation/mail-header-identity.mjs +209 -0
- package/packages/investigation/message-intake.mjs +235 -0
- package/packages/investigation/page-behaviour.mjs +113 -0
- package/packages/investigation/pdf-intake.mjs +155 -0
- package/packages/investigation/platform-continuity-review.mjs +60 -0
- package/packages/investigation/qr-intake.mjs +56 -0
- package/packages/investigation/requested-action-hints.mjs +11 -0
- package/packages/investigation/selected-input-review.mjs +17 -0
- package/packages/investigation/storefront-review.mjs +70 -0
- package/packages/monitoring/domain-feed.mjs +321 -0
- package/packages/workspace/brand-candidate-workflow.mjs +351 -0
- package/packages/workspace/brand-keyword-campaign.mjs +231 -0
- package/packages/workspace/brand-profile-model.mjs +48 -4
- package/packages/workspace/bulk-review-model.mjs +28 -26
- package/packages/workspace/bulk-session-model.mjs +27 -13
- package/packages/workspace/campaign-model.mjs +1 -1
- package/packages/workspace/candidate-watch-handoff.mjs +195 -0
- package/packages/workspace/detection-rule-model.mjs +11 -3
- package/packages/workspace/hostile-input.mjs +1 -1
- package/packages/workspace/investigation-guide.mjs +2 -2
- package/packages/workspace/investigation-template-model.mjs +26 -2
- package/packages/workspace/page-baseline.mjs +2 -2
- package/packages/workspace/relationship-observation-model.mjs +17 -13
- package/packages/workspace/shortlist-model.mjs +24 -7
- package/packages/workspace/watchlist-history.mjs +59 -16
- package/packages/workspace/watchlist-store.mjs +152 -11
- package/packages/workspace/website-snapshot-model.mjs +28 -3
- package/packages/workspace/workspace-archive-crypto.mjs +1 -1
- package/packages/workspace/workspace-archive.mjs +5 -4
- package/third-party-notices.txt +549 -16
- package/lib/hostname.mjs +0 -2
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { Buffer } from 'node:buffer';
|
|
3
|
-
import {
|
|
3
|
+
import { MAX_MAIL_REVIEW_DOMAINS, addressDomains, messageIdDomains, authenticationServiceDomains, dkimSigningDomains, normalizeHeaderDomain } from "../packages/investigation/mail-header-identity.mjs";
|
|
4
|
+
export { MAX_MAIL_REVIEW_DOMAINS } from "../packages/investigation/mail-header-identity.mjs";
|
|
4
5
|
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
5
6
|
import { CliUsageError } from "./errors.mjs";
|
|
7
|
+
import { reviewMailAuthentication, selectReceiverTrust, authenticationHeaderLabel, aggregateMailAuthentication } from "../packages/investigation/mail-authentication-review.mjs";
|
|
6
8
|
export const CLI_MAIL_HEADER_REVIEW_SCHEMA = 'whoisleuth.cli.mail-header-review';
|
|
7
|
-
export const CLI_MAIL_HEADER_REVIEW_VERSION =
|
|
9
|
+
export const CLI_MAIL_HEADER_REVIEW_VERSION = 2;
|
|
8
10
|
export const MAX_MAIL_HEADER_INPUT_BYTES = 256 * 1024;
|
|
9
11
|
export const MAX_MAIL_HEADER_FIELDS = 512;
|
|
10
12
|
export const MAX_MAIL_HEADER_LINE_BYTES = 4 * 1024;
|
|
11
13
|
export const MAX_MAIL_HEADER_VALUE_LENGTH = 16 * 1024;
|
|
12
14
|
export const MAX_MAIL_RECEIVED_HOPS = 64;
|
|
13
|
-
export const MAX_MAIL_REVIEW_DOMAINS = 128;
|
|
14
15
|
const UNSAFE_HEADER_CONTROL_RE = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f\u200b-\u200f\u202a-\u202e\u2060-\u206f]/u;
|
|
15
16
|
const HEADER_NAME_RE = /^[A-Za-z0-9-]{1,78}$/u;
|
|
16
|
-
const AUTHENTICATION_METHODS = Object.freeze(['spf', 'dkim', 'dmarc', 'arc']);
|
|
17
|
-
const AUTHENTICATION_STATES = new Set(['fail', 'neutral', 'none', 'pass', 'permerror', 'softfail', 'temperror']);
|
|
18
17
|
function boundedHeaderSection(value) {
|
|
19
18
|
if (typeof value !== 'string' || !value)
|
|
20
19
|
throw new CliUsageError('Mail-header review requires one UTF-8 message or header block.');
|
|
@@ -75,240 +74,6 @@ function parseHeaderFields(value) {
|
|
|
75
74
|
function fieldsNamed(fields, name) {
|
|
76
75
|
return fields.filter((field) => field.name === name).map((field) => field.value);
|
|
77
76
|
}
|
|
78
|
-
function normalizeHeaderDomain(value) {
|
|
79
|
-
const normalized = value.trim().toLowerCase().replace(/^\[|\]$/gu, '').replace(/\.$/u, '');
|
|
80
|
-
return isValidAsciiDomainName(normalized, { requireLowercase: true }) ? normalized : null;
|
|
81
|
-
}
|
|
82
|
-
function lexicalSegments(value, delimiter, trackAngles = false) {
|
|
83
|
-
const segments = [];
|
|
84
|
-
let start = 0;
|
|
85
|
-
let quoted = false;
|
|
86
|
-
let escaped = false;
|
|
87
|
-
let commentDepth = 0;
|
|
88
|
-
let angleDepth = 0;
|
|
89
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
90
|
-
const character = value[index] ?? '';
|
|
91
|
-
if (escaped) {
|
|
92
|
-
escaped = false;
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
if (character === '\\' && (quoted || commentDepth > 0)) {
|
|
96
|
-
escaped = true;
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
if (commentDepth > 0) {
|
|
100
|
-
if (character === '(')
|
|
101
|
-
commentDepth += 1;
|
|
102
|
-
else if (character === ')')
|
|
103
|
-
commentDepth -= 1;
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
if (quoted) {
|
|
107
|
-
if (character === '"')
|
|
108
|
-
quoted = false;
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
if (character === '"')
|
|
112
|
-
quoted = true;
|
|
113
|
-
else if (character === '(')
|
|
114
|
-
commentDepth = 1;
|
|
115
|
-
else if (character === ')')
|
|
116
|
-
return null;
|
|
117
|
-
else if (trackAngles && character === '<') {
|
|
118
|
-
if (angleDepth !== 0)
|
|
119
|
-
return null;
|
|
120
|
-
angleDepth = 1;
|
|
121
|
-
}
|
|
122
|
-
else if (trackAngles && character === '>') {
|
|
123
|
-
if (angleDepth !== 1)
|
|
124
|
-
return null;
|
|
125
|
-
angleDepth = 0;
|
|
126
|
-
}
|
|
127
|
-
else if (character === delimiter && angleDepth === 0) {
|
|
128
|
-
segments.push(value.slice(start, index));
|
|
129
|
-
start = index + 1;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
if (escaped || quoted || commentDepth !== 0 || angleDepth !== 0)
|
|
133
|
-
return null;
|
|
134
|
-
segments.push(value.slice(start));
|
|
135
|
-
return segments;
|
|
136
|
-
}
|
|
137
|
-
function angleAddress(value) {
|
|
138
|
-
let quoted = false;
|
|
139
|
-
let escaped = false;
|
|
140
|
-
let commentDepth = 0;
|
|
141
|
-
let start = -1;
|
|
142
|
-
let end = -1;
|
|
143
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
144
|
-
const character = value[index] ?? '';
|
|
145
|
-
if (escaped) {
|
|
146
|
-
escaped = false;
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
if (character === '\\' && (quoted || commentDepth > 0)) {
|
|
150
|
-
escaped = true;
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
if (commentDepth > 0) {
|
|
154
|
-
if (character === '(')
|
|
155
|
-
commentDepth += 1;
|
|
156
|
-
else if (character === ')')
|
|
157
|
-
commentDepth -= 1;
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
if (quoted) {
|
|
161
|
-
if (character === '"')
|
|
162
|
-
quoted = false;
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
if (character === '"')
|
|
166
|
-
quoted = true;
|
|
167
|
-
else if (character === '(')
|
|
168
|
-
commentDepth = 1;
|
|
169
|
-
else if (character === ')')
|
|
170
|
-
return null;
|
|
171
|
-
else if (character === '<') {
|
|
172
|
-
if (start >= 0 || end >= 0)
|
|
173
|
-
return null;
|
|
174
|
-
start = index;
|
|
175
|
-
}
|
|
176
|
-
else if (character === '>') {
|
|
177
|
-
if (start < 0 || end >= 0)
|
|
178
|
-
return null;
|
|
179
|
-
end = index;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
if (escaped || quoted || commentDepth !== 0 || (start >= 0) !== (end >= 0))
|
|
183
|
-
return null;
|
|
184
|
-
return start >= 0 ? value.slice(start + 1, end) : value;
|
|
185
|
-
}
|
|
186
|
-
function addressDomain(value) {
|
|
187
|
-
let candidate = '';
|
|
188
|
-
let quoted = false;
|
|
189
|
-
let escaped = false;
|
|
190
|
-
let commentDepth = 0;
|
|
191
|
-
const separators = [];
|
|
192
|
-
for (const character of value) {
|
|
193
|
-
if (escaped) {
|
|
194
|
-
candidate += character;
|
|
195
|
-
escaped = false;
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
if (character === '\\' && (quoted || commentDepth > 0)) {
|
|
199
|
-
if (quoted)
|
|
200
|
-
candidate += character;
|
|
201
|
-
escaped = true;
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
if (commentDepth > 0) {
|
|
205
|
-
if (character === '(')
|
|
206
|
-
commentDepth += 1;
|
|
207
|
-
else if (character === ')')
|
|
208
|
-
commentDepth -= 1;
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
if (quoted) {
|
|
212
|
-
candidate += character;
|
|
213
|
-
if (character === '"')
|
|
214
|
-
quoted = false;
|
|
215
|
-
continue;
|
|
216
|
-
}
|
|
217
|
-
if (character === '"') {
|
|
218
|
-
quoted = true;
|
|
219
|
-
candidate += character;
|
|
220
|
-
}
|
|
221
|
-
else if (character === '(')
|
|
222
|
-
commentDepth = 1;
|
|
223
|
-
else if (character === ')')
|
|
224
|
-
return null;
|
|
225
|
-
else {
|
|
226
|
-
if (character === '@')
|
|
227
|
-
separators.push(candidate.length);
|
|
228
|
-
candidate += character;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
if (escaped || quoted || commentDepth !== 0 || separators.length !== 1)
|
|
232
|
-
return null;
|
|
233
|
-
const separator = separators[0] ?? -1;
|
|
234
|
-
const local = candidate.slice(0, separator).trim();
|
|
235
|
-
const domain = candidate.slice(separator + 1).trim();
|
|
236
|
-
const validLocal = /^(?:[^\s"<>(),:;@]+|"(?:[^"\\]|\\.)+")$/u.test(local);
|
|
237
|
-
if (!validLocal || !domain || /\s/u.test(domain))
|
|
238
|
-
return null;
|
|
239
|
-
return normalizeHeaderDomain(domain);
|
|
240
|
-
}
|
|
241
|
-
function addressDomains(values) {
|
|
242
|
-
const domains = new Set();
|
|
243
|
-
for (const value of values) {
|
|
244
|
-
const mailboxes = lexicalSegments(value, ',', true);
|
|
245
|
-
if (!mailboxes)
|
|
246
|
-
continue;
|
|
247
|
-
for (const mailbox of mailboxes) {
|
|
248
|
-
const selected = angleAddress(mailbox);
|
|
249
|
-
const domain = selected === null ? null : addressDomain(selected);
|
|
250
|
-
if (domain)
|
|
251
|
-
domains.add(domain);
|
|
252
|
-
if (domains.size >= MAX_MAIL_REVIEW_DOMAINS)
|
|
253
|
-
return [...domains].sort();
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
return [...domains].sort();
|
|
257
|
-
}
|
|
258
|
-
function messageIdDomains(values) {
|
|
259
|
-
return addressDomains(values.map((value) => value.replace(/[<>]/gu, '')));
|
|
260
|
-
}
|
|
261
|
-
function authenticationState(values, method, directValues = []) {
|
|
262
|
-
const states = new Set();
|
|
263
|
-
let observations = 0;
|
|
264
|
-
for (const value of values) {
|
|
265
|
-
const clauses = lexicalSegments(value, ';');
|
|
266
|
-
if (!clauses)
|
|
267
|
-
continue;
|
|
268
|
-
for (const clause of clauses) {
|
|
269
|
-
const match = /^\s*([a-z][a-z0-9_-]*)\s*=\s*([a-z0-9_-]+)(?=\s|$)/iu.exec(clause);
|
|
270
|
-
if (match?.[1]?.toLowerCase() === method) {
|
|
271
|
-
observations += 1;
|
|
272
|
-
const raw = (match[2] ?? '').toLowerCase();
|
|
273
|
-
states.add(AUTHENTICATION_STATES.has(raw) ? raw : 'unknown');
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
for (const value of directValues) {
|
|
278
|
-
const match = /^\s*([a-z0-9_-]+)(?=\s|\(|$)/iu.exec(value);
|
|
279
|
-
const raw = (match?.[1] ?? '').toLowerCase();
|
|
280
|
-
if (!raw)
|
|
281
|
-
continue;
|
|
282
|
-
observations += 1;
|
|
283
|
-
states.add(AUTHENTICATION_STATES.has(raw) ? raw : 'unknown');
|
|
284
|
-
}
|
|
285
|
-
return Object.freeze({
|
|
286
|
-
state: observations === 0 ? 'unknown' : states.size === 1 ? [...states][0] : 'mixed',
|
|
287
|
-
observations,
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
function authenticationServiceDomains(values) {
|
|
291
|
-
const output = new Set();
|
|
292
|
-
for (const value of values) {
|
|
293
|
-
const clauses = lexicalSegments(value, ';');
|
|
294
|
-
const service = normalizeHeaderDomain((clauses?.[0] ?? '').trim().split(/\s/u, 1)[0] ?? '');
|
|
295
|
-
if (service)
|
|
296
|
-
output.add(service);
|
|
297
|
-
}
|
|
298
|
-
return [...output].sort().slice(0, MAX_MAIL_REVIEW_DOMAINS);
|
|
299
|
-
}
|
|
300
|
-
function dkimSigningDomains(values) {
|
|
301
|
-
const output = new Set();
|
|
302
|
-
for (const value of values) {
|
|
303
|
-
for (const clause of lexicalSegments(value, ';') ?? []) {
|
|
304
|
-
const match = /^\s*d\s*=\s*([^\s;]+)/iu.exec(clause);
|
|
305
|
-
const domain = normalizeHeaderDomain(match?.[1] ?? '');
|
|
306
|
-
if (domain)
|
|
307
|
-
output.add(domain);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
return [...output].sort().slice(0, MAX_MAIL_REVIEW_DOMAINS);
|
|
311
|
-
}
|
|
312
77
|
function reportedDate(values) {
|
|
313
78
|
if (values.length !== 1 || values[0].length > 240)
|
|
314
79
|
return null;
|
|
@@ -348,21 +113,26 @@ function addDomainRoles(store, domains, role) {
|
|
|
348
113
|
store.set(domain, roles);
|
|
349
114
|
}
|
|
350
115
|
}
|
|
351
|
-
export function buildCliMailHeaderReview(value, generatedAt = new Date().toISOString()) {
|
|
116
|
+
export function buildCliMailHeaderReview(value, generatedAt = new Date().toISOString(), trustedHeaders = []) {
|
|
352
117
|
const normalizedGeneratedAt = normalizeExplicitIsoTimestamp(generatedAt);
|
|
353
118
|
if (!normalizedGeneratedAt)
|
|
354
119
|
throw new CliUsageError('Mail-header review generation time must be a valid ISO 8601 timestamp.');
|
|
355
120
|
const parsed = parseHeaderFields(value);
|
|
121
|
+
let authenticationReview;
|
|
122
|
+
try {
|
|
123
|
+
authenticationReview = selectReceiverTrust(reviewMailAuthentication(parsed.fields), trustedHeaders);
|
|
124
|
+
}
|
|
125
|
+
catch (cause) {
|
|
126
|
+
if (cause instanceof TypeError)
|
|
127
|
+
throw new CliUsageError(cause.message);
|
|
128
|
+
throw cause;
|
|
129
|
+
}
|
|
356
130
|
const fromDomains = addressDomains(fieldsNamed(parsed.fields, 'from'));
|
|
357
131
|
const replyToDomains = addressDomains(fieldsNamed(parsed.fields, 'reply-to'));
|
|
358
132
|
const returnPathDomains = addressDomains(fieldsNamed(parsed.fields, 'return-path'));
|
|
359
133
|
const messageIdentifierDomains = messageIdDomains(fieldsNamed(parsed.fields, 'message-id'));
|
|
360
134
|
const authenticationResults = fieldsNamed(parsed.fields, 'authentication-results');
|
|
361
|
-
const
|
|
362
|
-
const authentication = Object.fromEntries(AUTHENTICATION_METHODS.map((method) => [
|
|
363
|
-
method,
|
|
364
|
-
authenticationState(authenticationResults, method, method === 'spf' ? receivedSpf : []),
|
|
365
|
-
]));
|
|
135
|
+
const authentication = aggregateMailAuthentication(authenticationReview);
|
|
366
136
|
const authServiceDomains = authenticationServiceDomains(authenticationResults);
|
|
367
137
|
const signerDomains = dkimSigningDomains(fieldsNamed(parsed.fields, 'dkim-signature'));
|
|
368
138
|
const receivedValues = fieldsNamed(parsed.fields, 'received');
|
|
@@ -414,6 +184,7 @@ export function buildCliMailHeaderReview(value, generatedAt = new Date().toISOSt
|
|
|
414
184
|
messageIdentifierDomains: Object.freeze(messageIdentifierDomains),
|
|
415
185
|
}),
|
|
416
186
|
authentication: Object.freeze(authentication),
|
|
187
|
+
authenticationReview,
|
|
417
188
|
routing: Object.freeze({
|
|
418
189
|
receivedHops: Object.freeze(receivedHops),
|
|
419
190
|
omittedReceivedHops,
|
|
@@ -428,6 +199,7 @@ export function buildCliMailHeaderReview(value, generatedAt = new Date().toISOSt
|
|
|
428
199
|
limitations: Object.freeze([
|
|
429
200
|
'This review parses analyst-selected message headers offline. It makes no DNS, SMTP, HTTP, registry or provider request.',
|
|
430
201
|
'Authentication results are claims reported in the supplied headers; they are not independently validated here.',
|
|
202
|
+
'Receiver trust is an explicit analyst selection for an exact header, not a conclusion from a matching service name. Aggregate counts combine all reported sources, not a trusted verdict.',
|
|
431
203
|
'Exact-domain alignment is descriptive. Divergence can be legitimate and does not establish spoofing, abuse or maliciousness.',
|
|
432
204
|
'Display names, subjects, address local parts, message bodies, attachments and raw header values are not retained in the output.',
|
|
433
205
|
...(omittedReceivedHops ? [`${omittedReceivedHops} older Received header${omittedReceivedHops === 1 ? ' was' : 's were'} omitted by the ${MAX_MAIL_RECEIVED_HOPS}-hop bound.`] : []),
|
|
@@ -444,8 +216,13 @@ export function formatCliMailHeaderReview(document) {
|
|
|
444
216
|
`Reply-To domains ${document.identity.replyToDomains.join(', ') || 'Unavailable'}`,
|
|
445
217
|
`Return-Path ${document.identity.returnPathDomains.join(', ') || 'Unavailable'}`,
|
|
446
218
|
'',
|
|
447
|
-
'
|
|
448
|
-
...
|
|
219
|
+
'Authentication by source header',
|
|
220
|
+
...document.authenticationReview.headers.flatMap(header => [
|
|
221
|
+
` ${authenticationHeaderLabel(header)} · ${header.state} · receiver trust: ${header.receiverTrust.replaceAll('_', ' ')}`,
|
|
222
|
+
...header.claims.map(claim => ` ${claim.method.toUpperCase()}/${claim.methodVersion}: ${claim.result}${claim.state === 'unsupported' ? ' (unsupported)' : ''}${claim.domains.map(value => ` · ${value.property}=${value.domain}`).join('')}${claim.duplicateProperties.length ? ` · ambiguous: ${claim.duplicateProperties.join(', ')}` : ''}`),
|
|
223
|
+
...(header.duplicateOf ? [` Duplicate of header ${header.duplicateOf} in this part.`] : []),
|
|
224
|
+
...header.issues.map(issue => ` ${issue}`),
|
|
225
|
+
]),
|
|
449
226
|
'',
|
|
450
227
|
'Exact-domain alignment',
|
|
451
228
|
` From / Reply-To ${document.alignment.fromToReplyTo}`,
|
package/cli/mail-review.mjs
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
2
|
import { getDomain } from 'tldts';
|
|
3
|
-
import { scanBoundedJson } from "../
|
|
4
|
-
import {
|
|
3
|
+
import { scanBoundedJson } from "../packages/analysis/bounded-json.mjs";
|
|
4
|
+
import { compareCodeUnits } from "../lib/bounded-contract-normalizers.mjs";
|
|
5
|
+
import { isValidAsciiDomainName } from "../packages/contracts/domain-name.mjs";
|
|
5
6
|
import { isRecord, recordOrEmpty } from "../lib/json-record.mjs";
|
|
6
7
|
import { classifyQuery } from "../lib/classify.mjs";
|
|
7
8
|
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
8
9
|
import { CliUsageError } from "./errors.mjs";
|
|
10
|
+
import { PASSIVE_MAIL_INTERPRETATION, PASSIVE_MAIL_LABELS } from "../packages/contracts/passive-mail.mjs";
|
|
9
11
|
export const CLI_MAIL_REVIEW_SCHEMA = 'whoisleuth.cli.mail-review';
|
|
10
12
|
export const CLI_MAIL_REVIEW_VERSION = 4;
|
|
11
13
|
export const MAX_MAIL_REVIEW_INPUT_BYTES = 16 * 1024 * 1024;
|
|
@@ -326,7 +328,7 @@ function buildCliMailReview(textValue, generatedAt = new Date().toISOString()) {
|
|
|
326
328
|
limitation: 'Shared mail providers are common and do not establish common ownership, control, intent, safety, or maliciousness.',
|
|
327
329
|
};
|
|
328
330
|
})
|
|
329
|
-
.sort((left, right) => left.providerDomain
|
|
331
|
+
.sort((left, right) => compareCodeUnits(left.providerDomain, right.providerDomain));
|
|
330
332
|
const providerRelationships = allProviderRelationships.slice(0, 100);
|
|
331
333
|
const rowsWithIncompleteDns = rows.filter((row) => row.dnsStatus !== 'success' || row.hasMx === null || row.hasNullMx === null).length;
|
|
332
334
|
const providerCoverage = {
|
|
@@ -355,7 +357,7 @@ function buildCliMailReview(textValue, generatedAt = new Date().toISOString()) {
|
|
|
355
357
|
providerCoverage,
|
|
356
358
|
limitations: [
|
|
357
359
|
'This review is passive and uses DNS evidence already retained in a WHOISleuth Bulk result; it makes no network request.',
|
|
358
|
-
|
|
360
|
+
PASSIVE_MAIL_INTERPRETATION,
|
|
359
361
|
'SMTP delivery, mailbox existence, catch-all behaviour, banner collection, and message acceptance were not tested.',
|
|
360
362
|
...(failedRows.length ? [`${failedRows.length} input target${failedRows.length === 1 ? '' : 's'} failed collection and cannot contribute mail evidence. Input positions and normalised targets are retained; consult the original input for failure diagnostics.`] : []),
|
|
361
363
|
...(rowsWithIncompleteDns ? [`${rowsWithIncompleteDns} reviewed domain${rowsWithIncompleteDns === 1 ? ' has' : 's have'} incomplete DNS evidence; unobserved provider relationships remain unknown.`] : []),
|
|
@@ -371,15 +373,15 @@ function formatCliMailReview(document) {
|
|
|
371
373
|
`Input targets ${document.inputCoverage.inputRows}`,
|
|
372
374
|
`Domains ${document.rows.length}`,
|
|
373
375
|
`Failed targets ${document.inputCoverage.failedRows}`,
|
|
374
|
-
`
|
|
375
|
-
`
|
|
376
|
+
`MX + SPF + DMARC ${document.counts.authenticated_mail}`,
|
|
377
|
+
`DNS policy gaps ${document.counts.mail_auth_gap}`,
|
|
376
378
|
`Null MX ${document.counts.null_mx}`,
|
|
377
379
|
`No explicit MX ${document.counts.no_explicit_mx}`,
|
|
378
380
|
`Incomplete ${document.counts.evidence_incomplete + document.counts.mail_auth_incomplete + document.inputCoverage.failedRows}`,
|
|
379
381
|
'',
|
|
380
382
|
];
|
|
381
383
|
for (const row of document.rows) {
|
|
382
|
-
lines.push(`${row.domain} ${row.state
|
|
384
|
+
lines.push(`${row.domain} ${PASSIVE_MAIL_LABELS[row.state]}`);
|
|
383
385
|
lines.push(` Observed ${row.provenance.observedAt ?? 'unknown'} · ${row.provenance.collectionOrigin.replaceAll('_', ' ')}`);
|
|
384
386
|
lines.push(` DNS observed ${row.provenance.dnsObservedAt ?? 'unknown'} · Source output ${row.provenance.sourceGeneratedAt}`);
|
|
385
387
|
lines.push(` MX providers ${row.providerDomains.join(', ') || 'None observed'}${row.providerDomainsOmitted ? ` · +${row.providerDomainsOmitted} omitted` : ''}`);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {} from "./command-argument-grammar.mjs";
|
|
2
|
+
import { commandDefaultText } from "./command-reference.mjs";
|
|
3
|
+
import { NETWORK_COMMAND_DEFINITIONS } from "./network-command-definitions.mjs";
|
|
4
|
+
import { terminalOptions, jsonOutput, parseOutput, singleInputAction, } from "./argument-values.mjs";
|
|
5
|
+
function parsePostureArguments(parsed) {
|
|
6
|
+
const mailProfile = parsed.optionValue('--mail-profile') ?? commandDefaultText('posture', '--mail-profile');
|
|
7
|
+
return {
|
|
8
|
+
action: 'posture',
|
|
9
|
+
domain: parsed.positionalValue('domain'),
|
|
10
|
+
output: parseOutput(parsed, NETWORK_COMMAND_DEFINITIONS.posture),
|
|
11
|
+
selectorText: parsed.optionValue('--selectors'),
|
|
12
|
+
retiredSelectorText: parsed.optionValue('--retired-selectors'),
|
|
13
|
+
mailProfile: mailProfile === 'defensive-no-mail'
|
|
14
|
+
? 'defensive_no_mail'
|
|
15
|
+
: mailProfile,
|
|
16
|
+
ownedDomain: parsed.hasOption('--owned-domain'),
|
|
17
|
+
...(parsed.hasOption('--include-inherited-dns') ? { includeInheritedDns: true } : {}),
|
|
18
|
+
...terminalOptions(parsed),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function parseDnssecValidateArguments(parsed) {
|
|
22
|
+
return {
|
|
23
|
+
action: 'dnssec-validate',
|
|
24
|
+
target: parsed.positionalValue('domain'),
|
|
25
|
+
resolver: parsed.optionValue('--resolver'),
|
|
26
|
+
trustAnchorSource: parsed.optionValue('--trust-anchor'),
|
|
27
|
+
ownedOrAuthorized: true,
|
|
28
|
+
output: jsonOutput(parsed),
|
|
29
|
+
...terminalOptions(parsed),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function parseMailTransportArguments(parsed) {
|
|
33
|
+
return {
|
|
34
|
+
action: 'mail-transport',
|
|
35
|
+
source: parsed.positionalValue('source'),
|
|
36
|
+
resolver: parsed.optionValue('--resolver'),
|
|
37
|
+
trustAnchorSource: parsed.optionValue('--trust-anchor'),
|
|
38
|
+
ownedOrAuthorized: true,
|
|
39
|
+
activeProbeAcknowledged: true,
|
|
40
|
+
output: jsonOutput(parsed),
|
|
41
|
+
...terminalOptions(parsed),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export const NETWORK_ARGUMENT_PARSERS = Object.freeze({
|
|
45
|
+
'ct-search': (parsed) => singleInputAction('ct-search', parsed, 'keyword'),
|
|
46
|
+
posture: parsePostureArguments,
|
|
47
|
+
http: (parsed) => singleInputAction('http', parsed, 'domain'),
|
|
48
|
+
tls: (parsed) => singleInputAction('tls', parsed, 'hostname'),
|
|
49
|
+
'dnssec-validate': parseDnssecValidateArguments,
|
|
50
|
+
'mail-transport': parseMailTransportArguments,
|
|
51
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { positional, OPTIONAL_FILE_POSITIONAL, constraint, EMPTY_CONSTRAINTS, commandSeed } from "./command-definition.mjs";
|
|
2
|
+
export const NETWORK_COMMAND_DEFINITIONS = Object.freeze({
|
|
3
|
+
"ct-search": commandSeed({
|
|
4
|
+
reference: {
|
|
5
|
+
description: 'Search certificate-transparency observations for one bounded keyword.',
|
|
6
|
+
example: 'whoisleuth ct-search "example brand" --json',
|
|
7
|
+
boundary: 'Certificate observations do not prove website activity, registration ownership, or malicious intent.',
|
|
8
|
+
},
|
|
9
|
+
collection: { scope: 'Accepts one bounded search keyword and queries the fixed certificate-transparency source.' },
|
|
10
|
+
summary: 'Search certificate observations',
|
|
11
|
+
options: ['--json', '--quiet', '--no-color'],
|
|
12
|
+
positionals: Object.freeze([positional('keyword', 'text', 0, 1, [], 'argv_or_stdin')]),
|
|
13
|
+
constraints: EMPTY_CONSTRAINTS,
|
|
14
|
+
handlerOwner: 'network',
|
|
15
|
+
networkEffect: 'always_network',
|
|
16
|
+
common: false,
|
|
17
|
+
schemaIdentifiers: Object.freeze(['whoisleuth.cli.ct-search']),
|
|
18
|
+
primaryArtefacts: Object.freeze([]),
|
|
19
|
+
planSupport: false,
|
|
20
|
+
additionalOutputFormats: Object.freeze([]),
|
|
21
|
+
bootstrapProfile: 'allowed',
|
|
22
|
+
}),
|
|
23
|
+
posture: commandSeed({
|
|
24
|
+
reference: {
|
|
25
|
+
description: 'Review bounded DNS mail, delegation, and domain-control posture.',
|
|
26
|
+
example: 'whoisleuth posture example.test --mail-profile standard --json',
|
|
27
|
+
boundary: 'Missing or failed DNS observations remain inconclusive. --include-inherited-dns adds inherited policy and reporting-boundary reviews within 32 additional TXT queries and ten seconds, plus a direct parent-delegation sample. No message is sent and receiver enforcement is not inferred.',
|
|
28
|
+
},
|
|
29
|
+
collection: { scope: 'Accepts one domain and performs bounded RDAP, DNS, and conditional MTA-STS HTTPS requests. --include-inherited-dns separately adds ancestor and reporting-destination DMARC questions and direct DNS/TCP to sampled parent servers.' },
|
|
30
|
+
summary: 'Review DNS and mail posture',
|
|
31
|
+
options: ['--selectors', '--retired-selectors', '--mail-profile', '--include-inherited-dns', '--json', '--sarif', '--owned-domain', '--quiet', '--no-color'],
|
|
32
|
+
positionals: Object.freeze([positional('domain', 'text', 0, 1, [], 'argv_or_stdin')]),
|
|
33
|
+
constraints: Object.freeze([
|
|
34
|
+
constraint({ kind: 'mutually_exclusive', options: ['--json', '--sarif'] }),
|
|
35
|
+
constraint({ kind: 'requires_all', option: '--sarif', requiredOptions: ['--owned-domain'] }),
|
|
36
|
+
]),
|
|
37
|
+
handlerOwner: 'network',
|
|
38
|
+
networkEffect: 'always_network',
|
|
39
|
+
common: false,
|
|
40
|
+
schemaIdentifiers: Object.freeze(['whoisleuth.cli.posture']),
|
|
41
|
+
primaryArtefacts: Object.freeze([]),
|
|
42
|
+
planSupport: false,
|
|
43
|
+
additionalOutputFormats: Object.freeze([]),
|
|
44
|
+
bootstrapProfile: 'allowed',
|
|
45
|
+
}),
|
|
46
|
+
http: commandSeed({
|
|
47
|
+
reference: {
|
|
48
|
+
description: 'Inspect one homepage request, redirects, and bounded response metadata.',
|
|
49
|
+
example: 'whoisleuth http example.test --json',
|
|
50
|
+
boundary: 'Requests use the shared public-address and redirect guards. Fixed content-coding and cache-policy metadata describes only the selected response, excludes raw header values, and does not prove caching, transfer savings, performance, privacy, or safety. This is not a rendered browser or vulnerability scan.',
|
|
51
|
+
},
|
|
52
|
+
collection: { scope: 'Accepts one domain and follows only the bounded SSRF-guarded homepage redirect workflow.' },
|
|
53
|
+
summary: 'Inspect one homepage request',
|
|
54
|
+
options: ['--json', '--quiet', '--no-color'],
|
|
55
|
+
positionals: Object.freeze([positional('domain', 'text', 0, 1, [], 'argv_or_stdin')]),
|
|
56
|
+
constraints: EMPTY_CONSTRAINTS,
|
|
57
|
+
handlerOwner: 'network',
|
|
58
|
+
networkEffect: 'always_network',
|
|
59
|
+
common: false,
|
|
60
|
+
schemaIdentifiers: Object.freeze(['whoisleuth.cli.http']),
|
|
61
|
+
primaryArtefacts: Object.freeze([]),
|
|
62
|
+
planSupport: false,
|
|
63
|
+
additionalOutputFormats: Object.freeze([]),
|
|
64
|
+
bootstrapProfile: 'allowed',
|
|
65
|
+
}),
|
|
66
|
+
tls: commandSeed({
|
|
67
|
+
reference: {
|
|
68
|
+
description: 'Inspect one hostname certificate through a bounded TLS connection.',
|
|
69
|
+
example: 'whoisleuth tls example.test --json',
|
|
70
|
+
boundary: 'One observed connection is point-in-time evidence and does not establish every address, edge, or historical certificate.',
|
|
71
|
+
},
|
|
72
|
+
collection: { scope: 'Accepts one public hostname and opens one bounded certificate connection.' },
|
|
73
|
+
summary: 'Inspect one TLS connection',
|
|
74
|
+
options: ['--json', '--quiet', '--no-color'],
|
|
75
|
+
positionals: Object.freeze([positional('hostname', 'text', 0, 1, [], 'argv_or_stdin')]),
|
|
76
|
+
constraints: EMPTY_CONSTRAINTS,
|
|
77
|
+
handlerOwner: 'network',
|
|
78
|
+
networkEffect: 'always_network',
|
|
79
|
+
common: false,
|
|
80
|
+
schemaIdentifiers: Object.freeze(['whoisleuth.cli.tls']),
|
|
81
|
+
primaryArtefacts: Object.freeze([]),
|
|
82
|
+
planSupport: false,
|
|
83
|
+
additionalOutputFormats: Object.freeze([]),
|
|
84
|
+
bootstrapProfile: 'allowed',
|
|
85
|
+
}),
|
|
86
|
+
"dnssec-validate": commandSeed({
|
|
87
|
+
reference: {
|
|
88
|
+
description: 'Cryptographically validate one authorised DNSSEC chain from a supplied trust anchor through one selected public resolver.',
|
|
89
|
+
example: 'whoisleuth dnssec-validate example.test --resolver "$PUBLIC_RESOLVER_IP" --trust-anchor anchor.json --owned-or-authorized --json',
|
|
90
|
+
boundary: 'This isolated action is never invoked by Lookup, Bulk, monitoring, or recipes. It caps DNS queries, aliases, delegations, bytes, and duration; transport and validation failures remain separate, and secure is not a general safety verdict.',
|
|
91
|
+
},
|
|
92
|
+
collection: { scope: 'Accepts one authorised domain, one public resolver IP, and one local trust-anchor file; DNS-over-TCP validation is capped at 32 queries and 15 seconds.' },
|
|
93
|
+
summary: 'Validate an authorised DNSSEC chain',
|
|
94
|
+
options: ['--resolver', '--trust-anchor', '--owned-or-authorized', '--json', '--quiet', '--no-color'],
|
|
95
|
+
positionals: Object.freeze([positional('domain', 'text', 1, 1)]),
|
|
96
|
+
constraints: Object.freeze([
|
|
97
|
+
constraint({ kind: 'required', options: ['--resolver', '--trust-anchor', '--owned-or-authorized'] }),
|
|
98
|
+
]),
|
|
99
|
+
handlerOwner: 'network',
|
|
100
|
+
networkEffect: 'always_network',
|
|
101
|
+
common: false,
|
|
102
|
+
schemaIdentifiers: Object.freeze(['whoisleuth.dnssec-chain-validation', 'whoisleuth.dnssec-trust-anchor']),
|
|
103
|
+
primaryArtefacts: Object.freeze([]),
|
|
104
|
+
planSupport: false,
|
|
105
|
+
additionalOutputFormats: Object.freeze([]),
|
|
106
|
+
bootstrapProfile: 'allowed',
|
|
107
|
+
}),
|
|
108
|
+
"mail-transport": commandSeed({
|
|
109
|
+
reference: {
|
|
110
|
+
description: 'Review selected authorised MX endpoints, DNSSEC-qualified TLSA evidence, SMTP capabilities, and optional STARTTLS certificates.',
|
|
111
|
+
example: 'whoisleuth mail-transport selected-mx.json --resolver "$PUBLIC_RESOLVER_IP" --trust-anchor anchor.json --owned-or-authorized --active-probe --json',
|
|
112
|
+
boundary: 'This isolated action probes at most three selected MX hosts sequentially, reports selection, public revalidation, connection, and address authentication separately, sends only EHLO and optional STARTTLS, never retries, and performs no authentication, relay, recipient, mailbox, catch-all, or message test. If a DANE-TA TLSA usage 2 association is published, active collection retains only the leaf certificate and leaves that comparison partial without certificate-path construction and trust-anchor path validation. SMTP relay PKIX-TA usage 0 and PKIX-EE usage 1 records remain unsupported and cannot complete SMTP DANE assurance; a separate usage 3 match remains eligible.',
|
|
113
|
+
},
|
|
114
|
+
collection: { scope: 'Accepts at most three selected authorised MX hosts, uses one public resolver, and performs sequential bounded SMTP connections with no retries.' },
|
|
115
|
+
summary: 'Review selected authorised SMTP transports',
|
|
116
|
+
options: ['--resolver', '--trust-anchor', '--owned-or-authorized', '--active-probe', '--json', '--quiet', '--no-color'],
|
|
117
|
+
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
118
|
+
constraints: Object.freeze([
|
|
119
|
+
constraint({ kind: 'required', options: ['--resolver', '--trust-anchor', '--owned-or-authorized', '--active-probe'] }),
|
|
120
|
+
]),
|
|
121
|
+
handlerOwner: 'network',
|
|
122
|
+
networkEffect: 'always_network',
|
|
123
|
+
common: false,
|
|
124
|
+
schemaIdentifiers: Object.freeze(['whoisleuth.mail-transport.input', 'whoisleuth.cli.mail-transport-review']),
|
|
125
|
+
primaryArtefacts: Object.freeze([]),
|
|
126
|
+
planSupport: false,
|
|
127
|
+
additionalOutputFormats: Object.freeze([]),
|
|
128
|
+
bootstrapProfile: 'allowed',
|
|
129
|
+
}),
|
|
130
|
+
});
|