@slicedearth/whoisleuth-cli 2.2.0 → 2.4.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/NOTICE +6 -0
- package/README.md +20 -15
- package/bin/whoisleuth.mjs +8 -1
- package/cli/archive-content-digest.mjs +14 -0
- package/cli/archive-inspect.mjs +20 -53
- package/cli/arguments.mjs +454 -1748
- package/cli/artifact-validation/case-response.mjs +66 -35
- package/cli/artifact-validation/investigation-capsule.mjs +52 -31
- package/cli/artifact-validation/lookup-evidence.mjs +12 -6
- package/cli/artifact-validation/signed-review.mjs +112 -72
- package/cli/artifact-validation/structure-primitives.mjs +1 -105
- package/cli/artifact-verify.mjs +90 -24
- package/cli/assurance-command-runner.mjs +39 -4
- package/cli/bagit-folder.mjs +57 -0
- package/cli/bagit-review.mjs +23 -0
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +34 -3
- package/cli/bulk-command-runner.mjs +2 -0
- package/cli/bulk-output.mjs +29 -3
- package/cli/case-command.mjs +195 -0
- package/cli/case-pack.mjs +66 -111
- package/cli/command-argument-grammar.mjs +215 -0
- package/cli/command-reference.mjs +342 -285
- package/cli/completion.mjs +303 -80
- package/cli/config-profile.mjs +7 -5
- package/cli/ct-event-intake.mjs +12 -12
- package/cli/discover.mjs +4 -3
- package/cli/discovery-observation-snapshot.mjs +59 -18
- package/cli/discovery-scan-command-runner.mjs +21 -0
- package/cli/discovery-scan.mjs +13 -4
- package/cli/discovery-workflow.mjs +1 -1
- package/cli/domain-control-monitor.mjs +50 -20
- package/cli/domain-control-observations.mjs +149 -102
- package/cli/errors.mjs +33 -1
- package/cli/evidence-command-runner.mjs +16 -5
- package/cli/evidence-signing.mjs +2 -2
- package/cli/external-observation-mapping.mjs +7 -8
- package/cli/formatters/terminal-lookup.mjs +18 -46
- package/cli/formatters/terminal-shared.mjs +5 -3
- package/cli/formatters/terminal-technology.mjs +114 -0
- package/cli/inline-command-families.mjs +2 -0
- package/cli/input.mjs +116 -0
- package/cli/interactive-launcher.mjs +2 -160
- package/cli/interchange-report.mjs +18 -8
- package/cli/investigation-artifacts.mjs +176 -0
- package/cli/investigation-checkpoint.mjs +10 -0
- package/cli/investigation-folder.mjs +114 -0
- package/cli/investigation-manifest.mjs +1 -112
- package/cli/investigation-package-review.mjs +103 -0
- package/cli/investigation-plan.mjs +32 -19
- package/cli/investigation-run.mjs +188 -33
- package/cli/local-document-checkpoint.mjs +170 -0
- package/cli/lookalike-calibration.mjs +7 -2
- package/cli/lookup-brief.mjs +4 -2
- package/cli/lookup-browser-view.mjs +1 -1
- package/cli/lookup-browser.mjs +5 -12
- package/cli/lookup-command-runner.mjs +17 -6
- package/cli/lookup-diff.mjs +37 -3
- package/cli/lookup-plan.mjs +11 -5
- package/cli/lookup-reconcile.mjs +10 -8
- package/cli/mail-header-review.mjs +467 -0
- package/cli/mail-review.mjs +66 -14
- package/cli/manual.mjs +1 -0
- package/cli/network-command-runner.mjs +2 -0
- package/cli/output-file.mjs +31 -5
- package/cli/registry-doctor.mjs +3 -1
- package/cli/registry-support.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +2 -2
- package/cli/review-command-runner.mjs +70 -0
- package/cli/risk-calibration.mjs +19 -17
- package/cli/runner.mjs +62 -7
- package/cli/sharing-review.mjs +21 -34
- package/cli/signer-trust.mjs +104 -0
- package/cli/source-reliability.mjs +74 -83
- package/cli/terminal-input.mjs +168 -0
- package/cli/workflow-command-runner.mjs +68 -13
- package/docs/cli-reference.md +77 -24
- package/docs/cli.md +300 -25
- package/lib/abort.mjs +4 -1
- package/lib/availability.mjs +161 -112
- package/lib/bounded-file.mjs +3 -1
- package/lib/bounded-html-document.mjs +125 -0
- package/lib/bounded-json.mjs +51 -8
- package/lib/browser-library-profile.mjs +42 -51
- package/lib/browser-library-worker.mjs +112 -0
- package/lib/client-behavior-profile.mjs +3 -3
- package/lib/ct-search.mjs +11 -4
- package/lib/dns-convergence-review.mjs +1 -0
- package/lib/dns-delegation-health.mjs +24 -14
- package/lib/dns-inheritance-review.mjs +217 -0
- package/lib/dns-intelligence.mjs +73 -26
- package/lib/dns-mx.mjs +1 -1
- package/lib/dnssec-chain-validation.mjs +6 -0
- package/lib/dnssec-evidence-validation.mjs +11 -3
- package/lib/domain-change-packet.mjs +49 -16
- package/lib/domain-change-review.mjs +14 -8
- package/lib/domain-control-flight-recorder.mjs +221 -117
- package/lib/domain-control-manifest.mjs +129 -69
- package/lib/domain-posture-analysis.mjs +20 -7
- package/lib/domain-posture-parsers.mjs +201 -8
- package/lib/domain-posture.mjs +147 -29
- package/lib/evidence-export-privacy.mjs +561 -0
- package/lib/evidence-export.mjs +11 -543
- package/lib/evidence-report.mjs +5 -3
- package/lib/favicon.mjs +117 -41
- package/lib/generated/cisa-kev-catalog.mjs +12 -4
- package/lib/generated/registrar-standing-catalogue.mjs +7 -7
- package/lib/generated/retire-browser-catalog.mjs +2335 -940
- package/lib/hostname.mjs +2 -38
- package/lib/html-signals.mjs +111 -108
- package/lib/http-evidence-bounds.mjs +2 -1
- package/lib/http-intelligence.mjs +39 -26
- package/lib/local-geoip-evidence.mjs +19 -3
- package/lib/lookup-cache.mjs +34 -11
- package/lib/lookup-child-profile-contract.mjs +24 -15
- package/lib/lookup-diagnostics.mjs +24 -1
- package/lib/lookup-progress.mjs +30 -13
- package/lib/lookup-response-contract.mjs +1048 -0
- package/lib/lookup-response.mjs +8 -10
- package/lib/lookup-source-progress.mjs +40 -14
- package/lib/lookup.mjs +48 -27
- package/lib/nameserver-preflight-review.mjs +1 -0
- package/lib/observed-network-context.mjs +43 -27
- package/lib/outbound-request-bounds.mjs +4 -1
- package/lib/page-fingerprints.mjs +69 -133
- package/lib/page-role-profile.mjs +12 -8
- package/lib/perceptual-hash.mjs +43 -14
- package/lib/rdap-bootstrap.mjs +72 -22
- package/lib/rdap-capabilities.mjs +1 -1
- package/lib/rdap-client.mjs +15 -7
- package/lib/rdap-normalization.mjs +33 -18
- package/lib/rdap-search-workbench.mjs +1 -1
- package/lib/rdap-transport.mjs +47 -20
- package/lib/rdap-types.mjs +2 -1
- package/lib/rdap.mjs +18 -3
- package/lib/registrable-domain.mjs +21 -1
- package/lib/registrar-standing-contract.mjs +1 -0
- package/lib/registry-capabilities.mjs +5 -1
- package/lib/registry-capability-catalogue.mjs +2 -1
- package/lib/registry-dates.mjs +2 -190
- package/lib/registry-insights.mjs +85 -34
- package/lib/response-policy.mjs +86 -48
- package/lib/risk-calibration-summary.mjs +18 -6
- package/lib/rpki-evidence.mjs +8 -5
- package/lib/safe-fetch.mjs +54 -47
- package/lib/security-txt.mjs +42 -7
- package/lib/service-binding-dns.mjs +8 -0
- package/lib/smtp-transport-review.mjs +27 -15
- package/lib/sslbl-certificates.generated.mjs +136 -135
- package/lib/static-html-analysis.mjs +178 -151
- package/lib/structured-data-identity.mjs +1 -0
- package/lib/technology-markup.mjs +100 -0
- package/lib/threat-intelligence-manifests.mjs +11 -139
- package/lib/threat-intelligence-runtime.mjs +24 -52
- package/lib/threat-intelligence-targets.mjs +6 -34
- package/lib/threat-intelligence-types.mjs +39 -0
- package/lib/typosquat-generator.mjs +19 -15
- package/lib/urlhaus-intelligence.mjs +8 -3
- package/lib/urlscan-intelligence.mjs +1 -1
- package/lib/website-security-posture.mjs +30 -6
- package/lib/website-technology.mjs +153 -45
- package/lib/whois-authority.mjs +14 -2
- package/lib/whois-chain.mjs +9 -2
- package/lib/whois-common-formats.mjs +5 -5
- package/lib/whois-contracts.mjs +2 -1
- package/lib/whois-dialects.mjs +2 -2
- package/lib/whois-parser-support.mjs +8 -1
- package/lib/whois-transport.mjs +23 -7
- package/lib/zone-intent-review.mjs +16 -6
- package/package.json +2 -1
- package/packages/cases/case-attachment-model.mjs +83 -0
- package/packages/cases/case-evidence-model.mjs +156 -62
- package/packages/cases/case-export-input.mjs +53 -0
- package/packages/cases/case-migration-model.mjs +127 -44
- package/packages/cases/case-model.mjs +1 -0
- package/packages/cases/case-recheck-model.mjs +91 -0
- package/packages/cases/case-record-contracts.mjs +5 -20
- package/packages/cases/case-record-core.mjs +12 -22
- package/packages/cases/case-record-decisions.mjs +72 -0
- package/packages/cases/case-record-model.mjs +1 -0
- package/packages/cases/case-record-operations.mjs +95 -31
- package/packages/cases/case-record-projection.mjs +221 -0
- package/packages/cases/case-report.mjs +70 -54
- package/packages/cases/case-response-actions.mjs +654 -0
- package/packages/cases/case-response-model.mjs +64 -1115
- package/packages/cases/case-response-outcomes.mjs +317 -0
- package/packages/cases/case-response-packet-render.mjs +153 -0
- package/packages/cases/case-response-packet-types.mjs +1 -0
- package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
- package/packages/cases/case-response-packet.mjs +74 -572
- package/packages/cases/case-response-records.mjs +94 -0
- package/packages/cases/case-response-review-inputs.mjs +371 -0
- package/packages/cases/case-response-values.mjs +85 -0
- package/packages/cases/case-review-package.mjs +45 -0
- package/packages/cases/case-selection.mjs +46 -0
- package/packages/cases/case-storage-model.mjs +31 -2
- package/packages/cases/response-route-freshness.mjs +16 -0
- package/packages/comparison/capture-context.mjs +61 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +13 -11
- package/packages/comparison/page-similarity.mjs +5 -2
- package/packages/comparison/relationship-evidence.mjs +85 -8
- package/packages/comparison/relationship-provenance.mjs +93 -0
- package/packages/contracts/analyst-interchange.mjs +216 -0
- package/packages/contracts/analyst-review-state-contract.mjs +11 -0
- package/packages/contracts/analyst-review-state.mjs +5 -4
- package/packages/contracts/case-portability.mjs +366 -83
- package/packages/contracts/case-views-contract.mjs +12 -0
- package/packages/contracts/case-views.mjs +26 -0
- package/packages/contracts/cli-command-semantics.mjs +94 -0
- package/packages/contracts/cli-lookup.mjs +0 -1
- package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
- package/packages/contracts/domain-control-manifest.mjs +127 -73
- package/packages/contracts/domain-control-monitor.mjs +48 -45
- package/packages/contracts/domain-control-review.mjs +179 -121
- package/packages/contracts/domain-name.mjs +21 -0
- package/packages/contracts/external-observation-interchange.mjs +2 -1
- package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
- package/packages/contracts/http-url.mjs +2 -0
- package/packages/contracts/investigation-package-limits.mjs +24 -0
- package/packages/contracts/investigation-portability.mjs +56 -27
- package/packages/contracts/investigation-run.mjs +16 -0
- package/packages/contracts/ip-address.mjs +34 -0
- package/packages/contracts/lookup-evidence.mjs +35 -0
- package/packages/contracts/offline-comparison.mjs +1 -2
- package/packages/contracts/page-fingerprints.mjs +15 -0
- package/packages/contracts/risk-calibration.mjs +15 -11
- package/packages/contracts/schema-compatibility.mjs +1 -1
- package/packages/contracts/schema-lifecycle.mjs +83 -153
- package/packages/contracts/selected-file-limits.mjs +6 -0
- package/packages/contracts/vulnerability-identifiers.mjs +5 -0
- package/packages/contracts/web-capture.mjs +10 -0
- package/packages/contracts/workspace-portability.mjs +183 -211
- package/packages/evidence/artifact-integrity.mjs +8 -2
- package/packages/evidence/artifact-structure.mjs +105 -0
- package/packages/evidence/decision-fact.mjs +4 -1
- package/packages/evidence/domain-control-runtime.mjs +201 -33
- package/packages/evidence/domain-posture-context.mjs +79 -0
- package/packages/evidence/image-regions.mjs +56 -0
- package/packages/evidence/latest-observations.mjs +28 -0
- package/packages/evidence/lookup-target.mjs +104 -0
- package/packages/evidence/observation.mjs +16 -1
- package/packages/evidence/passphrase-encryption.mjs +44 -0
- package/packages/evidence/record-identity.mjs +72 -0
- package/packages/evidence/registry-dates.mjs +190 -0
- package/packages/evidence/retained-file.mjs +69 -0
- package/packages/evidence/review-age.mjs +14 -0
- package/packages/interchange/bagit.mjs +342 -0
- package/packages/interchange/bounded-zip-extraction.mjs +278 -0
- package/packages/interchange/crc32.mjs +17 -0
- package/packages/interchange/external-findings-import.mjs +92 -51
- package/packages/interchange/web-capture-import.mjs +382 -0
- package/packages/investigation/bulk-mail-exposure.mjs +2 -1
- package/packages/investigation/investigation-bagit.mjs +14 -0
- package/packages/investigation/investigation-capsule.mjs +33 -5
- package/packages/investigation/investigation-manifest.mjs +200 -0
- package/packages/investigation/investigation-package-crypto.mjs +98 -0
- package/packages/investigation/investigation-package.mjs +200 -0
- package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
- package/packages/investigation/lookup-asset-graph.mjs +193 -165
- package/packages/investigation/lookup-claim-readiness.mjs +18 -0
- package/packages/investigation/lookup-investigation-brief.mjs +3 -3
- package/packages/investigation/lookup-task-guidance.mjs +58 -0
- package/packages/monitoring/analyst-review-state.mjs +38 -90
- package/packages/workspace/brand-profile-model.mjs +276 -22
- package/packages/workspace/bulk-columns.mjs +17 -0
- package/packages/workspace/bulk-review-model.mjs +23 -11
- package/packages/workspace/bulk-session-model.mjs +100 -24
- package/packages/workspace/bulk-sort.mjs +1 -0
- package/packages/workspace/case-views.mjs +102 -0
- package/packages/workspace/detection-rule-model.mjs +70 -22
- package/packages/workspace/hostile-input.mjs +29 -8
- package/packages/workspace/investigation-template-model.mjs +19 -4
- package/packages/workspace/page-baseline.mjs +18 -15
- package/packages/workspace/relationship-observation-model.mjs +41 -23
- package/packages/workspace/watchlist-history.mjs +2 -2
- package/packages/workspace/watchlist-store.mjs +8 -2
- package/packages/workspace/website-snapshot-model.mjs +164 -26
- package/packages/workspace/workspace-archive-crypto.mjs +41 -61
- package/packages/workspace/workspace-archive.mjs +87 -21
- package/third-party-notices.txt +29 -2
- package/cli/evidence-command-arguments.mjs +0 -156
- package/packages/investigation/bulk-review-export.mjs +0 -63
|
@@ -1,35 +1,23 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
2
|
import { canonicalArtifactJson } from "../packages/evidence/artifact-integrity.mjs";
|
|
3
3
|
import { scanBoundedJson } from "../lib/bounded-json.mjs";
|
|
4
|
+
import { LOOKUP_SOURCE_STATES, lookupDiagnosticStates } from "../lib/lookup-diagnostics.mjs";
|
|
4
5
|
import { normalizeExplicitIsoTimestamp, normalizeLegacyIsoTimestamp, } from "../packages/evidence/observation.mjs";
|
|
5
6
|
import { CLI_LOOKUP_SCHEMA, MAX_CLI_LOOKUP_BYTES, } from "../packages/contracts/cli-lookup.mjs";
|
|
6
7
|
import { normalizeCliLookupDocument } from "./saved-lookup.mjs";
|
|
8
|
+
import { CLI_BULK_SCHEMA, CLI_BULK_ITEM_SCHEMA, CLI_BULK_SCHEMA_VERSION } from "./formatters/json.mjs";
|
|
9
|
+
import { LOOKUP_THREAT_INTELLIGENCE_PROVIDERS } from "../lib/lookup-threat-provider-inventory.mjs";
|
|
10
|
+
import { MAX_THREAT_INTELLIGENCE_PROVIDERS } from "../lib/lookup-response-contract.mjs";
|
|
11
|
+
import { THREAT_INTELLIGENCE_SCHEMA, THREAT_INTELLIGENCE_CONTRACT_VERSION } from "../lib/threat-intelligence-types.mjs";
|
|
7
12
|
export const SOURCE_RELIABILITY_REPORT_SCHEMA = 'whoisleuth.source-reliability-report';
|
|
8
13
|
export const SOURCE_RELIABILITY_REPORT_VERSION = 1;
|
|
9
14
|
export const MAX_SOURCE_RELIABILITY_INPUT_BYTES = 12 * 1024 * 1024;
|
|
10
15
|
export const MAX_SOURCE_RELIABILITY_DOCUMENTS = 100;
|
|
11
16
|
export const MAX_SOURCE_RELIABILITY_SOURCES = 64;
|
|
12
|
-
export const MAX_SOURCE_RELIABILITY_TRAVERSAL_NODES = 25_000;
|
|
13
17
|
const SOURCE_RE = /^[a-z][a-z0-9_-]{0,39}$/u;
|
|
14
|
-
const STATES = new Set(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
'not_found',
|
|
18
|
-
'skipped',
|
|
19
|
-
'error',
|
|
20
|
-
'unsupported',
|
|
21
|
-
'not_applicable',
|
|
22
|
-
'unavailable',
|
|
23
|
-
'rate_limited',
|
|
24
|
-
'complete',
|
|
25
|
-
'disabled',
|
|
26
|
-
'stale',
|
|
27
|
-
]);
|
|
28
|
-
const LOOKUP_SCHEMAS = new Set([
|
|
29
|
-
CLI_LOOKUP_SCHEMA,
|
|
30
|
-
'whoisleuth.cli.bulk',
|
|
31
|
-
'whoisleuth.cli.bulk.item',
|
|
32
|
-
]);
|
|
18
|
+
const STATES = new Set(LOOKUP_SOURCE_STATES);
|
|
19
|
+
const BULK_HEALTH_VERSIONS = new Set([1, 2, CLI_BULK_SCHEMA_VERSION]);
|
|
20
|
+
const PROVIDER_IDS = new Set(LOOKUP_THREAT_INTELLIGENCE_PROVIDERS.map((provider) => provider.id));
|
|
33
21
|
const REPORT_KEYS = new Set([
|
|
34
22
|
'schema',
|
|
35
23
|
'version',
|
|
@@ -175,9 +163,9 @@ function parseLookupDocuments(values) {
|
|
|
175
163
|
throw new TypeError('Source reliability input requires a supported bounded CLI Lookup document.');
|
|
176
164
|
}
|
|
177
165
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
throw new TypeError('Source reliability input requires CLI Lookup
|
|
166
|
+
if ((schema !== CLI_BULK_SCHEMA && schema !== CLI_BULK_ITEM_SCHEMA)
|
|
167
|
+
|| typeof document.version !== 'number' || !BULK_HEALTH_VERSIONS.has(document.version)) {
|
|
168
|
+
throw new TypeError('Source reliability input requires supported CLI Lookup, Bulk or Bulk-item documents, or only source reliability reports.');
|
|
181
169
|
}
|
|
182
170
|
return document;
|
|
183
171
|
});
|
|
@@ -232,35 +220,21 @@ function collectTiming(document, store) {
|
|
|
232
220
|
}
|
|
233
221
|
}
|
|
234
222
|
function collectDiagnostics(document, store) {
|
|
235
|
-
const
|
|
236
|
-
if (!diagnostics)
|
|
237
|
-
return;
|
|
238
|
-
const fixed = [
|
|
239
|
-
['rdap', record(diagnostics.rdap)?.status],
|
|
240
|
-
['registrar_rdap', record(record(diagnostics.rdap)?.registrar)?.status],
|
|
241
|
-
['whois', record(diagnostics.whois)?.status],
|
|
242
|
-
['availability', record(diagnostics.availability)?.status],
|
|
243
|
-
['reverse_dns', record(diagnostics.reverseDns)?.status],
|
|
244
|
-
['network_context', record(diagnostics.network)?.status],
|
|
245
|
-
['security_txt', record(diagnostics.securityTxt)?.status],
|
|
246
|
-
['sslbl', record(diagnostics.sslbl)?.status],
|
|
247
|
-
];
|
|
248
|
-
for (const [source, value] of fixed) {
|
|
249
|
-
const state = safeState(value);
|
|
223
|
+
for (const [source, state] of Object.entries(lookupDiagnosticStates(document.diagnostics))) {
|
|
250
224
|
if (state)
|
|
251
225
|
addState(store, source, state);
|
|
252
226
|
}
|
|
253
227
|
}
|
|
254
|
-
function collectObservation(
|
|
255
|
-
|
|
228
|
+
function collectObservation(raw, source, store) {
|
|
229
|
+
const value = record(raw);
|
|
230
|
+
if (!value || value.version !== 1 || value.source !== source
|
|
256
231
|
|| typeof value.observedAt !== 'string'
|
|
257
232
|
|| typeof value.complete !== 'boolean'
|
|
258
233
|
|| typeof value.truncated !== 'boolean'
|
|
259
234
|
|| !Array.isArray(value.limitations))
|
|
260
235
|
return;
|
|
261
|
-
const source = safeSource(value.source);
|
|
262
236
|
const state = safeState(value.status);
|
|
263
|
-
if (!
|
|
237
|
+
if (!state)
|
|
264
238
|
return;
|
|
265
239
|
const item = accumulator(store, source);
|
|
266
240
|
item.observationSamples += 1;
|
|
@@ -271,48 +245,65 @@ function collectObservation(value, store) {
|
|
|
271
245
|
if (value.truncated === true)
|
|
272
246
|
item.truncated += 1;
|
|
273
247
|
}
|
|
274
|
-
function
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
248
|
+
function collectLookupHealth(document, store) {
|
|
249
|
+
collectDiagnostics(document, store);
|
|
250
|
+
collectTiming(document, store);
|
|
251
|
+
const availability = record(document.availability);
|
|
252
|
+
const dns = record(availability?.dns);
|
|
253
|
+
const technology = record(availability?.technologyProfile);
|
|
254
|
+
// Only collector-owned envelopes have health semantics. Raw publications,
|
|
255
|
+
// parsed registration extensions and nested provider findings remain opaque.
|
|
256
|
+
const observations = [
|
|
257
|
+
['dns', dns], ['dns', dns?.caaPolicy], ['dns_delegation', dns?.delegation],
|
|
258
|
+
['http', availability?.http], ['tls', availability?.tls],
|
|
259
|
+
['html', availability?.pageIdentity],
|
|
260
|
+
];
|
|
261
|
+
const fullLookup = document.schema === CLI_LOOKUP_SCHEMA;
|
|
262
|
+
if (fullLookup)
|
|
263
|
+
observations.push(['html', availability?.credentialSurfaceProfile], ['html', availability?.structuredDataIdentity], ['derived', technology], ['derived', technology?.browserLibraryProfile], ['derived', availability?.pageRoleProfile], ['derived', availability?.clientBehaviorProfile], ['derived', availability?.securityPosture], ['reverse_dns', document.reverseDns], ['ip_rdap', document.networkContext], ['security_txt', document.securityTxt], ['sslbl', document.sslbl]);
|
|
264
|
+
for (const [source, observation] of observations)
|
|
265
|
+
collectObservation(observation, source, store);
|
|
266
|
+
if (!fullLookup)
|
|
267
|
+
return;
|
|
268
|
+
const providers = record(document.threatIntelligence)?.providers;
|
|
269
|
+
if (providers === undefined)
|
|
270
|
+
return;
|
|
271
|
+
if (!Array.isArray(providers) || providers.length > MAX_THREAT_INTELLIGENCE_PROVIDERS) {
|
|
272
|
+
throw new TypeError(`Source reliability input exceeds the ${MAX_THREAT_INTELLIGENCE_PROVIDERS}-provider health boundary.`);
|
|
273
|
+
}
|
|
274
|
+
const seen = new Set();
|
|
275
|
+
for (const rawProvider of providers) {
|
|
276
|
+
const provider = record(rawProvider);
|
|
277
|
+
const source = record(provider?.provider)?.id;
|
|
278
|
+
if (provider?.schema !== THREAT_INTELLIGENCE_SCHEMA || provider.version !== THREAT_INTELLIGENCE_CONTRACT_VERSION
|
|
279
|
+
|| typeof source !== 'string' || !PROVIDER_IDS.has(source)
|
|
280
|
+
|| record(provider.observation)?.source !== source)
|
|
280
281
|
continue;
|
|
281
|
-
if (
|
|
282
|
-
throw new TypeError('Source reliability input
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if (child && typeof child === 'object')
|
|
304
|
-
stack.push({ value: child, depth: current.depth + 1 });
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
else if (Array.isArray(current.value)) {
|
|
308
|
-
if (current.value.length > 1_000) {
|
|
309
|
-
throw new TypeError('Source reliability input contains an array with more than 1000 items.');
|
|
310
|
-
}
|
|
311
|
-
for (const child of current.value) {
|
|
312
|
-
if (child && typeof child === 'object')
|
|
313
|
-
stack.push({ value: child, depth: current.depth + 1 });
|
|
314
|
-
}
|
|
282
|
+
if (seen.has(source))
|
|
283
|
+
throw new TypeError('Source reliability input contains duplicate provider health identities.');
|
|
284
|
+
seen.add(source);
|
|
285
|
+
collectObservation(provider?.observation, source, store);
|
|
286
|
+
if (provider?.state === 'rate_limited')
|
|
287
|
+
addState(store, source, 'rate_limited');
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function collectDocumentHealth(document, store) {
|
|
291
|
+
if (document.schema === CLI_LOOKUP_SCHEMA) {
|
|
292
|
+
collectLookupHealth(document, store);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const items = document.schema === CLI_BULK_SCHEMA ? document.results : [document];
|
|
296
|
+
if (!Array.isArray(items) || items.length > 1_000) {
|
|
297
|
+
throw new TypeError('Source reliability Bulk input requires at most 1000 result items.');
|
|
298
|
+
}
|
|
299
|
+
for (const rawItem of items) {
|
|
300
|
+
const item = record(rawItem);
|
|
301
|
+
if (!item || item.schema !== CLI_BULK_ITEM_SCHEMA || item.version !== document.version
|
|
302
|
+
|| typeof item.ok !== 'boolean') {
|
|
303
|
+
throw new TypeError('Source reliability Bulk input contains an invalid result envelope.');
|
|
315
304
|
}
|
|
305
|
+
if (item.ok)
|
|
306
|
+
collectLookupHealth(item, store);
|
|
316
307
|
}
|
|
317
308
|
}
|
|
318
309
|
function percentile(sorted, ratio) {
|
|
@@ -663,7 +654,7 @@ export function buildSourceReliabilityReport(raw, generatedAt = new Date().toISO
|
|
|
663
654
|
const sampleDate = lookupDocumentTimestamp(document);
|
|
664
655
|
if (sampleDate)
|
|
665
656
|
sampleDates.push(sampleDate);
|
|
666
|
-
|
|
657
|
+
collectDocumentHealth(document, store);
|
|
667
658
|
}
|
|
668
659
|
}
|
|
669
660
|
sampleDates.sort();
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
2
|
+
import { CliUsageError, hasUnsafeCliText } from "./errors.mjs";
|
|
3
|
+
export function canReadInteractiveLine(input, output, environment = process.env) {
|
|
4
|
+
return input?.isTTY === true && output?.isTTY === true
|
|
5
|
+
&& typeof input.setRawMode === 'function' && typeof input.resume === 'function'
|
|
6
|
+
&& typeof input.pause === 'function' && typeof input.on === 'function'
|
|
7
|
+
&& typeof input.off === 'function' && typeof output.write === 'function'
|
|
8
|
+
&& !environment.CI;
|
|
9
|
+
}
|
|
10
|
+
const MAX_INTERACTIVE_ANSWER_SCALARS = 1_024;
|
|
11
|
+
const MAX_INTERACTIVE_ANSWER_BYTES = 4_096;
|
|
12
|
+
function boundedInteractiveAnswer(value) {
|
|
13
|
+
const supplied = typeof value === 'string' ? value : '';
|
|
14
|
+
if (Buffer.byteLength(supplied, 'utf8') > MAX_INTERACTIVE_ANSWER_BYTES
|
|
15
|
+
|| Array.from(supplied).length > MAX_INTERACTIVE_ANSWER_SCALARS
|
|
16
|
+
|| hasUnsafeCliText(supplied)) {
|
|
17
|
+
throw new CliUsageError('Interactive input must be bounded text without control characters.');
|
|
18
|
+
}
|
|
19
|
+
return supplied.trim();
|
|
20
|
+
}
|
|
21
|
+
function abortError() {
|
|
22
|
+
return new DOMException('Aborted', 'AbortError');
|
|
23
|
+
}
|
|
24
|
+
function readBoundedInteractiveLine(prompt, options) {
|
|
25
|
+
const { input, output, signal } = options;
|
|
26
|
+
if (signal?.aborted)
|
|
27
|
+
return Promise.reject(signal.reason || abortError());
|
|
28
|
+
const previousRaw = input.isRaw === true;
|
|
29
|
+
const wasPaused = input.isPaused?.() === true;
|
|
30
|
+
let receivedBytes = 0;
|
|
31
|
+
let receivedScalars = 0;
|
|
32
|
+
let settled = false;
|
|
33
|
+
let characters = [];
|
|
34
|
+
const decoder = new StringDecoder('utf8');
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const cleanup = () => {
|
|
37
|
+
let failure = null;
|
|
38
|
+
const attempt = (operation) => {
|
|
39
|
+
try {
|
|
40
|
+
operation();
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
failure ||= error instanceof Error ? error : new Error('Interactive terminal cleanup failed.');
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
attempt(() => input.off?.('data', onData));
|
|
47
|
+
attempt(() => input.off?.('end', onEnd));
|
|
48
|
+
attempt(() => signal?.removeEventListener('abort', onAbort));
|
|
49
|
+
attempt(() => input.setRawMode?.(previousRaw));
|
|
50
|
+
if (wasPaused)
|
|
51
|
+
attempt(() => input.pause?.());
|
|
52
|
+
return failure;
|
|
53
|
+
};
|
|
54
|
+
const finish = (value, error) => {
|
|
55
|
+
if (settled)
|
|
56
|
+
return;
|
|
57
|
+
settled = true;
|
|
58
|
+
const cleanupFailure = cleanup();
|
|
59
|
+
characters = [];
|
|
60
|
+
if (error && cleanupFailure) {
|
|
61
|
+
reject(new AggregateError([error, cleanupFailure], `${error.message}; terminal cleanup also failed: ${cleanupFailure.message}`));
|
|
62
|
+
}
|
|
63
|
+
else if (error || cleanupFailure)
|
|
64
|
+
reject(error || cleanupFailure);
|
|
65
|
+
else
|
|
66
|
+
resolve(value || '');
|
|
67
|
+
};
|
|
68
|
+
const rejectUsage = () => finish(undefined, new CliUsageError('Interactive input must be bounded text without control characters.'));
|
|
69
|
+
const onData = (chunk) => {
|
|
70
|
+
if (settled)
|
|
71
|
+
return;
|
|
72
|
+
let rawChunk;
|
|
73
|
+
if (Buffer.isBuffer(chunk)) {
|
|
74
|
+
if (chunk.length > MAX_INTERACTIVE_ANSWER_BYTES - receivedBytes) {
|
|
75
|
+
rejectUsage();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
rawChunk = chunk;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
const supplied = String(chunk ?? '');
|
|
82
|
+
if (supplied.length > MAX_INTERACTIVE_ANSWER_BYTES - receivedBytes
|
|
83
|
+
|| Buffer.byteLength(supplied, 'utf8') > MAX_INTERACTIVE_ANSWER_BYTES - receivedBytes) {
|
|
84
|
+
rejectUsage();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
rawChunk = Buffer.from(supplied, 'utf8');
|
|
88
|
+
}
|
|
89
|
+
receivedBytes += rawChunk.length;
|
|
90
|
+
const value = decoder.write(rawChunk);
|
|
91
|
+
receivedScalars += Array.from(value).length;
|
|
92
|
+
if (receivedBytes > MAX_INTERACTIVE_ANSWER_BYTES
|
|
93
|
+
|| receivedScalars > MAX_INTERACTIVE_ANSWER_SCALARS
|
|
94
|
+
|| value.includes('\ufffd')) {
|
|
95
|
+
rejectUsage();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
for (const character of value) {
|
|
99
|
+
if (character === '\r' || character === '\n') {
|
|
100
|
+
try {
|
|
101
|
+
output.write('\n');
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
finish(undefined, error instanceof Error ? error : new Error('Interactive terminal output failed.'));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
finish(characters.join(''));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (character === '\u0003' || character === '\u0004' || character === '\u001b') {
|
|
111
|
+
finish(undefined, abortError());
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (character === '\u0008' || character === '\u007f') {
|
|
115
|
+
if (characters.length) {
|
|
116
|
+
characters.pop();
|
|
117
|
+
try {
|
|
118
|
+
output.write('\b \b');
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
finish(undefined, error instanceof Error ? error : new Error('Interactive terminal output failed.'));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (hasUnsafeCliText(character)) {
|
|
128
|
+
rejectUsage();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
characters.push(character);
|
|
132
|
+
try {
|
|
133
|
+
output.write(character);
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
finish(undefined, error instanceof Error ? error : new Error('Interactive terminal output failed.'));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
const onEnd = () => {
|
|
142
|
+
const remainder = decoder.end();
|
|
143
|
+
if (remainder)
|
|
144
|
+
rejectUsage();
|
|
145
|
+
else
|
|
146
|
+
finish(undefined, abortError());
|
|
147
|
+
};
|
|
148
|
+
const onAbort = () => finish(undefined, signal?.reason instanceof Error ? signal.reason : abortError());
|
|
149
|
+
try {
|
|
150
|
+
output.write(prompt);
|
|
151
|
+
input.on?.('data', onData);
|
|
152
|
+
input.on?.('end', onEnd);
|
|
153
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
154
|
+
if (signal?.aborted) {
|
|
155
|
+
onAbort();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
input.setRawMode?.(true);
|
|
159
|
+
if (settled)
|
|
160
|
+
return;
|
|
161
|
+
input.resume?.();
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
finish(undefined, error instanceof Error ? error : new Error('Interactive terminal input failed.'));
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
export { MAX_INTERACTIVE_ANSWER_BYTES, MAX_INTERACTIVE_ANSWER_SCALARS, boundedInteractiveAnswer, readBoundedInteractiveLine };
|
|
@@ -1,15 +1,39 @@
|
|
|
1
1
|
import { formatCliJunit } from "./ci-report.mjs";
|
|
2
2
|
import { formatDomainControlMonitor, runDomainControlMonitor } from "./domain-control-monitor.mjs";
|
|
3
|
-
import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
|
|
3
|
+
import { boundedCliErrorMessage, createCliDiagnosticOutput, CliUsageError } from "./errors.mjs";
|
|
4
4
|
import EXIT_CODES from "./exit-codes.mjs";
|
|
5
5
|
import { evaluateCliFailPolicies, formatFailPolicyNotice } from "./fail-policy.mjs";
|
|
6
6
|
import { formatJsonDocument } from "./formatters/json.mjs";
|
|
7
7
|
import { buildInvestigationPlan, buildWorkflowRecipeCatalogue, formatInvestigationPlan, formatWorkflowRecipeCatalogue, } from "./investigation-plan.mjs";
|
|
8
|
-
import { MAX_INVESTIGATION_RUN_BYTES, formatInvestigationRun, runInvestigationRecipe, } from "./investigation-run.mjs";
|
|
8
|
+
import { MAX_INVESTIGATION_RUN_BYTES, formatInvestigationRun, investigationRunExitCode, runInvestigationRecipe, } from "./investigation-run.mjs";
|
|
9
9
|
import { MAX_OFFLINE_EVIDENCE_INPUT_BYTES } from "./offline-evidence-review.mjs";
|
|
10
|
+
import { MAX_OFFLINE_ARTIFACT_BYTES } from "./artifact-verify.mjs";
|
|
11
|
+
import { MAX_RETAINED_ARTIFACT_DIFF_BYTES } from "./retained-artifact-diff.mjs";
|
|
12
|
+
import { MAX_SAVED_LOOKUP_INPUT_BYTES } from "./saved-lookup.mjs";
|
|
13
|
+
import { MAX_COMPARE_INPUT_BYTES } from "./compare.mjs";
|
|
14
|
+
import { MAX_SOURCE_RELIABILITY_INPUT_BYTES } from "./source-reliability.mjs";
|
|
15
|
+
import { MAX_SHARING_REVIEW_BYTES } from "./sharing-review.mjs";
|
|
10
16
|
import { createBufferedOutput } from "./output-file.mjs";
|
|
17
|
+
import { boundedInteractiveAnswer, canReadInteractiveLine, readBoundedInteractiveLine } from "./terminal-input.mjs";
|
|
11
18
|
import { WORKFLOW_INLINE_COMMANDS } from "./inline-command-families.mjs";
|
|
12
19
|
import { runDiscriminatedCommandHandler } from "./discriminated-command-handlers.mjs";
|
|
20
|
+
function boundWorkflowInputs(command, inputs, dependencies, context) {
|
|
21
|
+
if (!inputs.size)
|
|
22
|
+
return {};
|
|
23
|
+
const read = (source, fallback, maximumBytes, label) => typeof source === 'string' && inputs.has(source)
|
|
24
|
+
? inputs.get(source)
|
|
25
|
+
: fallback ? fallback(source) : context.readInput(source, maximumBytes, label);
|
|
26
|
+
switch (command) {
|
|
27
|
+
case 'export': return { readExportInput: (source) => read(source, dependencies.readExportInput, MAX_SAVED_LOOKUP_INPUT_BYTES, 'Evidence export input') };
|
|
28
|
+
case 'verify-artifact': return { readArtifactInput: (source) => read(source, dependencies.readArtifactInput, MAX_OFFLINE_ARTIFACT_BYTES, 'Artefact input') };
|
|
29
|
+
case 'sharing-review': return { readArtifactInput: (source) => read(source, dependencies.readArtifactInput, MAX_SHARING_REVIEW_BYTES, 'Sharing review input') };
|
|
30
|
+
case 'compare': return { readCompareInput: (source) => read(source, dependencies.readCompareInput, MAX_COMPARE_INPUT_BYTES, 'Comparison input') };
|
|
31
|
+
case 'source-report': return { readSourceReliabilityInput: (source) => read(source, dependencies.readSourceReliabilityInput, MAX_SOURCE_RELIABILITY_INPUT_BYTES, 'Source reliability input') };
|
|
32
|
+
case 'diff':
|
|
33
|
+
case 'timeline': return { readDiffInput: (source) => read(source, dependencies.readDiffInput, command === 'diff' ? MAX_RETAINED_ARTIFACT_DIFF_BYTES : MAX_SAVED_LOOKUP_INPUT_BYTES, command === 'diff' ? 'Retained diff input' : 'Lookup timeline input') };
|
|
34
|
+
default: throw new CliUsageError('This fixed-workflow command does not accept retained artefacts.');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
13
37
|
async function runMonitorOnceCommand(args, dependencies, context) {
|
|
14
38
|
context.setFailureLabel('One-shot domain control review');
|
|
15
39
|
let manifestInput;
|
|
@@ -82,12 +106,18 @@ async function runWorkflowPlanCommand(args, dependencies, context) {
|
|
|
82
106
|
}
|
|
83
107
|
async function runWorkflowRecipeCommand(args, dependencies, context) {
|
|
84
108
|
context.setFailureLabel('Investigation workflow');
|
|
109
|
+
const input = (dependencies.stdin ?? process.stdin);
|
|
110
|
+
if (args.interactive && !canReadInteractiveLine(input, context.stderr, dependencies.environment)) {
|
|
111
|
+
throw new CliUsageError('--interactive requires terminal input and terminal stderr. Use --select for unattended workflows.');
|
|
112
|
+
}
|
|
85
113
|
let resumeInput = null;
|
|
86
114
|
if (args.resumeSource) {
|
|
87
115
|
try {
|
|
88
|
-
resumeInput = dependencies.
|
|
89
|
-
?
|
|
90
|
-
:
|
|
116
|
+
resumeInput = dependencies.workflowResumeInput !== undefined
|
|
117
|
+
? dependencies.workflowResumeInput
|
|
118
|
+
: dependencies.readDiffInput
|
|
119
|
+
? await dependencies.readDiffInput(args.resumeSource)
|
|
120
|
+
: await context.readInput(args.resumeSource, MAX_INVESTIGATION_RUN_BYTES, 'Investigation resume state');
|
|
91
121
|
}
|
|
92
122
|
catch (error) {
|
|
93
123
|
if (error instanceof CliUsageError)
|
|
@@ -98,17 +128,42 @@ async function runWorkflowRecipeCommand(args, dependencies, context) {
|
|
|
98
128
|
const document = await runInvestigationRecipe(args.recipe, args.subject, {
|
|
99
129
|
approveNetwork: args.approveNetwork,
|
|
100
130
|
selections: args.selections,
|
|
131
|
+
artifactBindings: args.artifactBindings,
|
|
132
|
+
confirmedReviews: args.confirmedReviews,
|
|
133
|
+
...(args.interactive ? { selectInputs: async (request) => {
|
|
134
|
+
context.writeStderr(`${request.label}\nEnter literal input paths or values. Leave blank to pause; no approval is implied.\n`);
|
|
135
|
+
const answers = [];
|
|
136
|
+
for (const placeholder of request.inputs) {
|
|
137
|
+
const prompt = `${request.stepId} ${placeholder}: `;
|
|
138
|
+
const answer = boundedInteractiveAnswer(await (dependencies.workflowQuestion
|
|
139
|
+
? dependencies.workflowQuestion(prompt)
|
|
140
|
+
: readBoundedInteractiveLine(prompt, { input: input, output: context.stderr,
|
|
141
|
+
...(dependencies.signal ? { signal: dependencies.signal } : {}) })));
|
|
142
|
+
if (!answer)
|
|
143
|
+
break;
|
|
144
|
+
answers.push(answer);
|
|
145
|
+
}
|
|
146
|
+
return answers;
|
|
147
|
+
} } : {}),
|
|
101
148
|
resumeInput,
|
|
102
149
|
generatedAt: context.now(),
|
|
103
150
|
...(dependencies.signal ? { signal: dependencies.signal } : {}),
|
|
104
|
-
execute: async (command, stepArguments) => {
|
|
151
|
+
execute: async (command, stepArguments, inputs) => {
|
|
105
152
|
const stepStdout = createBufferedOutput();
|
|
106
|
-
const stepStderr =
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
153
|
+
const stepStderr = createCliDiagnosticOutput();
|
|
154
|
+
try {
|
|
155
|
+
const exitCode = await context.executeCli([command, ...stepArguments], {
|
|
156
|
+
stdout: stepStdout.stream,
|
|
157
|
+
stderr: stepStderr.stream,
|
|
158
|
+
...boundWorkflowInputs(command, inputs, dependencies, context),
|
|
159
|
+
});
|
|
160
|
+
return { exitCode, stdout: stepStdout.value() };
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
const diagnostic = stepStderr.value();
|
|
164
|
+
if (diagnostic)
|
|
165
|
+
context.writeStderr(`${command}: ${diagnostic}\n`);
|
|
166
|
+
}
|
|
112
167
|
},
|
|
113
168
|
});
|
|
114
169
|
if (!args.quiet) {
|
|
@@ -116,7 +171,7 @@ async function runWorkflowRecipeCommand(args, dependencies, context) {
|
|
|
116
171
|
? formatJsonDocument(document)
|
|
117
172
|
: context.terminal(formatInvestigationRun(document), args.color));
|
|
118
173
|
}
|
|
119
|
-
return document
|
|
174
|
+
return investigationRunExitCode(document);
|
|
120
175
|
}
|
|
121
176
|
const WORKFLOW_COMMAND_HANDLERS = Object.freeze({
|
|
122
177
|
'monitor-once': runMonitorOnceCommand,
|