@slicedearth/whoisleuth-cli 2.3.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 +8 -2
- package/README.md +19 -15
- package/bin/whoisleuth.mjs +8 -1
- package/cli/archive-content-digest.mjs +14 -0
- package/cli/archive-inspect.mjs +20 -53
- package/cli/argument-values.mjs +55 -0
- package/cli/arguments.mjs +24 -408
- package/cli/artifact-validation/case-response.mjs +65 -44
- package/cli/artifact-validation/investigation-capsule.mjs +52 -31
- package/cli/artifact-validation/lookup-evidence.mjs +15 -9
- package/cli/artifact-validation/signed-review.mjs +136 -68
- package/cli/artifact-validation/structure-primitives.mjs +1 -105
- package/cli/artifact-verify.mjs +92 -24
- package/cli/assurance-arguments.mjs +56 -0
- package/cli/assurance-command-definitions.mjs +179 -0
- package/cli/assurance-command-runner.mjs +40 -5
- 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 +1 -1
- package/cli/bulk-command-runner.mjs +4 -2
- package/cli/bulk-output.mjs +29 -3
- package/cli/bulk.mjs +2 -3
- package/cli/case-command.mjs +212 -0
- package/cli/case-pack.mjs +50 -32
- 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 -206
- package/cli/command-definition.mjs +329 -0
- package/cli/command-reference.mjs +94 -1357
- package/cli/command-seeds.mjs +18 -0
- package/cli/compare.mjs +1 -1
- package/cli/completion.mjs +298 -79
- package/cli/config-profile.mjs +8 -6
- package/cli/ct-event-intake.mjs +16 -16
- package/cli/discover.mjs +5 -4
- package/cli/discovery-observation-snapshot.mjs +46 -18
- package/cli/discovery-scan-command-runner.mjs +5 -3
- package/cli/discovery-scan.mjs +7 -4
- package/cli/discovery-snapshot.mjs +2 -2
- package/cli/discovery-workflow.mjs +5 -5
- package/cli/domain-control-monitor.mjs +50 -20
- package/cli/domain-control-observations.mjs +151 -103
- package/cli/errors.mjs +37 -6
- package/cli/evidence-arguments.mjs +46 -0
- package/cli/evidence-command-definitions.mjs +69 -0
- package/cli/evidence-command-runner.mjs +18 -8
- package/cli/evidence-signing.mjs +5 -4
- package/cli/external-observation-mapping.mjs +9 -10
- package/cli/formatters/terminal-command-formats.mjs +6 -0
- package/cli/formatters/terminal-lookup.mjs +24 -46
- package/cli/formatters/terminal-shared.mjs +5 -3
- package/cli/formatters/terminal-technology.mjs +114 -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 -46
- package/cli/inline-command-runner.mjs +4 -31
- package/cli/input.mjs +2 -2
- package/cli/intake-command.mjs +107 -0
- package/cli/interactive-launcher.mjs +2 -160
- package/cli/interchange-report.mjs +8 -10
- 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 +38 -147
- package/cli/investigation-recipes.mjs +177 -0
- package/cli/investigation-run.mjs +256 -35
- package/cli/local-document-checkpoint.mjs +170 -0
- 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-browser.mjs +5 -12
- package/cli/lookup-command-runner.mjs +33 -39
- package/cli/lookup-diff.mjs +47 -12
- package/cli/lookup-plan.mjs +16 -14
- package/cli/lookup-reconcile.mjs +15 -12
- package/cli/lookup-timeline.mjs +7 -6
- package/cli/mail-header-review.mjs +24 -247
- package/cli/mail-review.mjs +75 -21
- package/cli/manual.mjs +1 -0
- package/cli/network-arguments.mjs +51 -0
- package/cli/network-command-definitions.mjs +130 -0
- package/cli/network-command-runner.mjs +45 -31
- package/cli/offline-evidence-review.mjs +39 -6
- package/cli/output-file.mjs +31 -5
- package/cli/posture.mjs +1 -1
- package/cli/registry-doctor.mjs +3 -1
- package/cli/registry-fixture-scaffold.mjs +1 -1
- package/cli/registry-support.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +10 -9
- package/cli/review-arguments.mjs +131 -0
- package/cli/review-command-definitions.mjs +273 -0
- package/cli/review-command-runner.mjs +104 -149
- package/cli/risk-calibration.mjs +8 -8
- package/cli/runner.mjs +74 -37
- package/cli/saved-lookup.mjs +2 -2
- package/cli/selected-input-worker.mjs +63 -0
- package/cli/sharing-review.mjs +8 -80
- package/cli/signer-trust.mjs +104 -0
- package/cli/source-reliability.mjs +75 -84
- 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/terminal-input.mjs +168 -0
- package/cli/workflow-arguments.mjs +86 -0
- package/cli/workflow-command-definitions.mjs +78 -0
- package/cli/workflow-command-runner.mjs +75 -13
- package/docs/cli-reference.md +103 -30
- package/docs/cli.md +510 -25
- package/docs/contextual-reviews.md +155 -0
- package/lib/abort.mjs +4 -1
- package/lib/analyst-taxonomy.mjs +2 -40
- package/lib/application-version.mjs +1 -1
- package/lib/availability.mjs +27 -754
- package/lib/bounded-contract-normalizers.mjs +4 -0
- package/lib/bounded-file.mjs +3 -1
- package/lib/bounded-html-document.mjs +125 -0
- package/lib/bounded-json.mjs +2 -290
- package/lib/browser-library-profile.mjs +99 -130
- package/lib/browser-library-worker.mjs +112 -0
- package/lib/candidate-provenance-bounds.mjs +2 -0
- package/lib/classify.mjs +1 -1
- package/lib/client-behavior-profile.mjs +3 -3
- package/lib/ct-query.mjs +2 -27
- package/lib/ct-response-bounds.mjs +2 -9
- package/lib/ct-search.mjs +89 -46
- package/lib/dmarc-discovery.mjs +62 -0
- package/lib/dns-convergence-review.mjs +21 -8
- package/lib/dns-delegation-health.mjs +36 -22
- package/lib/dns-inheritance-review.mjs +207 -0
- package/lib/dns-intelligence.mjs +44 -29
- package/lib/dns-mx.mjs +1 -1
- package/lib/dns-query-coverage.mjs +40 -0
- package/lib/dnssec-evidence-validation.mjs +11 -3
- package/lib/domain-assurance.mjs +1 -1
- package/lib/domain-change-packet.mjs +18 -14
- package/lib/domain-change-review.mjs +40 -16
- package/lib/domain-control-flight-recorder.mjs +301 -124
- package/lib/domain-control-manifest.mjs +126 -58
- package/lib/domain-posture-analysis.mjs +41 -11
- package/lib/domain-posture-parsers.mjs +88 -6
- package/lib/domain-posture.mjs +120 -32
- package/lib/domain-registration.mjs +392 -0
- package/lib/evidence-export-privacy.mjs +568 -0
- package/lib/evidence-export.mjs +12 -593
- package/lib/evidence-report.mjs +7 -2
- package/lib/external-intelligence-risk.mjs +2 -124
- package/lib/favicon.mjs +117 -41
- package/lib/generated/cisa-kev-catalog.mjs +50 -4
- package/lib/generated/registrar-standing-catalogue.mjs +18 -10
- package/lib/generated/retire-browser-catalog.mjs +2742 -1034
- package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
- package/lib/html-signals.mjs +87 -85
- package/lib/http-evidence-bounds.mjs +2 -7
- package/lib/http-intelligence.mjs +52 -27
- 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-cache.mjs +34 -11
- package/lib/lookup-child-profile-contract.mjs +24 -15
- package/lib/lookup-contract-primitives.mjs +1 -1
- package/lib/lookup-diagnostics.mjs +2 -1
- package/lib/lookup-network-evidence-bounds.mjs +1 -10
- package/lib/lookup-progress.mjs +79 -13
- package/lib/lookup-response-contract.mjs +1077 -0
- package/lib/lookup-response.mjs +11 -24
- package/lib/lookup-source-progress.mjs +24 -35
- package/lib/lookup.mjs +49 -28
- package/lib/observed-network-context.mjs +43 -27
- package/lib/opportunity-scoring.mjs +2 -107
- package/lib/outbound-identity.mjs +1 -1
- package/lib/outbound-request-bounds.mjs +4 -1
- package/lib/page-fingerprints.mjs +87 -134
- package/lib/page-role-profile.mjs +12 -8
- package/lib/perceptual-hash-comparison.mjs +2 -34
- package/lib/perceptual-hash.mjs +79 -24
- package/lib/portable-generator.mjs +2 -29
- package/lib/project-metadata.mjs +2 -8
- package/lib/rdap-bootstrap.mjs +72 -22
- package/lib/rdap-client.mjs +26 -14
- 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 +2 -36
- package/lib/registrar-standing-contract.mjs +2 -1
- package/lib/registrar-standing.mjs +9 -13
- package/lib/registry-capabilities.mjs +7 -3
- package/lib/registry-capability-catalogue.mjs +29 -20
- package/lib/registry-dates.mjs +2 -190
- package/lib/registry-insights.mjs +56 -18
- package/lib/response-policy.mjs +86 -48
- package/lib/risk-calibration-summary.mjs +2 -338
- package/lib/risk-scoring.mjs +2 -345
- package/lib/rpki-evidence.mjs +8 -5
- package/lib/safe-fetch.mjs +54 -47
- package/lib/scoring-evidence-quality.mjs +2 -107
- package/lib/security-txt.mjs +10 -7
- package/lib/semantic-version.mjs +2 -54
- package/lib/service-binding-dns.mjs +35 -0
- package/lib/smtp-transport-review.mjs +75 -34
- package/lib/sslbl-certificates.generated.mjs +141 -138
- package/lib/static-html-analysis.mjs +117 -146
- package/lib/structured-data-identity.mjs +2 -1
- 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 +2 -36
- package/lib/typosquat-generator.mjs +29 -16
- package/lib/urlhaus-intelligence.mjs +8 -3
- package/lib/urlscan-intelligence.mjs +1 -1
- package/lib/website-enrichment.mjs +388 -0
- package/lib/website-security-posture.mjs +30 -6
- package/lib/website-technology.mjs +111 -71
- package/lib/whois-authority.mjs +55 -5
- 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-normalization.mjs +3 -1
- package/lib/whois-parser-support.mjs +8 -1
- package/lib/whois-parser.mjs +5 -3
- package/lib/whois-transport.mjs +23 -7
- package/package.json +14 -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-attachment-model.mjs +83 -0
- package/packages/cases/case-evidence-links.mjs +135 -0
- package/packages/cases/case-evidence-model.mjs +229 -69
- package/packages/cases/case-evidence-quality.mjs +5 -0
- package/packages/cases/case-export-input.mjs +55 -0
- package/packages/cases/case-incident-context.mjs +52 -0
- package/packages/cases/case-markdown.mjs +11 -0
- package/packages/cases/case-migration-model.mjs +98 -48
- package/packages/cases/case-model.mjs +1 -0
- package/packages/cases/case-recheck-model.mjs +149 -0
- package/packages/cases/case-record-contracts.mjs +1 -1
- package/packages/cases/case-record-core.mjs +4 -3
- package/packages/cases/case-record-operations.mjs +123 -133
- package/packages/cases/case-record-projection.mjs +30 -20
- package/packages/cases/case-record-version-input.mjs +19 -0
- package/packages/cases/case-report.mjs +114 -67
- package/packages/cases/case-requested-evidence.mjs +146 -0
- package/packages/cases/case-response-actions.mjs +756 -0
- package/packages/cases/case-response-model.mjs +49 -1129
- package/packages/cases/case-response-outcomes.mjs +338 -0
- package/packages/cases/case-response-packet-render.mjs +157 -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 +58 -593
- package/packages/cases/case-response-records.mjs +93 -0
- package/packages/cases/case-response-review-inputs.mjs +382 -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 +26 -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/cases/response-route-freshness.mjs +16 -0
- package/packages/comparison/capture-context.mjs +61 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
- package/packages/comparison/favicon-similarity.mjs +1 -1
- package/packages/comparison/page-similarity.mjs +3 -2
- package/packages/comparison/relationship-evidence.mjs +103 -14
- package/packages/comparison/relationship-provenance.mjs +99 -0
- package/packages/contracts/analyst-interchange.mjs +234 -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 +370 -114
- package/packages/contracts/case-views-contract.mjs +12 -0
- package/packages/contracts/case-views.mjs +26 -0
- package/packages/contracts/cli-command-semantics.mjs +3 -0
- package/packages/contracts/cli-grammar.mjs +6 -0
- package/packages/contracts/cli-lookup-brief.mjs +2 -0
- package/packages/contracts/cli-lookup.mjs +0 -1
- package/packages/contracts/context-review.mjs +24 -0
- package/packages/contracts/document-intake.mjs +11 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +98 -44
- package/packages/contracts/domain-control-manifest.mjs +128 -74
- package/packages/contracts/domain-control-monitor.mjs +59 -45
- package/packages/contracts/domain-control-review.mjs +200 -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/har-review.mjs +2 -0
- package/packages/contracts/http-url.mjs +2 -0
- package/packages/contracts/identity-events.mjs +4 -0
- package/packages/contracts/investigation-package-limits.mjs +24 -0
- package/packages/contracts/investigation-portability.mjs +56 -27
- package/packages/contracts/investigation-run.mjs +18 -0
- package/packages/contracts/ip-address.mjs +44 -0
- package/packages/contracts/lookup-evidence.mjs +35 -0
- package/packages/contracts/mail-authentication.mjs +5 -0
- package/packages/contracts/message-intake.mjs +31 -0
- package/packages/contracts/offline-comparison.mjs +4 -2
- package/packages/contracts/page-fingerprints.mjs +15 -0
- package/packages/contracts/passive-mail.mjs +10 -0
- package/packages/contracts/risk-calibration.mjs +0 -1
- 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 +219 -238
- package/packages/evidence/artifact-integrity.mjs +8 -2
- package/packages/evidence/artifact-structure.mjs +105 -0
- package/packages/evidence/collection-quality.mjs +67 -0
- package/packages/evidence/decision-fact.mjs +4 -1
- package/packages/evidence/dns-query-outcome.mjs +72 -0
- package/packages/evidence/domain-control-runtime.mjs +155 -27
- package/packages/evidence/domain-posture-context.mjs +79 -0
- package/packages/evidence/email-recipient.mjs +18 -0
- package/packages/evidence/image-regions.mjs +56 -0
- package/packages/evidence/latest-observations.mjs +28 -0
- package/packages/evidence/lookup-target.mjs +108 -0
- package/packages/evidence/observation.mjs +18 -1
- package/packages/evidence/passphrase-encryption.mjs +44 -0
- package/packages/evidence/png-pixels.mjs +117 -0
- package/packages/evidence/public-address-policy.mjs +83 -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/defensive-indicator-export.mjs +264 -0
- package/packages/interchange/external-findings-import.mjs +93 -61
- 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 +389 -0
- package/packages/investigation/acquisition-decision-packet.mjs +1 -1
- package/packages/investigation/bulk-mail-exposure.mjs +9 -14
- 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-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/link-intake.mjs +179 -0
- package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
- package/packages/investigation/lookup-asset-graph.mjs +161 -141
- 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/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/monitoring/analyst-review-state.mjs +38 -90
- package/packages/workspace/brand-profile-model.mjs +160 -22
- package/packages/workspace/bulk-columns.mjs +17 -0
- package/packages/workspace/bulk-review-model.mjs +39 -31
- package/packages/workspace/bulk-session-model.mjs +112 -27
- package/packages/workspace/bulk-sort.mjs +1 -0
- package/packages/workspace/campaign-model.mjs +1 -1
- package/packages/workspace/case-views.mjs +102 -0
- package/packages/workspace/detection-rule-model.mjs +1 -1
- package/packages/workspace/hostile-input.mjs +29 -8
- package/packages/workspace/investigation-guide.mjs +2 -2
- package/packages/workspace/investigation-template-model.mjs +45 -6
- package/packages/workspace/page-baseline.mjs +20 -17
- package/packages/workspace/relationship-observation-model.mjs +54 -35
- package/packages/workspace/shortlist-model.mjs +14 -5
- package/packages/workspace/watchlist-history.mjs +30 -14
- package/packages/workspace/watchlist-store.mjs +24 -6
- package/packages/workspace/website-snapshot-model.mjs +98 -33
- package/packages/workspace/workspace-archive-crypto.mjs +41 -61
- package/packages/workspace/workspace-archive.mjs +90 -25
- package/third-party-notices.txt +577 -17
- package/lib/hostname.mjs +0 -38
- package/packages/investigation/bulk-review-export.mjs +0 -63
package/cli/bounded-json.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { MAX_BOUNDED_JSON_DEPTH, MAX_BOUNDED_JSON_KEYS, MAX_BOUNDED_JSON_VALUES, parseBoundedJson, parseBoundedJsonObject, scanBoundedJson, } from "../
|
|
1
|
+
export { boundedJsonLimitsForBytes, MAX_BOUNDED_JSON_DEPTH, MAX_BOUNDED_JSON_KEYS, MAX_BOUNDED_JSON_VALUES, parseBoundedJson, parseBoundedJsonObject, scanBoundedJson, } from "../packages/analysis/bounded-json.mjs";
|
package/cli/bulk-checkpoint.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { lstat } from 'node:fs/promises';
|
|
3
3
|
import { recordOrNull } from "../lib/bounded-contract-normalizers.mjs";
|
|
4
4
|
import { readBoundedRegularTextFile } from "../lib/bounded-file.mjs";
|
|
5
|
-
import { scanBoundedJson } from "../
|
|
5
|
+
import { scanBoundedJson } from "../packages/analysis/bounded-json.mjs";
|
|
6
6
|
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
7
7
|
import { boundedCliInputError, CliUsageError } from "./errors.mjs";
|
|
8
8
|
import { writePrivateFile } from "./output-file.mjs";
|
|
@@ -28,12 +28,12 @@ async function runBulkCommand(args, dependencies, context) {
|
|
|
28
28
|
catch (error) {
|
|
29
29
|
if (error instanceof CliUsageError)
|
|
30
30
|
throw error;
|
|
31
|
-
throw new CliUsageError(`Could not read bulk input: ${boundedCliErrorMessage(error, 'Input could not be read')}
|
|
31
|
+
throw new CliUsageError(`Could not read bulk input: ${boundedCliErrorMessage(error, 'Input could not be read')}`, 'input_unavailable');
|
|
32
32
|
}
|
|
33
33
|
const parsed = parseBulkQueries(input, { deep: args.deep });
|
|
34
34
|
if (args.plan) {
|
|
35
35
|
const document = buildCollectionPreflight({
|
|
36
|
-
command: 'bulk', targetCount: parsed.queries.length,
|
|
36
|
+
command: 'bulk', targetCount: parsed.queries.length,
|
|
37
37
|
deep: args.deep, concurrency: args.concurrency, output: args.output, checkpoint: false,
|
|
38
38
|
});
|
|
39
39
|
context.writeStdout(args.output === 'json' ? formatJsonDocument(document) : context.terminal(formatCollectionPreflight(document), args.color));
|
|
@@ -113,6 +113,8 @@ async function runBulkCommand(args, dependencies, context) {
|
|
|
113
113
|
context.writeStdout(formatJsonLines(selectedItems, metadata));
|
|
114
114
|
else if (args.output === 'csv')
|
|
115
115
|
context.writeStdout(formatBulkCsv(selectedItems));
|
|
116
|
+
else if (args.output === 'csv_metadata')
|
|
117
|
+
context.writeStdout(formatBulkCsv(selectedItems, selectedDocument));
|
|
116
118
|
else if (args.output === 'domains')
|
|
117
119
|
context.writeStdout(formatBulkDomainList(selectedItems));
|
|
118
120
|
else if (args.output === 'queries')
|
package/cli/bulk-output.mjs
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
import { cliCsvCell } from "./csv.mjs";
|
|
2
|
+
import { lookupDiagnosticStates } from "../lib/lookup-diagnostics.mjs";
|
|
3
|
+
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
2
4
|
const REGISTERED_STATES = new Set(['expiring', 'for_sale', 'registered']);
|
|
3
5
|
const MAX_DNS_VALUES_PER_TYPE = 100;
|
|
6
|
+
const BULK_CSV_METADATA_COLUMNS = Object.freeze([
|
|
7
|
+
'source_schema', 'source_version', 'observed_at', 'report_generated_at',
|
|
8
|
+
'collection_origin', 'scan_mode', 'diagnostics_version', 'source_health',
|
|
9
|
+
]);
|
|
10
|
+
function bulkCsvMetadataValues(item, metadata) {
|
|
11
|
+
const diagnostics = record(item.diagnostics);
|
|
12
|
+
return [
|
|
13
|
+
metadata.schema,
|
|
14
|
+
metadata.version,
|
|
15
|
+
normalizeExplicitIsoTimestamp(item.observedAt) ?? 'unknown',
|
|
16
|
+
normalizeExplicitIsoTimestamp(metadata.generatedAt) ?? 'unknown',
|
|
17
|
+
item.collectionOrigin === 'current_run' || item.collectionOrigin === 'resumed_checkpoint' ? item.collectionOrigin : 'unknown',
|
|
18
|
+
metadata.mode,
|
|
19
|
+
Number.isSafeInteger(diagnostics.version) && Number(diagnostics.version) > 0 ? diagnostics.version : 'unknown',
|
|
20
|
+
JSON.stringify(lookupDiagnosticStates(item.diagnostics)),
|
|
21
|
+
];
|
|
22
|
+
}
|
|
4
23
|
function record(value) {
|
|
5
24
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
6
25
|
? value
|
|
@@ -78,12 +97,18 @@ function bulkDnsSummary(item) {
|
|
|
78
97
|
function booleanCell(value) {
|
|
79
98
|
return value === null ? 'unknown' : value ? 'observed' : 'not_observed';
|
|
80
99
|
}
|
|
81
|
-
function formatBulkCsv(items) {
|
|
100
|
+
function formatBulkCsv(items, metadata) {
|
|
101
|
+
if (metadata && metadata.results.length !== items.length)
|
|
102
|
+
throw new TypeError('CSV metadata does not match the selected result count.');
|
|
82
103
|
const header = [
|
|
83
104
|
'query', 'domain', 'outcome', 'availability', 'confidence', 'dns_status',
|
|
84
105
|
'a', 'aaaa', 'ns', 'mx', 'null_mx', 'spf', 'dmarc', 'error',
|
|
106
|
+
...(metadata ? BULK_CSV_METADATA_COLUMNS : []),
|
|
85
107
|
];
|
|
86
|
-
const rows = items.map((item) => {
|
|
108
|
+
const rows = items.map((item, index) => {
|
|
109
|
+
const retained = metadata?.results[index];
|
|
110
|
+
if (metadata && (!retained || retained.index !== item.index || retained.query !== item.query))
|
|
111
|
+
throw new TypeError('CSV metadata does not match the selected result identity.');
|
|
87
112
|
const availability = item.ok ? record(record(item.result).availability) : {};
|
|
88
113
|
const dns = bulkDnsSummary(item);
|
|
89
114
|
return [
|
|
@@ -101,6 +126,7 @@ function formatBulkCsv(items) {
|
|
|
101
126
|
booleanCell(dns.hasSpf),
|
|
102
127
|
booleanCell(dns.hasDmarc),
|
|
103
128
|
item.ok ? '' : item.error,
|
|
129
|
+
...(metadata && retained ? bulkCsvMetadataValues(retained, metadata) : []),
|
|
104
130
|
].map(cliCsvCell).join(',');
|
|
105
131
|
});
|
|
106
132
|
return `${[header.join(','), ...rows].join('\n')}\n`;
|
|
@@ -114,4 +140,4 @@ function formatBulkDomainList(items) {
|
|
|
114
140
|
function formatBulkQueryList(items) {
|
|
115
141
|
return items.length ? `${items.map((item) => item.query).join('\n')}\n` : '';
|
|
116
142
|
}
|
|
117
|
-
export { REGISTERED_STATES, availabilityState, bulkDnsSummary, formatBulkCsv, formatBulkDomainList, formatBulkQueryList, selectBulkItems, };
|
|
143
|
+
export { REGISTERED_STATES, BULK_CSV_METADATA_COLUMNS, availabilityState, bulkDnsSummary, bulkCsvMetadataValues, formatBulkCsv, formatBulkDomainList, formatBulkQueryList, selectBulkItems, };
|
package/cli/bulk.mjs
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
2
|
import { abortable } from "../lib/abort.mjs";
|
|
3
3
|
import { decodeBoundedUtf8 } from "../lib/bounded-file.mjs";
|
|
4
|
-
import { CliUsageError } from "./
|
|
5
|
-
import { hasUnsafeCliText } from "./errors.mjs";
|
|
4
|
+
import { CliUsageError, hasUnsafeCliText } from "./errors.mjs";
|
|
6
5
|
import { MAX_DEEP_BULK_QUERIES, MAX_FAST_BULK_QUERIES, } from "../lib/bulk-limits.mjs";
|
|
7
6
|
const MAX_BULK_INPUT_BYTES = 1024 * 1024;
|
|
8
7
|
async function readTextStreamBounded(stream, limit = MAX_BULK_INPUT_BYTES, signal) {
|
|
@@ -65,7 +64,7 @@ function parseBulkQueries(text, { deep = false } = {}) {
|
|
|
65
64
|
}
|
|
66
65
|
}
|
|
67
66
|
if (!queries.length)
|
|
68
|
-
throw new CliUsageError('Bulk input did not contain any queries.');
|
|
67
|
+
throw new CliUsageError('Bulk input did not contain any queries.', 'missing_input');
|
|
69
68
|
return { queries, duplicates, limit };
|
|
70
69
|
}
|
|
71
70
|
function boundedLookupError(error) {
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { boundedJsonLimitsForBytes, parseBoundedJsonObject } from "../packages/analysis/bounded-json.mjs";
|
|
3
|
+
import { canonicalArtifactJsonV2 } from "../packages/evidence/artifact-integrity.mjs";
|
|
4
|
+
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
5
|
+
import { MAX_CASE_CHECKPOINT_FACTS, MAX_CASE_OBJECTIVE_LENGTH, MAX_CASE_STORE_BYTES, MAX_EDITABLE_CASE_INPUT_BYTES, MAX_NOTE_LENGTH, MAX_RESPONSE_RATIONALE_LENGTH, MAX_RESPONSE_VALUE_LENGTH } from "../packages/contracts/case-portability.mjs";
|
|
6
|
+
import { readEditableCaseExport } from "../packages/cases/case-export-input.mjs";
|
|
7
|
+
import { createCaseIncident, openOrCreateCase, recordCaseConclusion, recordCaseRecheckOutcome, updateCase } from "../packages/cases/case-record-operations.mjs";
|
|
8
|
+
import { buildCaseExport, serializeCaseStore } from "../packages/cases/case-storage-model.mjs";
|
|
9
|
+
import { appendCaseEvidencePin } from "../packages/cases/case-response-model.mjs";
|
|
10
|
+
import { readCaseRecheckAnswerContext } from "../packages/cases/case-recheck-model.mjs";
|
|
11
|
+
import { caseEvidenceLinkIssues, mergeCaseEvidenceLinks } from "../packages/cases/case-evidence-links.mjs";
|
|
12
|
+
import { dispositionLabel, statusLabel } from "../packages/cases/case-record-decisions.mjs";
|
|
13
|
+
import { normalizeDomain } from "../packages/cases/case-record-core.mjs";
|
|
14
|
+
import { safeTerminalValue } from "./formatters/terminal.mjs";
|
|
15
|
+
import { CliUsageError } from "./errors.mjs";
|
|
16
|
+
import EXIT_CODES from "./exit-codes.mjs";
|
|
17
|
+
function fields(input, allowed, label) {
|
|
18
|
+
if (Object.keys(input).some(key => !allowed.includes(key)))
|
|
19
|
+
throw new CliUsageError(`${label} contains an unsupported field.`);
|
|
20
|
+
}
|
|
21
|
+
function prose(input, maximum, label) {
|
|
22
|
+
if (typeof input !== 'string' || !input.trim() || input.trim().length > maximum
|
|
23
|
+
|| /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u.test(input)) {
|
|
24
|
+
throw new CliUsageError(`${label} must contain 1–${maximum} characters without unsafe control characters.`);
|
|
25
|
+
}
|
|
26
|
+
return input.trim();
|
|
27
|
+
}
|
|
28
|
+
function selection(cases, args, required) {
|
|
29
|
+
const domain = args.domain === null ? null : normalizeDomain(args.domain);
|
|
30
|
+
if (args.domain !== null && !domain)
|
|
31
|
+
throw new CliUsageError('Select a valid Case domain.');
|
|
32
|
+
const selected = cases.filter(record => (!args.caseId || record.id === args.caseId) && (!domain || record.domain === domain));
|
|
33
|
+
if ((args.caseId || domain || required) && selected.length === 0)
|
|
34
|
+
throw new CliUsageError('The selected Case is not present in this file.');
|
|
35
|
+
if (required && selected.length !== 1)
|
|
36
|
+
throw new CliUsageError('Select exactly one Case with --case-id; no arbitrary Case was chosen.');
|
|
37
|
+
return selected;
|
|
38
|
+
}
|
|
39
|
+
function pin(input, now) {
|
|
40
|
+
if (!input || typeof input !== 'object' || Array.isArray(input))
|
|
41
|
+
throw new CliUsageError('An evidence pin must be a JSON object.');
|
|
42
|
+
const raw = input;
|
|
43
|
+
if (Object.hasOwn(raw, 'id') || Object.hasOwn(raw, 'createdAt'))
|
|
44
|
+
throw new CliUsageError('New pin identity and creation time are allocated by the Case operation.');
|
|
45
|
+
const normal = appendCaseEvidencePin([], raw, now)[0];
|
|
46
|
+
// Validate the supplied subset against the domain result, without duplicating
|
|
47
|
+
// its field inventory or accepting silent truncation/default substitution.
|
|
48
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
49
|
+
if (!Object.hasOwn(normal, key) || canonicalArtifactJsonV2(value) !== canonicalArtifactJsonV2(normal[key])) {
|
|
50
|
+
throw new CliUsageError(`Pin field ${safeTerminalValue(key)} is not canonical or exceeds its bound. Use explicit ISO timestamps and supported values; no pin was saved.`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return raw;
|
|
54
|
+
}
|
|
55
|
+
function retainExistingEntries(before, after) {
|
|
56
|
+
if (canonicalArtifactJsonV2(mergeCaseEvidenceLinks(before.evidenceLinks, after.evidenceLinks) ?? [])
|
|
57
|
+
!== canonicalArtifactJsonV2(mergeCaseEvidenceLinks(undefined, after.evidenceLinks) ?? [])) {
|
|
58
|
+
throw new CliUsageError('The operation would remove retained evidence relationships. Nothing was written.');
|
|
59
|
+
}
|
|
60
|
+
const pairs = [
|
|
61
|
+
[before.notes, after.notes], [before.evidenceHistory, after.evidenceHistory], [before.evidencePins, after.evidencePins],
|
|
62
|
+
[before.decisions, after.decisions], [before.actions, after.actions], [before.assertions, after.assertions],
|
|
63
|
+
[before.manualTrail, after.manualTrail], [before.sightings, after.sightings],
|
|
64
|
+
[before.observedEffects.reviews, after.observedEffects.reviews], [before.closures.records, after.closures.records],
|
|
65
|
+
[before.branches ?? [], after.branches ?? []], [before.attachments ?? [], after.attachments ?? []],
|
|
66
|
+
];
|
|
67
|
+
for (const [prior, next] of pairs) {
|
|
68
|
+
const retained = new Set(next.map(value => canonicalArtifactJsonV2(value)));
|
|
69
|
+
if (prior.some(value => !retained.has(canonicalArtifactJsonV2(value)))) {
|
|
70
|
+
throw new CliUsageError('The operation would remove or alter earlier retained evidence. Export a smaller working set or review it in the console; nothing was written.');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export function applyCliCaseOperation(cases, args, input, note, now) {
|
|
75
|
+
if (args.operation === 'open') {
|
|
76
|
+
const title = args.title === null ? undefined : prose(args.title, MAX_CASE_OBJECTIVE_LENGTH, 'Incident title');
|
|
77
|
+
const result = args.newIncident
|
|
78
|
+
? createCaseIncident(cases, { domain: args.domain, title }, now)
|
|
79
|
+
: openOrCreateCase(cases, { domain: args.domain, ...(title ? { title } : {}) }, now, args.caseId ? { caseId: args.caseId } : {});
|
|
80
|
+
if (!result.created && title && title !== result.record.title)
|
|
81
|
+
throw new CliUsageError('The selected Case already exists with another title. Use --new-incident for a separate investigation.');
|
|
82
|
+
return result.cases;
|
|
83
|
+
}
|
|
84
|
+
const current = selection(cases, args, true)[0];
|
|
85
|
+
let result;
|
|
86
|
+
if (args.operation === 'note') {
|
|
87
|
+
result = updateCase(cases, current.id, { note: prose(note, MAX_NOTE_LENGTH, 'Case note') }, now);
|
|
88
|
+
}
|
|
89
|
+
else if (args.operation === 'pin') {
|
|
90
|
+
result = updateCase(cases, current.id, { evidencePin: pin(input, now) }, now);
|
|
91
|
+
}
|
|
92
|
+
else if (args.operation === 'link' || args.operation === 'withdraw-link') {
|
|
93
|
+
result = updateCase(cases, current.id, args.operation === 'link' ? { evidenceLink: input } : { evidenceLinkWithdrawal: input }, now);
|
|
94
|
+
}
|
|
95
|
+
else if (args.operation === 'assess') {
|
|
96
|
+
if (!input)
|
|
97
|
+
throw new CliUsageError('An assessment requires JSON input.');
|
|
98
|
+
fields(input, ['disposition', 'reviewReasonCode', 'summary', 'rationale', 'evidence'], 'Assessment');
|
|
99
|
+
if (!Array.isArray(input.evidence) || !input.evidence.length || input.evidence.length > MAX_CASE_CHECKPOINT_FACTS) {
|
|
100
|
+
throw new CliUsageError(`Select 1–${MAX_CASE_CHECKPOINT_FACTS} evidence facts for the assessment.`);
|
|
101
|
+
}
|
|
102
|
+
const evidence = input.evidence.map(value => {
|
|
103
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
104
|
+
throw new CliUsageError('Each assessment fact needs a pin and stance.');
|
|
105
|
+
fields(value, ['pin', 'pinId', 'stance'], 'Assessment evidence');
|
|
106
|
+
if (!['supports', 'contradicts', 'unresolved'].includes(value.stance))
|
|
107
|
+
throw new CliUsageError('Use supports, contradicts or unresolved for each evidence stance.');
|
|
108
|
+
if (Object.hasOwn(value, 'pin') === Object.hasOwn(value, 'pinId'))
|
|
109
|
+
throw new CliUsageError('Select one new pin or one retained pinId for each assessment fact.');
|
|
110
|
+
return Object.hasOwn(value, 'pinId') ? { pinId: value.pinId, stance: value.stance } : { pin: pin(value.pin, now), stance: value.stance };
|
|
111
|
+
});
|
|
112
|
+
const summary = prose(input.summary, MAX_RESPONSE_VALUE_LENGTH, 'Assessment summary');
|
|
113
|
+
const rationale = prose(input.rationale, MAX_RESPONSE_RATIONALE_LENGTH, 'Assessment rationale');
|
|
114
|
+
result = recordCaseConclusion(cases, current.id, { disposition: input.disposition, reviewReasonCode: input.reviewReasonCode, summary, rationale, evidence }, now);
|
|
115
|
+
if (result.record.decisions.at(-1)?.summary !== summary || result.record.decisions.at(-1)?.rationale !== rationale) {
|
|
116
|
+
throw new CliUsageError('Assessment text needs canonical single-line values; no assessment was written.');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else if (args.operation === 'recheck') {
|
|
120
|
+
if (!input)
|
|
121
|
+
throw new CliUsageError('A recheck requires JSON input.');
|
|
122
|
+
fields(input, ['state', 'observedAt', 'completeness', 'comparisonSummary', 'source', 'followUpAt', 'limitations', 'collectionDepth', 'recheck', 'observationHostname'], 'Recheck');
|
|
123
|
+
const recheck = readCaseRecheckAnswerContext(input.recheck);
|
|
124
|
+
if (input.state === 'not_reproduced' && !recheck) {
|
|
125
|
+
throw new CliUsageError('Not reproduced requires a saved recheck question and confirmed comparable conditions. Import the planned Case or record unavailable for a limited observation.');
|
|
126
|
+
}
|
|
127
|
+
const summary = prose(input.comparisonSummary, MAX_RESPONSE_VALUE_LENGTH, 'Comparison summary');
|
|
128
|
+
result = recordCaseRecheckOutcome(cases, current.id, {
|
|
129
|
+
...input, state: input.state, observedAt: input.observedAt, completeness: input.completeness,
|
|
130
|
+
comparisonSummary: summary, source: input.source, ...(recheck ? { recheck } : {}),
|
|
131
|
+
}, now);
|
|
132
|
+
const review = result.record.observedEffects.reviews.at(-1);
|
|
133
|
+
const comparison = result.record.evidencePins.find(value => value.id === review.evidencePinId);
|
|
134
|
+
const projected = { state: review.state, observedAt: review.observedAt, completeness: review.completeness,
|
|
135
|
+
comparisonSummary: comparison.value, source: review.source, followUpAt: review.followUpAt,
|
|
136
|
+
limitations: review.limitations, collectionDepth: comparison.collectionDepth,
|
|
137
|
+
...(review.recheck ? { recheck: review.recheck } : {}),
|
|
138
|
+
...(comparison.observationHostname ? { observationHostname: comparison.observationHostname } : {}),
|
|
139
|
+
};
|
|
140
|
+
for (const [key, value] of Object.entries(input)) {
|
|
141
|
+
if (canonicalArtifactJsonV2(value) !== canonicalArtifactJsonV2(projected[key]))
|
|
142
|
+
throw new CliUsageError(`Recheck field ${safeTerminalValue(key)} would be repaired or truncated; nothing was written.`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else
|
|
146
|
+
throw new CliUsageError('Select a supported Case mutation.');
|
|
147
|
+
retainExistingEntries(current, result.record);
|
|
148
|
+
return result.cases;
|
|
149
|
+
}
|
|
150
|
+
function formatCases(cases, digest) {
|
|
151
|
+
const lines = ['Local Case review', `File digest ${digest}`, `Cases ${cases.length}`, ''];
|
|
152
|
+
for (const record of cases) {
|
|
153
|
+
lines.push(`Case ${safeTerminalValue(record.id)} · ${safeTerminalValue(record.title || record.domain)}`, `Domain ${safeTerminalValue(record.domain)}`, `Status ${statusLabel(record.status)}`, `Disposition ${dispositionLabel(record.disposition)} (analyst assessment)`, `Updated ${record.updatedAt}`);
|
|
154
|
+
for (const note of record.notes)
|
|
155
|
+
lines.push(`Note ${note.createdAt}`, safeTerminalValue(note.body, '—', MAX_NOTE_LENGTH));
|
|
156
|
+
for (const pin of record.evidencePins)
|
|
157
|
+
lines.push(`Pin ${safeTerminalValue(pin.id)} · ${safeTerminalValue(pin.label)}`, safeTerminalValue(pin.value, '—', MAX_RESPONSE_VALUE_LENGTH), ` Source ${safeTerminalValue(pin.source)} · ${safeTerminalValue(pin.observedAt, 'time unavailable')} · ${pin.completeness}`);
|
|
158
|
+
for (const decision of record.decisions)
|
|
159
|
+
lines.push(`Assessment ${decision.createdAt}`, safeTerminalValue(decision.summary, '—', MAX_RESPONSE_VALUE_LENGTH), safeTerminalValue(decision.rationale, '—', MAX_RESPONSE_RATIONALE_LENGTH));
|
|
160
|
+
for (const { link, missingPinIds, cyclic } of caseEvidenceLinkIssues(record.evidenceLinks ?? [], record.evidencePins)) {
|
|
161
|
+
lines.push(`Evidence relationship ${safeTerminalValue(link.id)} · analyst declaration`, ` ${safeTerminalValue(link.fromPinId)} ${link.kind.replaceAll('_', ' ')} ${safeTerminalValue(link.toPinId)}`, ` ${safeTerminalValue(link.basis, '—', MAX_RESPONSE_RATIONALE_LENGTH)}`);
|
|
162
|
+
if (link.withdrawal)
|
|
163
|
+
lines.push(` Withdrawn ${link.withdrawal.at}: ${safeTerminalValue(link.withdrawal.reason, '—', MAX_RESPONSE_RATIONALE_LENGTH)}`);
|
|
164
|
+
if (missingPinIds.length)
|
|
165
|
+
lines.push(` Referenced pins not retained: ${missingPinIds.map(id => safeTerminalValue(id)).join(', ')}`);
|
|
166
|
+
if (cyclic)
|
|
167
|
+
lines.push(' Conflicting imported derivation cycle; no order is inferred.');
|
|
168
|
+
}
|
|
169
|
+
for (const review of record.observedEffects.reviews)
|
|
170
|
+
lines.push(`Recheck ${safeTerminalValue(review.observedAt, 'time unavailable')} · ${review.state} · ${review.completeness} · ${safeTerminalValue(review.source)}`);
|
|
171
|
+
lines.push(`Other retained records: ${record.evidenceHistory.length} snapshots, ${record.actions.length} actions, ${record.assertions.length} assertions, ${record.attachments?.length ?? 0} file references.`, '');
|
|
172
|
+
}
|
|
173
|
+
lines.push('Use --json for the full Case export. Attached file bytes remain separate. This command makes no request.', '');
|
|
174
|
+
return lines.join('\n');
|
|
175
|
+
}
|
|
176
|
+
export async function runCaseCommand(args, dependencies, context) {
|
|
177
|
+
context.setFailureLabel('Local Case operation');
|
|
178
|
+
try {
|
|
179
|
+
const raw = args.source ? dependencies.caseFileInput ?? await context.readInput(args.source, MAX_EDITABLE_CASE_INPUT_BYTES, 'Case file') : null;
|
|
180
|
+
const digest = raw === null ? null : `sha256:${createHash('sha256').update(raw).digest('hex')}`;
|
|
181
|
+
if (args.expectedFileDigest && args.expectedFileDigest !== digest)
|
|
182
|
+
throw new CliUsageError('The Case file differs from the reviewed file digest. Nothing was written.');
|
|
183
|
+
const cases = raw === null ? [] : readEditableCaseExport(raw);
|
|
184
|
+
const now = normalizeExplicitIsoTimestamp(context.now());
|
|
185
|
+
if (!now)
|
|
186
|
+
throw new CliUsageError('A valid current timestamp is required.');
|
|
187
|
+
if (args.operation === 'show') {
|
|
188
|
+
const selected = selection(cases, args, false);
|
|
189
|
+
context.writeStdout(args.output === 'json' ? `${JSON.stringify(buildCaseExport(selected, now))}\n` : context.terminal(formatCases(selected, digest), args.color));
|
|
190
|
+
return EXIT_CODES.SUCCESS;
|
|
191
|
+
}
|
|
192
|
+
const input = args.inputSource ? parseBoundedJsonObject(await context.readInput(args.inputSource, MAX_EDITABLE_CASE_INPUT_BYTES, 'Case operation input'), {
|
|
193
|
+
maximumBytes: MAX_EDITABLE_CASE_INPUT_BYTES, limits: boundedJsonLimitsForBytes(MAX_CASE_STORE_BYTES), label: 'Case operation input',
|
|
194
|
+
}) : null;
|
|
195
|
+
const note = args.noteSource ? await context.readInput(args.noteSource, MAX_NOTE_LENGTH * 4 + 4, 'Case note') : args.text;
|
|
196
|
+
const next = applyCliCaseOperation(cases, args, input, note, now);
|
|
197
|
+
if (Buffer.byteLength(serializeCaseStore(next)) > MAX_CASE_STORE_BYTES)
|
|
198
|
+
throw new CliUsageError('The updated Case store exceeds its byte budget. No evidence was pruned and nothing was written.');
|
|
199
|
+
const output = `${JSON.stringify(buildCaseExport(next, now))}\n`;
|
|
200
|
+
// Apply the same strict admission to our output before atomic publication.
|
|
201
|
+
readEditableCaseExport(output);
|
|
202
|
+
dependencies.signal?.throwIfAborted();
|
|
203
|
+
context.writeStdout(output);
|
|
204
|
+
return EXIT_CODES.SUCCESS;
|
|
205
|
+
}
|
|
206
|
+
catch (cause) {
|
|
207
|
+
dependencies.signal?.throwIfAborted();
|
|
208
|
+
if (cause instanceof CliUsageError)
|
|
209
|
+
throw cause;
|
|
210
|
+
throw new CliUsageError(cause instanceof Error ? cause.message : 'The Case operation could not be completed.');
|
|
211
|
+
}
|
|
212
|
+
}
|
package/cli/case-pack.mjs
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { Buffer } from 'node:buffer';
|
|
3
3
|
import { canonicalArtifactJson, canonicalArtifactJsonV2, SORTED_JSON_V2, } from "../packages/evidence/artifact-integrity.mjs";
|
|
4
|
-
import { buildCaseReport } from "../packages/cases/case-report.mjs";
|
|
4
|
+
import { buildCaseReport, buildCaseReportVerificationProjection } from "../packages/cases/case-report.mjs";
|
|
5
5
|
import { WHOISLEUTH_APPLICATION_VERSION } from "../lib/application-version.mjs";
|
|
6
|
-
import { assertBoundedJsonStructure, scanBoundedJson } from "../
|
|
7
|
-
import { assertCaseBrandProfileIds
|
|
8
|
-
import {
|
|
6
|
+
import { assertBoundedJsonStructure, scanBoundedJson } from "../packages/analysis/bounded-json.mjs";
|
|
7
|
+
import { assertCaseBrandProfileIds } from "../packages/cases/case-brand-profile-references.mjs";
|
|
8
|
+
import { CASE_AUDIENCE_SENSITIVE_FIELD_NAMES, caseAudienceExclusions, projectCaseForAudience, } from "../packages/cases/case-record-projection.mjs";
|
|
9
|
+
import { normalizeDomain } from "../packages/evidence/domain-name.mjs";
|
|
10
|
+
import { normalizeCaseStore } from "../packages/cases/case-migration-model.mjs";
|
|
11
|
+
import { safeId } from "../packages/cases/case-record-core.mjs";
|
|
12
|
+
import { CASE_REPORT_SCHEMA, CASE_IMPORT_VERSIONS, CASE_SCHEMA_VERSION, INCIDENT_CASE_SCHEMA_VERSION, CLI_CASE_PACK_SCHEMA, CLI_CASE_PACK_VERSION, CLI_CASE_PACK_INPUT_CASE_VERSIONS, CLI_CASE_PACK_CURRENT_REDACTION_KEYS, CLI_CASE_PACK_PUBLIC_REPORT_KEYS, CLI_CASE_PACK_INTEGRITY_KEYS as INTEGRITY_KEYS, CLI_CASE_PACK_LIMITATIONS as PACKET_LIMITATIONS, CLI_CASE_PACK_PACKET_KEYS as PACKET_KEYS, CLI_CASE_PACK_REPORT_KEYS as REPORT_KEYS, CLI_CASE_PACK_ROOT_KEYS as ROOT_KEYS, MAX_CASE_IMPORT_BYTES, MAX_CASE_PACK_CASES, MAX_CASE_PACK_INPUT_BYTES, PUBLIC_CASE_SCHEMA_VERSION, PUBLISHED_V2_3_CASE_SCHEMA_VERSION, caseReportVersionMatchesCase, } from "../packages/contracts/case-portability.mjs";
|
|
9
13
|
import { CliUsageError } from "./errors.mjs";
|
|
14
|
+
import { canonicalCaseExportProjection as canonicalPackProjection } from "../packages/cases/case-export-input.mjs";
|
|
10
15
|
export { CLI_CASE_PACK_SCHEMA, CLI_CASE_PACK_VERSION, MAX_CASE_PACK_CASES, MAX_CASE_PACK_INPUT_BYTES, };
|
|
11
16
|
const MAX_CASE_PACK_REFERENCE_SCAN_DEPTH = 64;
|
|
12
17
|
function record(value) {
|
|
@@ -61,6 +66,11 @@ function allowedSensitiveFieldPath(key, path) {
|
|
|
61
66
|
return topCasePath(path) || reportCasePath(path);
|
|
62
67
|
if (key === 'notes')
|
|
63
68
|
return topCasePath(path) || reportCasePath(path);
|
|
69
|
+
if (key === 'workflowMetadata')
|
|
70
|
+
return topCasePath(path) || analystResponsePath(path);
|
|
71
|
+
if (key === 'incidentTargets' || key === 'investigationContext') {
|
|
72
|
+
return path.at(-1) === 'workflowMetadata' && (topCasePath(path.slice(0, -1)) || analystResponsePath(path.slice(0, -1)));
|
|
73
|
+
}
|
|
64
74
|
if (key === 'actions' || key === 'assertions' || key === 'manualTrail' || key === 'observedEffects' || key === 'closures' || key === 'branches') {
|
|
65
75
|
return topCasePath(path) || analystResponsePath(path);
|
|
66
76
|
}
|
|
@@ -125,7 +135,7 @@ function canonicalValuesMatch(left, right) {
|
|
|
125
135
|
return false;
|
|
126
136
|
}
|
|
127
137
|
}
|
|
128
|
-
function assertCanonicalCaseIdentities(cases, label) {
|
|
138
|
+
function assertCanonicalCaseIdentities(cases, label, caseVersion) {
|
|
129
139
|
const ids = new Set();
|
|
130
140
|
const domains = new Set();
|
|
131
141
|
for (const value of cases) {
|
|
@@ -136,23 +146,20 @@ function assertCanonicalCaseIdentities(cases, label) {
|
|
|
136
146
|
|| typeof item.domain !== 'string'
|
|
137
147
|
|| normalizeDomain(item.domain) !== item.domain
|
|
138
148
|
|| ids.has(item.id)
|
|
139
|
-
|| domains.has(item.domain)) {
|
|
149
|
+
|| (caseVersion < INCIDENT_CASE_SCHEMA_VERSION && domains.has(item.domain))) {
|
|
140
150
|
throw new TypeError(`${label} contains a missing, unsafe, non-canonical, or duplicate Case identity.`);
|
|
141
151
|
}
|
|
142
152
|
ids.add(item.id);
|
|
143
153
|
domains.add(item.domain);
|
|
144
154
|
}
|
|
145
155
|
}
|
|
146
|
-
function assertCurrentCaseProjection(rawCases, normalised, label) {
|
|
147
|
-
const requiredFields = new Set(CASE_RECORD_FIELD_NAMES);
|
|
156
|
+
function assertCurrentCaseProjection(rawCases, normalised, label, caseVersion = CASE_SCHEMA_VERSION) {
|
|
148
157
|
if (rawCases.length !== normalised.length || rawCases.some((item, index) => {
|
|
149
158
|
const raw = record(item);
|
|
150
159
|
return !raw
|
|
151
|
-
|| Object.keys(raw).length !== requiredFields.size
|
|
152
|
-
|| Object.keys(raw).some((key) => !requiredFields.has(key))
|
|
153
160
|
|| !canonicalValuesMatch(raw, normalised[index]);
|
|
154
161
|
})) {
|
|
155
|
-
throw new TypeError(`${label} contains a schema ${
|
|
162
|
+
throw new TypeError(`${label} contains a schema ${caseVersion} Case that would be repaired, truncated, or otherwise changed during normalisation.`);
|
|
156
163
|
}
|
|
157
164
|
}
|
|
158
165
|
/** Historical packs keep their immutable wire shape, so only their retained audience guarantees are checked here. */
|
|
@@ -196,17 +203,17 @@ function assertLegacyAudienceFields(value, normalised, audience) {
|
|
|
196
203
|
}
|
|
197
204
|
}
|
|
198
205
|
}
|
|
199
|
-
function assertCurrentAudienceProjection(value, normalised, audience) {
|
|
200
|
-
const expected = projectCaseForAudience(normalised, audience);
|
|
201
|
-
for (const field of
|
|
206
|
+
function assertCurrentAudienceProjection(value, normalised, audience, caseVersion) {
|
|
207
|
+
const expected = canonicalPackProjection([projectCaseForAudience(normalised, audience)], caseVersion)[0];
|
|
208
|
+
for (const field of Object.keys(expected)) {
|
|
202
209
|
if (!canonicalValuesMatch(value[field], expected[field])) {
|
|
203
210
|
throw new TypeError(`The CLI case pack contains ${field} excluded by its audience or transformed incorrectly.`);
|
|
204
211
|
}
|
|
205
212
|
}
|
|
206
213
|
}
|
|
207
|
-
function assertAudienceProjection(value, normalised, audience, currentSchema) {
|
|
214
|
+
function assertAudienceProjection(value, normalised, audience, currentSchema, caseVersion) {
|
|
208
215
|
if (currentSchema)
|
|
209
|
-
assertCurrentAudienceProjection(value, normalised, audience);
|
|
216
|
+
assertCurrentAudienceProjection(value, normalised, audience, caseVersion);
|
|
210
217
|
else
|
|
211
218
|
assertLegacyAudienceFields(value, normalised, audience);
|
|
212
219
|
}
|
|
@@ -217,11 +224,11 @@ function assertCurrentReportProjection(report, rawCase) {
|
|
|
217
224
|
if (typeof report.generatedAt !== 'string') {
|
|
218
225
|
throw new TypeError('The CLI case pack contains an invalid or mismatched Case report.');
|
|
219
226
|
}
|
|
220
|
-
const expected =
|
|
227
|
+
const expected = buildCaseReportVerificationProjection(rawCase, {
|
|
221
228
|
applicationVersion: reportApplicationVersion(report),
|
|
222
229
|
includeNotes: false,
|
|
223
230
|
generatedAt: report.generatedAt,
|
|
224
|
-
})
|
|
231
|
+
}, report.schemaVersion);
|
|
225
232
|
if (!canonicalValuesMatch(report, expected)) {
|
|
226
233
|
throw new TypeError('The CLI case pack contains an invalid or mismatched Case report projection.');
|
|
227
234
|
}
|
|
@@ -241,17 +248,18 @@ export function buildCliCasePack(input, options, generatedAt = new Date().toISOS
|
|
|
241
248
|
throw new CliUsageError('Case-pack input must be valid bounded JSON without duplicate keys.');
|
|
242
249
|
}
|
|
243
250
|
const root = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
244
|
-
|
|
245
|
-
|
|
251
|
+
const supportedInput = CLI_CASE_PACK_INPUT_CASE_VERSIONS.some((version) => version === root.version);
|
|
252
|
+
if (Number.isSafeInteger(root.version) && root.version < CASE_SCHEMA_VERSION && !supportedInput) {
|
|
253
|
+
throw new CliUsageError(`Case schema ${String(root.version)} cannot be packaged directly. This command accepts schemas ${CLI_CASE_PACK_INPUT_CASE_VERSIONS.join(', ')}. Use a supported Case import to create a current export first; no data was changed.`);
|
|
246
254
|
}
|
|
247
255
|
if (Number.isSafeInteger(root.version) && root.version > CASE_SCHEMA_VERSION) {
|
|
248
256
|
throw new CliUsageError(`Case schema ${String(root.version)} is newer than the supported schema ${CASE_SCHEMA_VERSION}; no data was changed.`);
|
|
249
257
|
}
|
|
250
|
-
if (
|
|
251
|
-
throw new CliUsageError(`Case-pack input must be a well-formed WHOISleuth Case schema ${
|
|
258
|
+
if (!supportedInput || !Array.isArray(root.cases)) {
|
|
259
|
+
throw new CliUsageError(`Case-pack input must be a well-formed WHOISleuth Case export with schema ${CLI_CASE_PACK_INPUT_CASE_VERSIONS.join(' or ')}.`);
|
|
252
260
|
}
|
|
253
261
|
try {
|
|
254
|
-
assertCanonicalCaseIdentities(root.cases, 'Case-pack input');
|
|
262
|
+
assertCanonicalCaseIdentities(root.cases, 'Case-pack input', root.version);
|
|
255
263
|
for (const item of root.cases) {
|
|
256
264
|
const rawCase = record(item);
|
|
257
265
|
if (!rawCase || !Object.hasOwn(rawCase, 'brandProfileIds'))
|
|
@@ -275,7 +283,7 @@ export function buildCliCasePack(input, options, generatedAt = new Date().toISOS
|
|
|
275
283
|
throw new CliUsageError(`Case packs are limited to ${MAX_CASE_PACK_CASES} reviewed cases. Export a smaller selected set so no case is silently omitted.`);
|
|
276
284
|
}
|
|
277
285
|
try {
|
|
278
|
-
assertCurrentCaseProjection(root.cases, normalised, 'Case-pack input');
|
|
286
|
+
assertCurrentCaseProjection(root.cases, canonicalPackProjection(normalised, root.version), 'Case-pack input', root.version);
|
|
279
287
|
}
|
|
280
288
|
catch (cause) {
|
|
281
289
|
throw new CliUsageError(cause instanceof Error ? cause.message : `Case-pack schema ${CASE_SCHEMA_VERSION} input is not exact.`);
|
|
@@ -313,7 +321,7 @@ export function buildCliCasePack(input, options, generatedAt = new Date().toISOS
|
|
|
313
321
|
}),
|
|
314
322
|
});
|
|
315
323
|
if (Buffer.byteLength(JSON.stringify(document), 'utf8') > MAX_CASE_IMPORT_BYTES) {
|
|
316
|
-
throw new CliUsageError(
|
|
324
|
+
throw new CliUsageError(`The generated case pack exceeds the browser import limit of ${MAX_CASE_IMPORT_BYTES} bytes. Select fewer cases or a more restrictive audience so no evidence is silently omitted.`);
|
|
317
325
|
}
|
|
318
326
|
return document;
|
|
319
327
|
}
|
|
@@ -382,15 +390,16 @@ export function verifyCliCasePack(input) {
|
|
|
382
390
|
|| !stringListsMatch(redactionManifest.excluded, caseAudienceExclusions(audience, root.version))) {
|
|
383
391
|
throw new TypeError('The CLI case pack has an invalid audience redaction manifest.');
|
|
384
392
|
}
|
|
385
|
-
assertCanonicalCaseIdentities(root.cases, 'The CLI case pack');
|
|
393
|
+
assertCanonicalCaseIdentities(root.cases, 'The CLI case pack', root.version);
|
|
386
394
|
const normalised = normalizeCaseStore(root).cases;
|
|
387
395
|
if (normalised.length !== root.cases.length) {
|
|
388
396
|
throw new TypeError('The CLI case pack contains an invalid case collection.');
|
|
389
397
|
}
|
|
390
|
-
const currentCaseSchema = root.version
|
|
398
|
+
const currentCaseSchema = root.version >= PUBLISHED_V2_3_CASE_SCHEMA_VERSION;
|
|
399
|
+
const projected = canonicalPackProjection(normalised, root.version);
|
|
391
400
|
if (currentCaseSchema)
|
|
392
|
-
assertCurrentCaseProjection(root.cases,
|
|
393
|
-
const
|
|
401
|
+
assertCurrentCaseProjection(root.cases, projected, 'The CLI case pack', root.version);
|
|
402
|
+
const normalisedById = new Map(projected.map((item) => [item.id, item]));
|
|
394
403
|
const caseReferenceLists = [];
|
|
395
404
|
const reportReferenceLists = [];
|
|
396
405
|
for (let index = 0; index < root.cases.length; index += 1) {
|
|
@@ -401,17 +410,18 @@ export function verifyCliCasePack(input) {
|
|
|
401
410
|
|| !report
|
|
402
411
|
|| !reportCase
|
|
403
412
|
|| typeof rawCase.domain !== 'string'
|
|
413
|
+
|| typeof rawCase.id !== 'string'
|
|
404
414
|
|| report.schema !== CASE_REPORT_SCHEMA
|
|
405
415
|
|| !caseReportVersionMatchesCase(root.version, report.schemaVersion)
|
|
406
416
|
|| reportCase.id !== rawCase.id
|
|
407
417
|
|| reportCase.domain !== rawCase.domain
|
|
408
|
-
|| !
|
|
418
|
+
|| !normalisedById.has(rawCase.id)) {
|
|
409
419
|
throw new TypeError('The CLI case pack contains an invalid or mismatched Case report.');
|
|
410
420
|
}
|
|
411
|
-
const normalisedCase =
|
|
421
|
+
const normalisedCase = normalisedById.get(rawCase.id);
|
|
412
422
|
if (!normalisedCase)
|
|
413
423
|
throw new TypeError('The CLI case pack contains an invalid Case projection.');
|
|
414
|
-
assertAudienceProjection(rawCase, normalisedCase, audience, currentCaseSchema);
|
|
424
|
+
assertAudienceProjection(rawCase, normalisedCase, audience, currentCaseSchema, root.version);
|
|
415
425
|
try {
|
|
416
426
|
if (!Object.hasOwn(rawCase, 'brandProfileIds') || !Object.hasOwn(reportCase, 'brandProfileIds'))
|
|
417
427
|
throw new Error('missing');
|
|
@@ -423,6 +433,13 @@ export function verifyCliCasePack(input) {
|
|
|
423
433
|
}
|
|
424
434
|
if (currentCaseSchema)
|
|
425
435
|
assertCurrentReportProjection(report, normalisedCase);
|
|
436
|
+
else {
|
|
437
|
+
assertLegacyAudienceFields(reportCase, normalisedCase, audience);
|
|
438
|
+
const response = record(report.analystResponse);
|
|
439
|
+
if (!response)
|
|
440
|
+
throw new TypeError('The CLI case pack contains an invalid analyst-response report.');
|
|
441
|
+
assertLegacyAudienceFields(response, normalisedCase, audience);
|
|
442
|
+
}
|
|
426
443
|
}
|
|
427
444
|
const omitted = redactionManifest.brandProfileReferencesOmitted;
|
|
428
445
|
if (!Object.hasOwn(redactionManifest, 'brandProfileReferencesOmitted')
|
|
@@ -465,6 +482,7 @@ export function formatCliCasePack(document) {
|
|
|
465
482
|
`Cases ${document.cases.length}`,
|
|
466
483
|
`Reports ${document.packet.reports.length}`,
|
|
467
484
|
`Digest ${document.integrity.digestSha256}`,
|
|
485
|
+
'Included for every audience: domain evidence, Case, pin and decision identifiers, tags, decision summaries and rationale. Review these before sharing.',
|
|
468
486
|
'',
|
|
469
487
|
...document.packet.redactionManifest.excluded.map((item) => `Excluded: ${item}`),
|
|
470
488
|
'',
|
package/cli/ci-report.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { WHOISLEUTH_REQUEST_POLICY_URL } from "../
|
|
1
|
+
import { WHOISLEUTH_REQUEST_POLICY_URL } from "../packages/analysis/project-metadata.mjs";
|
|
2
2
|
import { CLI_DOMAIN_CONTROL_MONITOR_SCHEMA } from "../packages/contracts/domain-control-monitor.mjs";
|
|
3
3
|
function record(value) {
|
|
4
4
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
@@ -45,9 +45,12 @@ function junitCases(document) {
|
|
|
45
45
|
return [{ name: 'WHOISleuth result', failure: 'Unsupported document for JUnit conversion.' }];
|
|
46
46
|
}
|
|
47
47
|
export function formatCliJunit(document) {
|
|
48
|
-
const
|
|
48
|
+
const root = record(document);
|
|
49
|
+
const cases = junitCases(root);
|
|
49
50
|
const failures = cases.filter((item) => item.failure).length;
|
|
50
|
-
|
|
51
|
+
const reminders = root.schema === CLI_DOMAIN_CONTROL_MONITOR_SCHEMA
|
|
52
|
+
? `\n <system-out>${xml(`Renewal reviews due: ${Number(record(record(root.review).counts).due ?? 0)}`)}</system-out>` : '';
|
|
53
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<testsuite name="WHOISleuth" tests="${cases.length}" failures="${failures}">\n${cases.map((item) => ` <testcase classname="whoisleuth" name="${xml(item.name)}">${item.failure ? `<failure message="${xml(item.failure)}" />` : ''}</testcase>`).join('\n')}${reminders}\n</testsuite>\n`;
|
|
51
54
|
}
|
|
52
55
|
export function buildPostureSarif(document) {
|
|
53
56
|
const root = record(document);
|