@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
|
@@ -5,13 +5,15 @@
|
|
|
5
5
|
// are treated as untrusted inputs and are not retained in the result.
|
|
6
6
|
import { createHash } from 'node:crypto';
|
|
7
7
|
import { Worker } from 'node:worker_threads';
|
|
8
|
+
import { createInlineLibraryScanner } from "./browser-library-worker.mjs";
|
|
8
9
|
import { RETIRE_BROWSER_CATALOG } from "./generated/retire-browser-catalog.mjs";
|
|
9
10
|
import { CISA_KEV_CATALOG } from "./generated/cisa-kev-catalog.mjs";
|
|
10
|
-
import { createObservation } from "../packages/evidence/observation.mjs";
|
|
11
|
+
import { createObservation, normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
12
|
+
import { isCveIdentifier } from "../packages/contracts/vulnerability-identifiers.mjs";
|
|
11
13
|
import { BROWSER_LIBRARY_PROFILE_VERSION, MAX_LIBRARY_FINDINGS, } from "./lookup-child-profile-contract.mjs";
|
|
12
|
-
import { MAX_INLINE_SCRIPT_CHARS, MAX_INLINE_SCRIPT_TOTAL_CHARS, MAX_SCRIPT_ELEMENTS, MAX_STATIC_HTML_CHARS, analyzeStaticHtml, } from "./static-html-analysis.mjs";
|
|
14
|
+
import { MAX_INLINE_SCRIPT_CHARS, MAX_INLINE_SCRIPT_TOTAL_CHARS, MAX_SCRIPT_ELEMENTS, MAX_SCRIPT_REFERENCE_LENGTH, MAX_STATIC_HTML_CHARS, analyzeStaticHtml, } from "./static-html-analysis.mjs";
|
|
13
15
|
// Catalogue expressions are reviewed third-party inputs. Keep every individual
|
|
14
|
-
// regular-expression evaluation and
|
|
16
|
+
// regular-expression evaluation and aggregate reference/inline-signature work small
|
|
15
17
|
// enough that a target-controlled script cannot monopolise the Node event loop.
|
|
16
18
|
// Full-content hashes still use the complete already-capped script body.
|
|
17
19
|
const MAX_INLINE_LIBRARY_SCAN_CHARS = 1_024;
|
|
@@ -21,11 +23,9 @@ const MAX_INLINE_LIBRARY_WORKER_BYTES = 64 * 1024;
|
|
|
21
23
|
const MAX_LIBRARY_HTML_CHARS = MAX_STATIC_HTML_CHARS;
|
|
22
24
|
const MAX_ADVISORY_IDENTIFIERS = 16;
|
|
23
25
|
const MAX_WEAKNESS_CLASSES = 12;
|
|
24
|
-
const MAX_MATCHES_PER_PATTERN = 4;
|
|
25
26
|
const MAX_VERSION_LENGTH = 64;
|
|
26
27
|
const COMPONENT_RE = /^[a-z0-9._-]{1,80}$/i;
|
|
27
28
|
const VERSION_RE = /^[0-9][0-9.a-z_-]{0,63}$/i;
|
|
28
|
-
const CVE_RE = /^CVE-[0-9X-]+$/;
|
|
29
29
|
const GHSA_RE = /^GHSA-[A-Z0-9-]+$/;
|
|
30
30
|
const CWE_RE = /^CWE-[0-9]+$/;
|
|
31
31
|
const SEVERITY_WEIGHT = Object.freeze({
|
|
@@ -37,38 +37,64 @@ const SEVERITY_WEIGHT = Object.freeze({
|
|
|
37
37
|
});
|
|
38
38
|
const CATALOG_COMPONENTS = RETIRE_BROWSER_CATALOG.components;
|
|
39
39
|
const KNOWN_EXPLOITED_IDENTIFIERS = new Set(CISA_KEV_CATALOG.identifiers);
|
|
40
|
-
const
|
|
40
|
+
const KNOWN_EXPLOITED_RELEASED_AT = normalizeExplicitIsoTimestamp(CISA_KEV_CATALOG.releasedAt);
|
|
41
|
+
if (KNOWN_EXPLOITED_RELEASED_AT === null)
|
|
42
|
+
throw new Error('Pinned known-exploited catalogue has an invalid release timestamp.');
|
|
43
|
+
const PATTERN_EXTRACTOR_CATALOGUE = Object.freeze(Object.entries(CATALOG_COMPONENTS).map(([component, value]) => {
|
|
41
44
|
const extractors = record(record(value).extractors);
|
|
45
|
+
const patterns = (name) => Object.freeze((Array.isArray(extractors[name]) ? extractors[name] : [])
|
|
46
|
+
.filter((pattern) => typeof pattern === 'string' && pattern.length > 0 && pattern.length <= 2_048)
|
|
47
|
+
.slice(0, 64));
|
|
42
48
|
return Object.freeze({
|
|
43
49
|
component,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
.slice(0, 64)),
|
|
47
|
-
replacements: Object.freeze((Array.isArray(extractors.filecontentreplace) ? extractors.filecontentreplace : [])
|
|
48
|
-
.filter((pattern) => typeof pattern === 'string' && pattern.length > 0 && pattern.length <= 2_048)
|
|
49
|
-
.slice(0, 64)),
|
|
50
|
+
uri: patterns('uri'), filename: patterns('filename'),
|
|
51
|
+
patterns: patterns('filecontent'), replacements: patterns('filecontentreplace'),
|
|
50
52
|
});
|
|
51
53
|
}));
|
|
52
|
-
const
|
|
54
|
+
const LIBRARY_REGEX_WORKER_SOURCE = String.raw `
|
|
53
55
|
const { parentPort, workerData } = require('node:worker_threads');
|
|
54
56
|
const versions = /^[0-9][0-9.a-z_-]{0,63}$/i;
|
|
55
57
|
parentPort.on('message', (job) => {
|
|
56
58
|
const control = new Int32Array(job.control);
|
|
57
59
|
const output = new Uint8Array(job.output);
|
|
58
|
-
const matches =
|
|
59
|
-
const add = (component, version) => {
|
|
60
|
+
const matches = new Map();
|
|
61
|
+
const add = (component, version, method) => {
|
|
60
62
|
const normalized = String(version).replace(/(?:\.|-)?min$/i, '').slice(0, 64);
|
|
61
|
-
|
|
63
|
+
const key = component + '\u0000' + normalized;
|
|
64
|
+
if (!versions.test(normalized)) return;
|
|
65
|
+
if (!matches.has(key) && matches.size < 64) matches.set(key, [component, normalized, new Set()]);
|
|
66
|
+
matches.get(key)?.[2].add(method);
|
|
62
67
|
};
|
|
63
68
|
try {
|
|
69
|
+
const input = JSON.parse(job.value);
|
|
70
|
+
for (const reference of input.references) {
|
|
71
|
+
const filename = (reference.replace(/\\/g, '/').split('/').pop() || '').split(/[?#]/, 1)[0];
|
|
72
|
+
for (const entry of workerData.catalogue) {
|
|
73
|
+
for (const [patterns, value, method, anchored] of [
|
|
74
|
+
[entry.uri, reference, 'script URL', false],
|
|
75
|
+
[entry.filename, filename, 'script filename', true],
|
|
76
|
+
]) {
|
|
77
|
+
if (!value) continue;
|
|
78
|
+
for (const pattern of patterns) {
|
|
79
|
+
let expression;
|
|
80
|
+
try { expression = new RegExp(anchored ? '^(?:' + pattern + ')$' : pattern, 'g'); } catch { continue; }
|
|
81
|
+
let match, count = 0;
|
|
82
|
+
while ((match = expression.exec(value)) && count < 4) {
|
|
83
|
+
if (typeof match[1] === 'string' && versions.test(match[1])) { add(entry.component, match[1], method); count += 1; }
|
|
84
|
+
if (match[0] === '') expression.lastIndex += 1;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
64
90
|
for (const entry of workerData.catalogue) {
|
|
65
91
|
for (const pattern of entry.patterns) {
|
|
66
92
|
let expression;
|
|
67
93
|
try { expression = new RegExp(pattern, 'g'); } catch { continue; }
|
|
68
94
|
let match;
|
|
69
95
|
let count = 0;
|
|
70
|
-
while ((match = expression.exec(
|
|
71
|
-
if (typeof match[1] === 'string') add(entry.component, match[1]);
|
|
96
|
+
while ((match = expression.exec(input.inline)) && count < 4) {
|
|
97
|
+
if (typeof match[1] === 'string') add(entry.component, match[1], 'inline signature');
|
|
72
98
|
if (match[0] === '') expression.lastIndex += 1;
|
|
73
99
|
count += 1;
|
|
74
100
|
}
|
|
@@ -80,25 +106,32 @@ parentPort.on('message', (job) => {
|
|
|
80
106
|
try { expression = new RegExp(descriptor[1], 'g'); } catch { continue; }
|
|
81
107
|
let match;
|
|
82
108
|
let count = 0;
|
|
83
|
-
while ((match = expression.exec(
|
|
109
|
+
while ((match = expression.exec(input.inline)) && count < 4) {
|
|
84
110
|
let replaced = match[0];
|
|
85
111
|
try { replaced = match[0].replace(new RegExp(descriptor[1]), descriptor[2]); } catch { break; }
|
|
86
|
-
add(entry.component, replaced);
|
|
112
|
+
add(entry.component, replaced, 'inline signature');
|
|
87
113
|
if (match[0] === '') expression.lastIndex += 1;
|
|
88
114
|
count += 1;
|
|
89
115
|
}
|
|
90
116
|
}
|
|
91
117
|
}
|
|
92
|
-
const encoded = Buffer.from(JSON.stringify(matches));
|
|
118
|
+
const encoded = Buffer.from(JSON.stringify([...matches.values()].flatMap(([component, version, methods]) => [...methods].map(method => [component, version, method]))));
|
|
93
119
|
if (encoded.length > output.length) Atomics.store(control, 0, -2);
|
|
94
120
|
else { output.set(encoded); Atomics.store(control, 0, encoded.length); }
|
|
95
121
|
} catch {
|
|
96
122
|
Atomics.store(control, 0, -1);
|
|
97
123
|
}
|
|
98
|
-
|
|
124
|
+
parentPort.postMessage({ id: job.id });
|
|
99
125
|
});
|
|
100
126
|
`;
|
|
101
|
-
|
|
127
|
+
const scanLibraryPatterns = createInlineLibraryScanner(() => new Worker(LIBRARY_REGEX_WORKER_SOURCE, { eval: true, workerData: { catalogue: PATTERN_EXTRACTOR_CATALOGUE } }), {
|
|
128
|
+
// JSON can escape each admitted character to six characters; include separators and keys.
|
|
129
|
+
maximumCharacters: 6 * (MAX_INLINE_LIBRARY_SCAN_TOTAL_CHARS + MAX_SCRIPT_ELEMENTS * MAX_SCRIPT_REFERENCE_LENGTH) + MAX_SCRIPT_ELEMENTS * 10 + 128,
|
|
130
|
+
outputBytes: MAX_INLINE_LIBRARY_WORKER_BYTES,
|
|
131
|
+
deadlineMs: MAX_INLINE_LIBRARY_SCAN_MS,
|
|
132
|
+
// One worker/output buffer, with at most sixteen bounded pending inputs.
|
|
133
|
+
pending: 16,
|
|
134
|
+
});
|
|
102
135
|
function record(value) {
|
|
103
136
|
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
104
137
|
? value
|
|
@@ -156,26 +189,6 @@ function matchingVulnerabilities(component, version) {
|
|
|
156
189
|
}
|
|
157
190
|
return matched;
|
|
158
191
|
}
|
|
159
|
-
function matchPattern(pattern, value) {
|
|
160
|
-
if (typeof pattern !== 'string' || pattern.length === 0 || pattern.length > 2_048)
|
|
161
|
-
return [];
|
|
162
|
-
let expression;
|
|
163
|
-
try {
|
|
164
|
-
expression = new RegExp(pattern, 'g');
|
|
165
|
-
}
|
|
166
|
-
catch {
|
|
167
|
-
return [];
|
|
168
|
-
}
|
|
169
|
-
const matches = [];
|
|
170
|
-
let match;
|
|
171
|
-
while ((match = expression.exec(value)) && matches.length < MAX_MATCHES_PER_PATTERN) {
|
|
172
|
-
if (typeof match[1] === 'string' && VERSION_RE.test(match[1]))
|
|
173
|
-
matches.push(match[1]);
|
|
174
|
-
if (match[0] === '')
|
|
175
|
-
expression.lastIndex += 1;
|
|
176
|
-
}
|
|
177
|
-
return matches;
|
|
178
|
-
}
|
|
179
192
|
function addDetected(detected, component, version, method) {
|
|
180
193
|
const normalizedVersion = version.replace(/(?:\.|-)?min$/i, '').slice(0, MAX_VERSION_LENGTH);
|
|
181
194
|
if (!COMPONENT_RE.test(component) || !VERSION_RE.test(normalizedVersion))
|
|
@@ -190,84 +203,24 @@ function addDetected(detected, component, version, method) {
|
|
|
190
203
|
return;
|
|
191
204
|
detected.set(key, { component, version: normalizedVersion, detections: new Set([method]) });
|
|
192
205
|
}
|
|
193
|
-
function
|
|
194
|
-
|
|
195
|
-
const component = record(rawComponent);
|
|
196
|
-
const extractors = record(component.extractors);
|
|
197
|
-
const patterns = Array.isArray(extractors[extractorName]) ? extractors[extractorName] : [];
|
|
198
|
-
for (const pattern of patterns.slice(0, 64)) {
|
|
199
|
-
for (const version of matchPattern(pattern, value))
|
|
200
|
-
addDetected(detected, componentName, version, method);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
function scanFilename(detected, reference) {
|
|
205
|
-
const filename = (reference.replace(/\\/g, '/').split('/').pop() || '').split(/[?#]/, 1)[0];
|
|
206
|
-
if (!filename)
|
|
207
|
-
return;
|
|
208
|
-
for (const [componentName, rawComponent] of Object.entries(CATALOG_COMPONENTS)) {
|
|
209
|
-
const component = record(rawComponent);
|
|
210
|
-
const extractors = record(component.extractors);
|
|
211
|
-
const patterns = Array.isArray(extractors.filename) ? extractors.filename : [];
|
|
212
|
-
for (const pattern of patterns.slice(0, 64)) {
|
|
213
|
-
if (typeof pattern !== 'string' || pattern.length > 2_048)
|
|
214
|
-
continue;
|
|
215
|
-
for (const version of matchPattern(`^(?:${pattern})$`, filename)) {
|
|
216
|
-
addDetected(detected, componentName, version, 'script filename');
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
function scanInlineSignatures(detected, value) {
|
|
222
|
-
if (!value)
|
|
206
|
+
async function scanSignatures(detected, value, references, signal) {
|
|
207
|
+
if (!value && !references.length)
|
|
223
208
|
return { timedOut: false, unavailable: false };
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (!inlineRegexWorker) {
|
|
229
|
-
const createdWorker = new Worker(INLINE_REGEX_WORKER_SOURCE, {
|
|
230
|
-
eval: true,
|
|
231
|
-
workerData: { catalogue: INLINE_EXTRACTOR_CATALOGUE },
|
|
232
|
-
});
|
|
233
|
-
inlineRegexWorker = createdWorker;
|
|
234
|
-
createdWorker.unref();
|
|
235
|
-
createdWorker.once('exit', () => {
|
|
236
|
-
if (inlineRegexWorker === createdWorker)
|
|
237
|
-
inlineRegexWorker = null;
|
|
238
|
-
});
|
|
239
|
-
createdWorker.once('error', () => {
|
|
240
|
-
if (inlineRegexWorker === createdWorker)
|
|
241
|
-
inlineRegexWorker = null;
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
inlineRegexWorker.postMessage({ value, control: controlBuffer, output: outputBuffer });
|
|
245
|
-
}
|
|
246
|
-
catch {
|
|
247
|
-
void inlineRegexWorker?.terminate().catch(() => { });
|
|
248
|
-
inlineRegexWorker = null;
|
|
249
|
-
return { timedOut: false, unavailable: true };
|
|
250
|
-
}
|
|
251
|
-
const wait = Atomics.wait(control, 0, 0, MAX_INLINE_LIBRARY_SCAN_MS);
|
|
252
|
-
const length = Atomics.load(control, 0);
|
|
253
|
-
if (wait === 'timed-out') {
|
|
254
|
-
void inlineRegexWorker?.terminate().catch(() => { });
|
|
255
|
-
inlineRegexWorker = null;
|
|
256
|
-
return { timedOut: true, unavailable: false };
|
|
257
|
-
}
|
|
258
|
-
if (length < 1 || length > MAX_INLINE_LIBRARY_WORKER_BYTES) {
|
|
259
|
-
return { timedOut: false, unavailable: true };
|
|
260
|
-
}
|
|
209
|
+
const result = await scanLibraryPatterns(JSON.stringify({ inline: value, references }), signal);
|
|
210
|
+
signal?.throwIfAborted();
|
|
211
|
+
if (result.output === null)
|
|
212
|
+
return result;
|
|
261
213
|
try {
|
|
262
|
-
const parsed = JSON.parse(
|
|
214
|
+
const parsed = JSON.parse(result.output);
|
|
263
215
|
if (!Array.isArray(parsed))
|
|
264
216
|
return { timedOut: false, unavailable: true };
|
|
265
|
-
for (const item of parsed.slice(0, MAX_LIBRARY_FINDINGS * 4)) {
|
|
266
|
-
if (!Array.isArray(item) || item.length !==
|
|
217
|
+
for (const item of parsed.slice(0, MAX_LIBRARY_FINDINGS * 4 * 3)) {
|
|
218
|
+
if (!Array.isArray(item) || item.length !== 3)
|
|
267
219
|
continue;
|
|
268
|
-
const [component, version] = item;
|
|
269
|
-
if (typeof component === 'string' && typeof version === 'string'
|
|
270
|
-
|
|
220
|
+
const [component, version, method] = item;
|
|
221
|
+
if (typeof component === 'string' && typeof version === 'string'
|
|
222
|
+
&& (method === 'inline signature' || method === 'script URL' || method === 'script filename')) {
|
|
223
|
+
addDetected(detected, component, version, method);
|
|
271
224
|
}
|
|
272
225
|
}
|
|
273
226
|
return { timedOut: false, unavailable: false };
|
|
@@ -311,13 +264,20 @@ function findingFromDetection(detected) {
|
|
|
311
264
|
let highestSeverity = null;
|
|
312
265
|
const identifiers = new Set();
|
|
313
266
|
const weaknesses = new Set();
|
|
267
|
+
let omittedCveIdentifiers = 0;
|
|
314
268
|
for (const vulnerability of vulnerabilities) {
|
|
315
269
|
const candidateSeverity = severity(vulnerability.severity);
|
|
316
270
|
if (candidateSeverity
|
|
317
271
|
&& (!highestSeverity || SEVERITY_WEIGHT[candidateSeverity] > SEVERITY_WEIGHT[highestSeverity]))
|
|
318
272
|
highestSeverity = candidateSeverity;
|
|
319
273
|
const vulnerabilityIdentifiers = record(vulnerability.identifiers);
|
|
320
|
-
|
|
274
|
+
omittedCveIdentifiers += typeof vulnerability.omittedCveIdentifiers === 'number'
|
|
275
|
+
&& Number.isSafeInteger(vulnerability.omittedCveIdentifiers) && vulnerability.omittedCveIdentifiers > 0
|
|
276
|
+
? vulnerability.omittedCveIdentifiers : 0;
|
|
277
|
+
const suppliedCve = Array.isArray(vulnerabilityIdentifiers.CVE) ? vulnerabilityIdentifiers.CVE : [];
|
|
278
|
+
const validCve = suppliedCve.filter(isCveIdentifier);
|
|
279
|
+
omittedCveIdentifiers += suppliedCve.length - validCve.length;
|
|
280
|
+
for (const cve of validCve.slice(0, 16)) {
|
|
321
281
|
if (identifiers.size < MAX_ADVISORY_IDENTIFIERS)
|
|
322
282
|
identifiers.add(cve);
|
|
323
283
|
}
|
|
@@ -332,7 +292,7 @@ function findingFromDetection(detected) {
|
|
|
332
292
|
}
|
|
333
293
|
const advisoryIdentifiers = [...identifiers].sort();
|
|
334
294
|
const knownExploitedIdentifiers = advisoryIdentifiers.filter((identifier) => KNOWN_EXPLOITED_IDENTIFIERS.has(identifier));
|
|
335
|
-
|
|
295
|
+
const finding = {
|
|
336
296
|
id: detected.component,
|
|
337
297
|
name: detected.component,
|
|
338
298
|
apparentVersion: detected.version,
|
|
@@ -344,8 +304,10 @@ function findingFromDetection(detected) {
|
|
|
344
304
|
knownExploitedIdentifiers,
|
|
345
305
|
weaknessClasses: [...weaknesses].sort(),
|
|
346
306
|
};
|
|
307
|
+
return { finding, omittedCveIdentifiers };
|
|
347
308
|
}
|
|
348
|
-
function analyzeBrowserLibraries(input = {}) {
|
|
309
|
+
async function analyzeBrowserLibraries(input = {}) {
|
|
310
|
+
input.signal?.throwIfAborted();
|
|
349
311
|
const htmlAnalysis = input.htmlAnalysis ?? analyzeStaticHtml(input.html);
|
|
350
312
|
const detected = new Map();
|
|
351
313
|
let referencesExamined = 0;
|
|
@@ -353,11 +315,11 @@ function analyzeBrowserLibraries(input = {}) {
|
|
|
353
315
|
let inlineSignatureCharactersExamined = 0;
|
|
354
316
|
let inlineSignatureLimitReached = false;
|
|
355
317
|
const inlineSignatureSamples = [];
|
|
318
|
+
const references = [];
|
|
356
319
|
for (const script of htmlAnalysis.scripts) {
|
|
357
320
|
if (script.reference) {
|
|
358
321
|
referencesExamined += 1;
|
|
359
|
-
|
|
360
|
-
scanFilename(detected, script.reference);
|
|
322
|
+
references.push(script.reference);
|
|
361
323
|
}
|
|
362
324
|
else if (script.inlineContent && script.mediaType !== 'application/ld+json') {
|
|
363
325
|
inlineScriptsExamined += 1;
|
|
@@ -369,10 +331,12 @@ function analyzeBrowserLibraries(input = {}) {
|
|
|
369
331
|
inlineSignatureSamples.push(signatureScan.signatureContent);
|
|
370
332
|
}
|
|
371
333
|
}
|
|
372
|
-
const inlineSignatureResult =
|
|
334
|
+
const inlineSignatureResult = await scanSignatures(detected, inlineSignatureSamples.join('\u0000'), references, input.signal);
|
|
335
|
+
input.signal?.throwIfAborted();
|
|
373
336
|
const inlineSignatureUnavailable = inlineSignatureResult.timedOut || inlineSignatureResult.unavailable;
|
|
374
|
-
const
|
|
375
|
-
|
|
337
|
+
const projected = [...detected.values()].map(findingFromDetection);
|
|
338
|
+
const omittedCveIdentifiers = projected.reduce((total, entry) => total + entry.omittedCveIdentifiers, 0);
|
|
339
|
+
const allFindings = projected.map((entry) => entry.finding)
|
|
376
340
|
.sort((left, right) => ((right.highestSeverity ? SEVERITY_WEIGHT[right.highestSeverity] : -1)
|
|
377
341
|
- (left.highestSeverity ? SEVERITY_WEIGHT[left.highestSeverity] : -1)
|
|
378
342
|
|| right.advisoryCount - left.advisoryCount
|
|
@@ -386,7 +350,8 @@ function analyzeBrowserLibraries(input = {}) {
|
|
|
386
350
|
|| htmlAnalysis.inlineLimitReached
|
|
387
351
|
|| inlineSignatureLimitReached
|
|
388
352
|
|| inlineSignatureUnavailable
|
|
389
|
-
|| findingLimitReached
|
|
353
|
+
|| findingLimitReached
|
|
354
|
+
|| omittedCveIdentifiers > 0;
|
|
390
355
|
const limitations = [
|
|
391
356
|
'Library versions are inferred from passive static signatures and may be absent, transformed, or misleading.',
|
|
392
357
|
'A catalogue advisory match identifies a component version associated with a published advisory; it does not establish reachability or exploitability.',
|
|
@@ -406,11 +371,13 @@ function analyzeBrowserLibraries(input = {}) {
|
|
|
406
371
|
if (inlineSignatureLimitReached)
|
|
407
372
|
limitations.push(`Passive library signature matching evaluated a deterministic ${MAX_INLINE_LIBRARY_SCAN_TOTAL_CHARS}-character cumulative sample across inline scripts; full-content hashes still used the complete bounded script content.`);
|
|
408
373
|
if (inlineSignatureResult.timedOut)
|
|
409
|
-
limitations.push(`Passive library signature matching exceeded its ${MAX_INLINE_LIBRARY_SCAN_MS} ms isolated-worker deadline; hash evidence was still evaluated.`);
|
|
374
|
+
limitations.push(`Passive library URL, filename and inline signature matching exceeded its ${MAX_INLINE_LIBRARY_SCAN_MS} ms isolated-worker deadline; hash evidence was still evaluated.`);
|
|
410
375
|
if (inlineSignatureResult.unavailable)
|
|
411
|
-
limitations.push('Passive library signature matching was unavailable in its isolated worker; hash evidence was still evaluated.');
|
|
376
|
+
limitations.push('Passive library URL, filename and inline signature matching was unavailable in its isolated worker; hash evidence was still evaluated.');
|
|
412
377
|
if (findingLimitReached)
|
|
413
378
|
limitations.push(`Only the first ${MAX_LIBRARY_FINDINGS} library findings were retained.`);
|
|
379
|
+
if (omittedCveIdentifiers)
|
|
380
|
+
limitations.push(`${omittedCveIdentifiers} supplied CVE identifier ${omittedCveIdentifiers === 1 ? 'entry was' : 'entries were'} omitted from matching catalogue advisories because of invalid syntax or a source limit. Advisory matches are still counted.`);
|
|
414
381
|
return {
|
|
415
382
|
profileVersion: BROWSER_LIBRARY_PROFILE_VERSION,
|
|
416
383
|
catalog: {
|
|
@@ -421,7 +388,7 @@ function analyzeBrowserLibraries(input = {}) {
|
|
|
421
388
|
knownExploitedCatalog: {
|
|
422
389
|
name: 'CISA KEV',
|
|
423
390
|
version: CISA_KEV_CATALOG.catalogVersion,
|
|
424
|
-
releasedAt:
|
|
391
|
+
releasedAt: KNOWN_EXPLOITED_RELEASED_AT,
|
|
425
392
|
},
|
|
426
393
|
...createObservation({
|
|
427
394
|
status: truncated ? 'partial' : 'success',
|
|
@@ -437,8 +404,10 @@ function analyzeBrowserLibraries(input = {}) {
|
|
|
437
404
|
inlineScriptsExamined,
|
|
438
405
|
inlineCharactersExamined: htmlAnalysis.inlineCharactersExamined,
|
|
439
406
|
inlineSignatureCharactersExamined,
|
|
440
|
-
inlineSignatureTimedOut: inlineSignatureResult.timedOut,
|
|
441
|
-
inlineSignatureUnavailable: inlineSignatureResult.unavailable,
|
|
407
|
+
inlineSignatureTimedOut: inlineSignatureSamples.length > 0 && inlineSignatureResult.timedOut,
|
|
408
|
+
inlineSignatureUnavailable: inlineSignatureSamples.length > 0 && inlineSignatureResult.unavailable,
|
|
409
|
+
referenceSignatureTimedOut: references.length > 0 && inlineSignatureResult.timedOut,
|
|
410
|
+
referenceSignatureUnavailable: references.length > 0 && inlineSignatureResult.unavailable,
|
|
442
411
|
catalogComponents: Object.keys(CATALOG_COMPONENTS).length,
|
|
443
412
|
findings: allFindings.length,
|
|
444
413
|
advisoryMatches: allFindings.reduce((sum, finding) => sum + finding.advisoryCount, 0),
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/** One bounded matcher worker. Queue deadlines include waiting; cancellation never stops a different scan. */
|
|
2
|
+
export function createInlineLibraryScanner(createWorker, limits) {
|
|
3
|
+
let worker = null;
|
|
4
|
+
let active = null;
|
|
5
|
+
let closing = false;
|
|
6
|
+
let nextId = 0;
|
|
7
|
+
const queue = [];
|
|
8
|
+
const pending = new Set();
|
|
9
|
+
function retireWorker() {
|
|
10
|
+
if (!worker)
|
|
11
|
+
return;
|
|
12
|
+
const retired = worker;
|
|
13
|
+
worker = null;
|
|
14
|
+
closing = true;
|
|
15
|
+
void Promise.resolve().then(() => retired.terminate()).catch(() => { }).finally(() => { closing = false; pump(); });
|
|
16
|
+
}
|
|
17
|
+
function pump() {
|
|
18
|
+
if (active || closing)
|
|
19
|
+
return;
|
|
20
|
+
const job = queue.shift();
|
|
21
|
+
if (!job) {
|
|
22
|
+
worker?.unref();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
active = job;
|
|
26
|
+
let current;
|
|
27
|
+
const control = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
|
|
28
|
+
const output = new Uint8Array(new SharedArrayBuffer(limits.outputBytes));
|
|
29
|
+
const cleanup = job.cleanup;
|
|
30
|
+
const failed = () => settle(job, { timedOut: false, unavailable: true, output: null }, true);
|
|
31
|
+
const message = (value) => {
|
|
32
|
+
if (!value || typeof value !== 'object' || !('id' in value) || value.id !== job.id)
|
|
33
|
+
return;
|
|
34
|
+
const length = Atomics.load(control, 0);
|
|
35
|
+
if (length < 1 || length > output.byteLength) {
|
|
36
|
+
failed();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
settle(job, { timedOut: false, unavailable: false, output: Buffer.from(output.subarray(0, length)).toString('utf8') });
|
|
40
|
+
};
|
|
41
|
+
job.cleanup = () => {
|
|
42
|
+
cleanup();
|
|
43
|
+
current?.off('message', message);
|
|
44
|
+
current?.off('error', failed);
|
|
45
|
+
current?.off('exit', failed);
|
|
46
|
+
};
|
|
47
|
+
try {
|
|
48
|
+
if (!worker) {
|
|
49
|
+
const created = createWorker();
|
|
50
|
+
worker = created;
|
|
51
|
+
const idleStopped = () => { if (!active && worker === created)
|
|
52
|
+
worker = null; };
|
|
53
|
+
created.on('error', idleStopped);
|
|
54
|
+
created.on('exit', idleStopped);
|
|
55
|
+
}
|
|
56
|
+
current = worker;
|
|
57
|
+
if (active !== job) {
|
|
58
|
+
retireWorker();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
current.ref();
|
|
62
|
+
current.on('message', message);
|
|
63
|
+
current.on('error', failed);
|
|
64
|
+
current.on('exit', failed);
|
|
65
|
+
current.postMessage({ id: job.id, value: job.value, control: control.buffer, output: output.buffer });
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
failed();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function settle(job, result, retire = false, aborted = false) {
|
|
72
|
+
if (!pending.delete(job))
|
|
73
|
+
return;
|
|
74
|
+
job.cleanup();
|
|
75
|
+
if (active === job) {
|
|
76
|
+
active = null;
|
|
77
|
+
if (retire)
|
|
78
|
+
retireWorker();
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
const index = queue.indexOf(job);
|
|
82
|
+
if (index >= 0)
|
|
83
|
+
queue.splice(index, 1);
|
|
84
|
+
}
|
|
85
|
+
if (aborted)
|
|
86
|
+
job.reject(job.signal?.reason ?? new DOMException('Aborted', 'AbortError'));
|
|
87
|
+
else
|
|
88
|
+
job.resolve(result);
|
|
89
|
+
queueMicrotask(pump);
|
|
90
|
+
}
|
|
91
|
+
return function scan(value, signal) {
|
|
92
|
+
signal?.throwIfAborted();
|
|
93
|
+
if (!value)
|
|
94
|
+
return Promise.resolve({ timedOut: false, unavailable: false, output: '[]' });
|
|
95
|
+
if (value.length > limits.maximumCharacters || pending.size >= limits.pending) {
|
|
96
|
+
return Promise.resolve({ timedOut: false, unavailable: true, output: null });
|
|
97
|
+
}
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
const job = { id: ++nextId, value, ...(signal ? { signal } : {}), resolve, reject, cleanup: () => { } };
|
|
100
|
+
const timer = setTimeout(() => settle(job, { timedOut: true, unavailable: false, output: null }, true), limits.deadlineMs);
|
|
101
|
+
const abort = () => settle(job, { timedOut: false, unavailable: true, output: null }, true, true);
|
|
102
|
+
job.cleanup = () => { clearTimeout(timer); signal?.removeEventListener('abort', abort); };
|
|
103
|
+
pending.add(job);
|
|
104
|
+
queue.push(job);
|
|
105
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
106
|
+
if (signal?.aborted)
|
|
107
|
+
abort();
|
|
108
|
+
else
|
|
109
|
+
pump();
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
}
|
package/lib/classify.mjs
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { isIP } from 'node:net';
|
|
17
17
|
import { domainToASCII } from 'node:url';
|
|
18
18
|
import { parse } from 'tldts';
|
|
19
|
-
import { canonicalRegistrableDomain } from "
|
|
19
|
+
import { canonicalRegistrableDomain } from "../packages/analysis/registrable-domain.mjs";
|
|
20
20
|
// A CR/LF here would let one query become multiple lines once it reaches
|
|
21
21
|
// lib/whois.mts's raw `socket.write(query + '\r\n')` - a normal URL-encoded
|
|
22
22
|
// query string (?q=example.com%0D%0AHELP) decodes back into literal control
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Static client-side behaviour indicators derived from the existing bounded
|
|
2
|
-
// HTML
|
|
2
|
+
// HTML parse. Scripts are never fetched or executed. Only fixed
|
|
3
3
|
// indicator identifiers, descriptions, evidence classes, and bounded counts
|
|
4
4
|
// leave this module; script references and contents are discarded.
|
|
5
5
|
import { createObservation } from "../packages/evidence/observation.mjs";
|
|
@@ -20,7 +20,6 @@ function patternCount(value, pattern) {
|
|
|
20
20
|
}
|
|
21
21
|
function analyzeClientBehavior(input = {}) {
|
|
22
22
|
const htmlAnalysis = input.htmlAnalysis ?? analyzeStaticHtml(input.html);
|
|
23
|
-
const markup = htmlAnalysis.markup;
|
|
24
23
|
const inlineScripts = htmlAnalysis.scripts
|
|
25
24
|
.filter((script) => script.reference === null)
|
|
26
25
|
.map((script) => script.inlineContent)
|
|
@@ -31,7 +30,8 @@ function analyzeClientBehavior(input = {}) {
|
|
|
31
30
|
return;
|
|
32
31
|
indicators.push({ id, label, evidenceClass, occurrences: boundedOccurrences(occurrences), explanation });
|
|
33
32
|
}
|
|
34
|
-
add('inline_event_handlers', 'Inline event handlers', 'static_markup',
|
|
33
|
+
add('inline_event_handlers', 'Inline event handlers', 'static_markup', htmlAnalysis.elements.reduce((count, element) => count
|
|
34
|
+
+ element.attributes.filter((attribute) => /^on[a-z]{2,24}$/u.test(attribute.name)).length, 0), 'Inline HTML event-handler attributes were observed.');
|
|
35
35
|
add('client_navigation', 'Client-side navigation', 'inline_script', patternCount(inlineScripts, /\b(?:history\.(?:pushState|replaceState)|location\.(?:assign|replace)|window\.location)\b/gu), 'Inline script contains a client-side navigation API.');
|
|
36
36
|
add('form_interception', 'Form interception', 'inline_script', patternCount(inlineScripts, /(?:addEventListener\s*\(\s*['"]submit['"]|\.onsubmit\s*=|preventDefault\s*\()/gu), 'Inline script contains a form-submission or default-action interception marker.');
|
|
37
37
|
add('service_worker', 'Service worker registration', 'inline_script', patternCount(inlineScripts, /\bnavigator\.serviceWorker\.register\s*\(/gu), 'Inline script contains a service-worker registration call.');
|
package/lib/ct-query.mjs
CHANGED
|
@@ -1,27 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const CT_QUERY_ERROR_MESSAGE = `Certificate Transparency query must be at most ${MAX_CT_QUERY_LENGTH} characters and contain no control or invisible formatting characters.`;
|
|
4
|
-
const UNSAFE_QUERY_CHARACTER_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/u;
|
|
5
|
-
class CtQueryError extends Error {
|
|
6
|
-
code = CT_QUERY_ERROR_CODE;
|
|
7
|
-
constructor() {
|
|
8
|
-
super(CT_QUERY_ERROR_MESSAGE);
|
|
9
|
-
this.name = 'CtQueryError';
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
function normalizeCtQuery(value) {
|
|
13
|
-
if (value === undefined || value === null)
|
|
14
|
-
return '';
|
|
15
|
-
if (typeof value !== 'string'
|
|
16
|
-
|| value.length > MAX_CT_QUERY_LENGTH
|
|
17
|
-
|| UNSAFE_QUERY_CHARACTER_RE.test(value)) {
|
|
18
|
-
throw new CtQueryError();
|
|
19
|
-
}
|
|
20
|
-
return value.trim();
|
|
21
|
-
}
|
|
22
|
-
function isCtQueryError(value) {
|
|
23
|
-
return value instanceof Error
|
|
24
|
-
&& 'code' in value
|
|
25
|
-
&& value.code === CT_QUERY_ERROR_CODE;
|
|
26
|
-
}
|
|
27
|
-
export { CT_QUERY_ERROR_CODE, CT_QUERY_ERROR_MESSAGE, CtQueryError, isCtQueryError, MAX_CT_QUERY_LENGTH, normalizeCtQuery, };
|
|
1
|
+
// Retained public import; the runtime-neutral implementation lives in the domain layer.
|
|
2
|
+
export * from "../packages/analysis/ct-query.mjs";
|
|
@@ -1,9 +1,2 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
// not accidentally clipped by a stale client-side mirror.
|
|
4
|
-
export const MAX_CT_RESPONSE_RESULTS = 500;
|
|
5
|
-
export const MAX_CT_RESPONSE_HOSTNAMES_PER_MATCH = 50;
|
|
6
|
-
export const MAX_CT_RESPONSE_TIMESTAMP_LENGTH = 64;
|
|
7
|
-
export const MAX_CT_RESPONSE_CERTIFICATE_GROUPS = 100;
|
|
8
|
-
export const MAX_CT_RESPONSE_DOMAINS_PER_GROUP = 20;
|
|
9
|
-
export const MAX_CT_RESPONSE_HOSTNAMES_PER_GROUP = 30;
|
|
1
|
+
// Retained public import; the runtime-neutral implementation lives in the domain layer.
|
|
2
|
+
export * from "../packages/analysis/ct-response-bounds.mjs";
|