@slicedearth/whoisleuth-cli 2.4.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DISCLOSURE +1 -1
- package/NOTICE +2 -2
- package/cli/argument-values.mjs +55 -0
- package/cli/arguments.mjs +24 -470
- package/cli/artifact-validation/case-response.mjs +67 -9
- package/cli/artifact-validation/lookup-evidence.mjs +18 -4
- package/cli/artifact-validation/signed-review.mjs +61 -64
- package/cli/artifact-verify.mjs +11 -2
- package/cli/assurance-arguments.mjs +56 -0
- package/cli/assurance-command-definitions.mjs +179 -0
- package/cli/assurance-command-runner.mjs +1 -1
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +1 -1
- package/cli/bulk-command-runner.mjs +6 -8
- package/cli/bulk.mjs +2 -3
- package/cli/case-command.mjs +99 -6
- package/cli/case-pack.mjs +19 -4
- package/cli/ci-report.mjs +6 -3
- package/cli/collection-arguments.mjs +123 -0
- package/cli/collection-command-definitions.mjs +330 -0
- package/cli/collection-preflight.mjs +8 -1
- package/cli/command-argument-grammar.mjs +8 -210
- package/cli/command-definition.mjs +330 -0
- package/cli/command-reference.mjs +55 -1388
- package/cli/command-seeds.mjs +18 -0
- package/cli/compare.mjs +1 -1
- package/cli/config-profile.mjs +1 -1
- package/cli/ct-event-intake.mjs +4 -4
- package/cli/discover.mjs +2 -2
- package/cli/discovery-observation-snapshot.mjs +2 -2
- package/cli/discovery-scan-command-runner.mjs +7 -9
- package/cli/discovery-scan.mjs +1 -1
- package/cli/discovery-snapshot.mjs +2 -2
- package/cli/discovery-workflow.mjs +4 -4
- package/cli/domain-control-monitor.mjs +6 -6
- package/cli/domain-control-observations.mjs +2 -1
- package/cli/domain-feed.mjs +61 -0
- package/cli/errors.mjs +5 -6
- package/cli/evidence-arguments.mjs +46 -0
- package/cli/evidence-command-definitions.mjs +69 -0
- package/cli/evidence-command-runner.mjs +2 -3
- package/cli/evidence-signing.mjs +3 -2
- package/cli/external-observation-mapping.mjs +2 -2
- package/cli/formatters/json.mjs +1 -13
- package/cli/formatters/terminal-command-formats.mjs +6 -0
- package/cli/formatters/terminal-lookup.mjs +6 -0
- package/cli/history-arguments.mjs +39 -0
- package/cli/history-command-definitions.mjs +88 -0
- package/cli/indicator-set-command.mjs +32 -0
- package/cli/inline-command-families.mjs +6 -47
- package/cli/inline-command-runner.mjs +4 -31
- package/cli/input.mjs +2 -2
- package/cli/intake-command.mjs +147 -0
- package/cli/interchange-report.mjs +4 -2
- package/cli/investigation-artifacts.mjs +3 -4
- package/cli/investigation-package-review.mjs +2 -0
- package/cli/investigation-plan.mjs +32 -154
- package/cli/investigation-recipes.mjs +177 -0
- package/cli/investigation-run.mjs +78 -12
- package/cli/local-mmdb-review.mjs +220 -17
- package/cli/lookalike-calibration.mjs +1 -1
- package/cli/lookup-brief.mjs +2 -2
- package/cli/lookup-command-runner.mjs +19 -35
- package/cli/lookup-diff.mjs +10 -9
- package/cli/lookup-plan.mjs +10 -14
- package/cli/lookup-reconcile.mjs +6 -5
- package/cli/lookup-timeline.mjs +7 -6
- package/cli/mail-header-review.mjs +24 -247
- package/cli/mail-review.mjs +9 -7
- package/cli/network-arguments.mjs +51 -0
- package/cli/network-command-definitions.mjs +130 -0
- package/cli/network-command-runner.mjs +43 -31
- package/cli/offline-evidence-review.mjs +85 -6
- package/cli/posture.mjs +1 -1
- package/cli/registry-fixture-scaffold.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +8 -7
- package/cli/review-arguments.mjs +132 -0
- package/cli/review-command-definitions.mjs +279 -0
- package/cli/review-command-runner.mjs +72 -159
- package/cli/risk-calibration.mjs +14 -12
- package/cli/runner.mjs +20 -32
- package/cli/saved-lookup.mjs +2 -2
- package/cli/selected-input-worker.mjs +73 -0
- package/cli/sharing-review.mjs +5 -64
- package/cli/source-reliability.mjs +2 -2
- package/cli/support-arguments.mjs +49 -0
- package/cli/support-command-definitions.mjs +209 -0
- package/cli/support-command-runner.mjs +4 -4
- package/cli/watchlist-review.mjs +43 -0
- package/cli/workflow-arguments.mjs +100 -0
- package/cli/workflow-command-definitions.mjs +103 -0
- package/cli/workflow-command-runner.mjs +40 -1
- package/docs/cli-reference.md +27 -0
- package/docs/cli.md +322 -4
- package/docs/contextual-reviews.md +178 -0
- package/docs/infrastructure-observations.md +126 -0
- package/docs/offline-intake-context.md +213 -0
- package/lib/analyst-taxonomy.mjs +2 -40
- package/lib/application-version.mjs +1 -1
- package/lib/availability.mjs +13 -789
- package/lib/bounded-contract-normalizers.mjs +4 -0
- package/lib/bounded-json.mjs +2 -330
- package/lib/browser-library-profile.mjs +62 -84
- package/lib/candidate-provenance-bounds.mjs +2 -0
- package/lib/classify.mjs +1 -1
- package/lib/credential-form-attribution.mjs +136 -0
- package/lib/credential-surface-profile.mjs +6 -2
- package/lib/ct-query.mjs +2 -27
- package/lib/ct-response-bounds.mjs +2 -9
- package/lib/ct-search.mjs +83 -47
- package/lib/dmarc-discovery.mjs +62 -0
- package/lib/dns-convergence-review.mjs +21 -8
- package/lib/dns-delegation-health.mjs +35 -15
- package/lib/dns-inheritance-review.mjs +53 -63
- package/lib/dns-intelligence.mjs +27 -11
- package/lib/dns-query-coverage.mjs +40 -0
- package/lib/domain-assurance.mjs +1 -1
- package/lib/domain-change-packet.mjs +12 -13
- package/lib/domain-change-review.mjs +32 -14
- package/lib/domain-control-flight-recorder.mjs +103 -27
- package/lib/domain-control-manifest.mjs +25 -17
- package/lib/domain-posture-analysis.mjs +75 -7
- package/lib/domain-posture-parsers.mjs +9 -0
- package/lib/domain-posture.mjs +45 -25
- package/lib/domain-registration.mjs +392 -0
- package/lib/evidence-export-privacy.mjs +27 -17
- package/lib/evidence-export.mjs +3 -3
- package/lib/evidence-report.mjs +5 -1
- package/lib/external-intelligence-risk.mjs +2 -124
- package/lib/favicon.mjs +13 -4
- package/lib/generated/cisa-kev-catalog.mjs +41 -3
- package/lib/generated/registrar-standing-catalogue.mjs +18 -10
- package/lib/generated/retire-browser-catalog.mjs +332 -19
- package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
- package/lib/html-signals.mjs +13 -2
- package/lib/http-evidence-bounds.mjs +2 -8
- package/lib/http-intelligence.mjs +13 -1
- package/lib/idn-confusable-policy.mjs +6 -4
- package/lib/idn-confusables.mjs +1 -1
- package/lib/interchange-fidelity-registry.mjs +10 -0
- package/lib/lookup-child-profile-contract.mjs +28 -11
- package/lib/lookup-contract-primitives.mjs +1 -1
- package/lib/lookup-diagnostics.mjs +1 -23
- package/lib/lookup-network-evidence-bounds.mjs +1 -10
- package/lib/lookup-progress.mjs +50 -1
- package/lib/lookup-response-contract.mjs +70 -40
- package/lib/lookup-response.mjs +5 -15
- package/lib/lookup-source-progress.mjs +8 -22
- package/lib/lookup.mjs +1 -1
- package/lib/operation-budget.mjs +4 -0
- package/lib/opportunity-scoring.mjs +2 -107
- package/lib/outbound-identity.mjs +1 -1
- package/lib/page-fingerprints.mjs +18 -1
- package/lib/perceptual-hash-comparison.mjs +2 -34
- package/lib/perceptual-hash.mjs +36 -10
- package/lib/portable-generator.mjs +2 -29
- package/lib/project-metadata.mjs +2 -8
- package/lib/rdap-client.mjs +11 -7
- package/lib/registrable-domain.mjs +2 -56
- package/lib/registrar-standing-contract.mjs +1 -1
- package/lib/registrar-standing.mjs +9 -13
- package/lib/registry-capabilities.mjs +2 -2
- package/lib/registry-capability-catalogue.mjs +27 -19
- package/lib/risk-calibration-summary.mjs +2 -352
- package/lib/risk-scoring.mjs +2 -345
- package/lib/scoring-evidence-quality.mjs +2 -107
- package/lib/semantic-version.mjs +2 -54
- package/lib/service-binding-dns.mjs +46 -2
- package/lib/smtp-transport-review.mjs +48 -19
- package/lib/sslbl-certificates.generated.mjs +142 -139
- package/lib/static-html-analysis.mjs +32 -4
- package/lib/structured-data-identity.mjs +1 -1
- package/lib/technology-evidence-role.mjs +1 -51
- package/lib/threat-intelligence-manifests.mjs +1 -1
- package/lib/threat-intelligence-runtime.mjs +1 -1
- package/lib/threat-intelligence-targets.mjs +1 -1
- package/lib/threat-intelligence-types.mjs +2 -75
- package/lib/tls-intelligence.mjs +30 -5
- package/lib/typosquat-generator.mjs +11 -2
- package/lib/website-enrichment.mjs +404 -0
- package/lib/whois-authority.mjs +43 -5
- package/lib/whois-chain.mjs +12 -3
- package/lib/whois-common-formats.mjs +3 -1
- package/lib/whois-contacts.mjs +13 -2
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-parser.mjs +5 -3
- package/package.json +15 -14
- package/packages/analysis/analyst-taxonomy.mjs +40 -0
- package/packages/analysis/bounded-json.mjs +330 -0
- package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
- package/packages/analysis/cli-argument-grammar.mjs +216 -0
- package/packages/analysis/ct-query.mjs +27 -0
- package/packages/analysis/ct-response-bounds.mjs +9 -0
- package/packages/analysis/external-intelligence-risk.mjs +124 -0
- package/packages/analysis/http-evidence-bounds.mjs +8 -0
- package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
- package/packages/analysis/opportunity-scoring.mjs +107 -0
- package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
- package/packages/analysis/portable-generator.mjs +29 -0
- package/packages/analysis/project-metadata.mjs +8 -0
- package/packages/analysis/registrable-domain.mjs +56 -0
- package/packages/analysis/risk-calibration-summary.mjs +352 -0
- package/packages/analysis/risk-scoring.mjs +348 -0
- package/packages/analysis/scoring-evidence-quality.mjs +107 -0
- package/packages/analysis/semantic-version.mjs +54 -0
- package/packages/analysis/threat-intelligence-types.mjs +75 -0
- package/packages/cases/case-containment-handoff.mjs +195 -0
- package/packages/cases/case-evidence-links.mjs +151 -0
- package/packages/cases/case-evidence-model.mjs +89 -12
- package/packages/cases/case-evidence-quality.mjs +5 -0
- package/packages/cases/case-export-input.mjs +7 -5
- package/packages/cases/case-incident-context.mjs +52 -0
- package/packages/cases/case-markdown.mjs +11 -0
- package/packages/cases/case-migration-model.mjs +40 -24
- package/packages/cases/case-object-coverage.mjs +33 -0
- package/packages/cases/case-packet-correction.mjs +106 -0
- package/packages/cases/case-recheck-model.mjs +124 -36
- package/packages/cases/case-record-contracts.mjs +1 -1
- package/packages/cases/case-record-core.mjs +1 -1
- package/packages/cases/case-record-operations.mjs +111 -118
- package/packages/cases/case-record-projection.mjs +23 -4
- package/packages/cases/case-record-version-input.mjs +41 -0
- package/packages/cases/case-report.mjs +93 -27
- package/packages/cases/case-requested-evidence.mjs +146 -0
- package/packages/cases/case-response-actions.mjs +159 -1
- package/packages/cases/case-response-model.mjs +16 -1
- package/packages/cases/case-response-object.mjs +119 -0
- package/packages/cases/case-response-outcomes.mjs +108 -20
- package/packages/cases/case-response-packet-render.mjs +35 -12
- package/packages/cases/case-response-packet.mjs +55 -13
- package/packages/cases/case-response-records.mjs +0 -1
- package/packages/cases/case-response-review-inputs.mjs +60 -8
- package/packages/cases/case-storage-model.mjs +1 -1
- package/packages/cases/case-workflow-metadata.mjs +217 -0
- package/packages/cases/case-workflow-migration.mjs +43 -0
- package/packages/cases/http-summary.mjs +6 -1
- package/packages/comparison/favicon-similarity.mjs +1 -1
- package/packages/comparison/page-similarity.mjs +1 -1
- package/packages/comparison/relationship-evidence.mjs +21 -9
- package/packages/comparison/relationship-provenance.mjs +7 -1
- package/packages/contracts/analyst-interchange.mjs +35 -15
- package/packages/contracts/candidate-watch-review.mjs +54 -0
- package/packages/contracts/case-portability.mjs +360 -49
- package/packages/contracts/cli-command-semantics.mjs +5 -0
- package/packages/contracts/cli-grammar.mjs +6 -0
- package/packages/contracts/cli-lookup-brief.mjs +2 -0
- package/packages/contracts/context-review.mjs +25 -0
- package/packages/contracts/document-intake.mjs +11 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +30 -14
- package/packages/contracts/domain-control-manifest.mjs +1 -1
- package/packages/contracts/domain-control-monitor.mjs +13 -2
- package/packages/contracts/domain-control-review.mjs +36 -15
- package/packages/contracts/external-observation-interchange.mjs +96 -28
- package/packages/contracts/har-review.mjs +2 -0
- package/packages/contracts/identity-events.mjs +4 -0
- package/packages/contracts/investigation-run.mjs +2 -0
- package/packages/contracts/ip-address.mjs +10 -0
- package/packages/contracts/lookup-evidence.mjs +3 -2
- package/packages/contracts/mail-authentication.mjs +5 -0
- package/packages/contracts/message-intake.mjs +37 -0
- package/packages/contracts/offline-comparison.mjs +4 -1
- package/packages/contracts/passive-mail.mjs +10 -0
- package/packages/contracts/risk-calibration.mjs +44 -29
- package/packages/contracts/schema-lifecycle.mjs +3 -1
- package/packages/contracts/web-capture.mjs +2 -2
- package/packages/contracts/workspace-portability.mjs +144 -28
- package/packages/evidence/collection-quality.mjs +67 -0
- package/packages/evidence/credential-form-attribution.mjs +60 -0
- package/packages/evidence/dns-query-outcome.mjs +72 -0
- package/packages/evidence/email-recipient.mjs +18 -0
- package/packages/evidence/image-regions.mjs +15 -0
- package/packages/evidence/lookup-target.mjs +5 -1
- package/packages/evidence/observation.mjs +2 -0
- package/packages/evidence/png-pixels.mjs +117 -0
- package/packages/evidence/public-address-policy.mjs +83 -0
- package/packages/evidence/record-identity.mjs +60 -16
- package/packages/evidence/technology-evidence-role.mjs +51 -0
- package/packages/interchange/defensive-indicator-export.mjs +264 -0
- package/packages/interchange/external-findings-import.mjs +36 -29
- package/packages/interchange/json-output.mjs +14 -0
- package/packages/interchange/managed-indicator-export.mjs +36 -0
- package/packages/interchange/managed-indicator-set.mjs +223 -0
- package/packages/interchange/misp-indicator-export.mjs +93 -0
- package/packages/interchange/sharing-policy.mjs +56 -0
- package/packages/interchange/stix-indicator-export.mjs +129 -0
- package/packages/interchange/web-capture-import.mjs +22 -15
- package/packages/investigation/acquisition-decision-packet.mjs +79 -1
- package/packages/investigation/bulk-mail-exposure.mjs +7 -13
- package/packages/investigation/capture-coverage.mjs +57 -0
- package/packages/investigation/connector-provenance-review.mjs +100 -0
- package/packages/investigation/context-review.mjs +35 -0
- package/packages/investigation/document-intake.mjs +87 -0
- package/packages/investigation/docx-intake.mjs +133 -0
- package/packages/investigation/domain-history-review.mjs +132 -0
- package/packages/investigation/domain-transition-review.mjs +49 -0
- package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
- package/packages/investigation/har-intake.mjs +68 -0
- package/packages/investigation/identity-event-intake.mjs +131 -0
- package/packages/investigation/identity-incident-review.mjs +39 -0
- package/packages/investigation/incident-sequence-review.mjs +58 -0
- package/packages/investigation/infrastructure-observation.mjs +234 -0
- package/packages/investigation/intake-context.mjs +284 -0
- package/packages/investigation/intake-indicators.mjs +116 -0
- package/packages/investigation/intake-phones.mjs +155 -0
- package/packages/investigation/intake-report.mjs +20 -0
- package/packages/investigation/investigation-manifest.mjs +35 -9
- package/packages/investigation/link-intake.mjs +209 -0
- package/packages/investigation/lookup-asset-graph.mjs +1 -1
- package/packages/investigation/mail-authentication-review.mjs +222 -0
- package/packages/investigation/mail-header-identity.mjs +209 -0
- package/packages/investigation/message-intake.mjs +235 -0
- package/packages/investigation/page-behaviour.mjs +113 -0
- package/packages/investigation/pdf-intake.mjs +155 -0
- package/packages/investigation/platform-continuity-review.mjs +60 -0
- package/packages/investigation/qr-intake.mjs +56 -0
- package/packages/investigation/requested-action-hints.mjs +11 -0
- package/packages/investigation/selected-input-review.mjs +17 -0
- package/packages/investigation/storefront-review.mjs +70 -0
- package/packages/monitoring/domain-feed.mjs +321 -0
- package/packages/workspace/brand-candidate-workflow.mjs +351 -0
- package/packages/workspace/brand-keyword-campaign.mjs +231 -0
- package/packages/workspace/brand-profile-model.mjs +48 -4
- package/packages/workspace/bulk-review-model.mjs +28 -26
- package/packages/workspace/bulk-session-model.mjs +27 -13
- package/packages/workspace/campaign-model.mjs +1 -1
- package/packages/workspace/candidate-watch-handoff.mjs +195 -0
- package/packages/workspace/detection-rule-model.mjs +11 -3
- package/packages/workspace/hostile-input.mjs +1 -1
- package/packages/workspace/investigation-guide.mjs +2 -2
- package/packages/workspace/investigation-template-model.mjs +26 -2
- package/packages/workspace/page-baseline.mjs +2 -2
- package/packages/workspace/relationship-observation-model.mjs +17 -13
- package/packages/workspace/shortlist-model.mjs +24 -7
- package/packages/workspace/watchlist-history.mjs +59 -16
- package/packages/workspace/watchlist-store.mjs +152 -11
- package/packages/workspace/website-snapshot-model.mjs +28 -3
- package/packages/workspace/workspace-archive-crypto.mjs +1 -1
- package/packages/workspace/workspace-archive.mjs +5 -4
- package/third-party-notices.txt +549 -16
- package/lib/hostname.mjs +0 -2
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Deliberately small interoperability projection over reviewed MISP
|
|
2
|
+
// taxonomies. WHOISleuth retains its own bounded workflow vocabulary and
|
|
3
|
+
// derives machine tags only when an analyst-owned disposition and reason make
|
|
4
|
+
// the mapping unambiguous. These tags never alter collection or scoring.
|
|
5
|
+
const ANALYST_REVIEW_REASONS = Object.freeze([
|
|
6
|
+
Object.freeze({ value: '', label: 'Not recorded' }),
|
|
7
|
+
Object.freeze({ value: 'authorized_or_owned', label: 'Authorised or owned domain' }),
|
|
8
|
+
Object.freeze({ value: 'shared_infrastructure', label: 'Shared infrastructure explained the signal' }),
|
|
9
|
+
Object.freeze({ value: 'generic_platform_or_template', label: 'Generic platform or template' }),
|
|
10
|
+
Object.freeze({ value: 'parked_or_reseller', label: 'Parking or reseller page' }),
|
|
11
|
+
Object.freeze({ value: 'insufficient_evidence', label: 'Insufficient evidence' }),
|
|
12
|
+
Object.freeze({ value: 'legitimate_third_party', label: 'Legitimate third party' }),
|
|
13
|
+
Object.freeze({ value: 'confirmed_credential_abuse', label: 'Confirmed credential abuse' }),
|
|
14
|
+
Object.freeze({ value: 'confirmed_malware', label: 'Confirmed malware' }),
|
|
15
|
+
Object.freeze({ value: 'other_reviewed', label: 'Other reviewed reason' }),
|
|
16
|
+
]);
|
|
17
|
+
const ANALYST_REVIEW_REASON_VALUES = new Set(ANALYST_REVIEW_REASONS.map(({ value }) => value).filter(Boolean));
|
|
18
|
+
const MISP_TAXONOMY_REFERENCES = Object.freeze([
|
|
19
|
+
Object.freeze({ namespace: 'domain-abuse', version: 2, sourceSha256: '5fcfb44742daac97061726faaaadd2ce6b7eac55306acaae3d30c3882d18f991', sourceUrl: 'https://github.com/MISP/misp-taxonomies/blob/main/domain-abuse/machinetag.json' }),
|
|
20
|
+
Object.freeze({ namespace: 'estimative-language', version: 5, sourceSha256: '1010927d396f66f7cc58a229ebb8af6ad2cdc0250ad5f8ce3b1bd9aacf5ab4da', sourceUrl: 'https://github.com/MISP/misp-taxonomies/blob/main/estimative-language/machinetag.json' }),
|
|
21
|
+
Object.freeze({ namespace: 'event-assessment', version: 2, sourceSha256: '39291bdd50e1b2975a93e33e396f42b398178015580a84402bc3d8d549dd721e', sourceUrl: 'https://github.com/MISP/misp-taxonomies/blob/main/event-assessment/machinetag.json' }),
|
|
22
|
+
Object.freeze({ namespace: 'false-positive', version: 7, sourceSha256: 'ffc8d35ae36eb1d39f33611d1090793021f2bfe3b4b31ae41631e22cf544a2fb', sourceUrl: 'https://github.com/MISP/misp-taxonomies/blob/main/false-positive/machinetag.json' }),
|
|
23
|
+
Object.freeze({ namespace: 'phishing', version: 5, sourceSha256: '8628ebe4d5935216a7b006a4d975669c4ae1a554ef8ed78ffaadd62f763d7263', sourceUrl: 'https://github.com/MISP/misp-taxonomies/blob/main/phishing/machinetag.json' }),
|
|
24
|
+
]);
|
|
25
|
+
function analystInteroperabilityTags(disposition, reviewReasonCode) {
|
|
26
|
+
const tags = new Set();
|
|
27
|
+
if (disposition === 'false_positive')
|
|
28
|
+
tags.add('false-positive:confirmed="true"');
|
|
29
|
+
if ((disposition === 'false_positive' || disposition === 'expected')
|
|
30
|
+
&& ['authorized_or_owned', 'shared_infrastructure', 'generic_platform_or_template', 'parked_or_reseller', 'legitimate_third_party'].includes(String(reviewReasonCode)))
|
|
31
|
+
tags.add('false-positive:risk="high"');
|
|
32
|
+
if (reviewReasonCode === 'insufficient_evidence') {
|
|
33
|
+
tags.add('estimative-language:confidence-in-analytic-judgment="low"');
|
|
34
|
+
}
|
|
35
|
+
if (disposition === 'confirmed_abuse' && reviewReasonCode === 'confirmed_credential_abuse') {
|
|
36
|
+
tags.add('phishing:techniques="fake-website"');
|
|
37
|
+
}
|
|
38
|
+
return [...tags].sort();
|
|
39
|
+
}
|
|
40
|
+
export { ANALYST_REVIEW_REASONS, ANALYST_REVIEW_REASON_VALUES, MISP_TAXONOMY_REFERENCES, analystInteroperabilityTags, };
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
export const MAX_BOUNDED_JSON_DEPTH = 48;
|
|
2
|
+
export const MAX_BOUNDED_JSON_KEYS = 50_000;
|
|
3
|
+
export const MAX_BOUNDED_JSON_VALUES = 100_000;
|
|
4
|
+
export const MAX_BOUNDED_JSON_CONTAINER_ITEMS = 10_000;
|
|
5
|
+
export const UNSAFE_JSON_OBJECT_KEYS = Object.freeze(['__proto__']);
|
|
6
|
+
/** Aggregate work allowed by a separately enforced UTF-8 byte budget. */
|
|
7
|
+
export function boundedJsonLimitsForBytes(maximumBytes) {
|
|
8
|
+
if (!Number.isSafeInteger(maximumBytes) || maximumBytes < 1) {
|
|
9
|
+
throw new TypeError('The JSON byte budget must be a positive safe integer.');
|
|
10
|
+
}
|
|
11
|
+
// A member needs at least five bytes (comma, two quotes, colon, value).
|
|
12
|
+
// Values need at least two bytes each, apart from a scalar root or the last
|
|
13
|
+
// array item. These upper bounds do not depend on a model's current fields.
|
|
14
|
+
// Nesting and individual-container limits remain independently bounded.
|
|
15
|
+
return {
|
|
16
|
+
maximumKeys: Math.floor(maximumBytes / 5),
|
|
17
|
+
maximumValues: Math.ceil(maximumBytes / 2),
|
|
18
|
+
// Decoded text is a lower bound on its UTF-8 JSON representation. Checking
|
|
19
|
+
// it before stringify also prevents an oversized string allocation.
|
|
20
|
+
maximumStringCodeUnits: maximumBytes,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function syntaxError() {
|
|
24
|
+
throw new TypeError('Artefact input is not valid JSON.');
|
|
25
|
+
}
|
|
26
|
+
export function isSafeJsonObjectKey(value) {
|
|
27
|
+
return !UNSAFE_JSON_OBJECT_KEYS.includes(value);
|
|
28
|
+
}
|
|
29
|
+
export function assertBoundedJsonStructure(value, label = 'JSON value', limits = {}) {
|
|
30
|
+
const maximumDepth = limits.maximumDepth ?? MAX_BOUNDED_JSON_DEPTH;
|
|
31
|
+
const maximumKeys = limits.maximumKeys ?? MAX_BOUNDED_JSON_KEYS;
|
|
32
|
+
const maximumValues = limits.maximumValues ?? MAX_BOUNDED_JSON_VALUES;
|
|
33
|
+
const maximumContainerItems = limits.maximumContainerItems ?? MAX_BOUNDED_JSON_CONTAINER_ITEMS;
|
|
34
|
+
let keys = 0;
|
|
35
|
+
let values = 0;
|
|
36
|
+
let stringCodeUnits = 0;
|
|
37
|
+
const ancestors = new Set();
|
|
38
|
+
const text = (value) => {
|
|
39
|
+
stringCodeUnits += value.length;
|
|
40
|
+
if (limits.maximumStringCodeUnits !== undefined && stringCodeUnits > limits.maximumStringCodeUnits) {
|
|
41
|
+
throw new TypeError(`${label} exceeds the aggregate text limit.`);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const visit = (current, depth) => {
|
|
45
|
+
values += 1;
|
|
46
|
+
if (values > maximumValues) {
|
|
47
|
+
throw new TypeError(`${label} exceeds the ${maximumValues}-value limit.`);
|
|
48
|
+
}
|
|
49
|
+
if (depth > maximumDepth) {
|
|
50
|
+
throw new TypeError(`${label} exceeds the ${maximumDepth}-level nesting limit.`);
|
|
51
|
+
}
|
|
52
|
+
if (typeof current === 'string') {
|
|
53
|
+
text(current);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (current === null || typeof current === 'boolean')
|
|
57
|
+
return;
|
|
58
|
+
if (typeof current === 'number') {
|
|
59
|
+
if (!Number.isFinite(current))
|
|
60
|
+
throw new TypeError(`${label} contains a non-JSON number.`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (typeof current !== 'object')
|
|
64
|
+
throw new TypeError(`${label} contains a non-JSON value.`);
|
|
65
|
+
if (ancestors.has(current))
|
|
66
|
+
throw new TypeError(`${label} contains a cyclic object reference.`);
|
|
67
|
+
let prototype;
|
|
68
|
+
let ownKeys;
|
|
69
|
+
let descriptors;
|
|
70
|
+
try {
|
|
71
|
+
prototype = Object.getPrototypeOf(current);
|
|
72
|
+
ownKeys = Reflect.ownKeys(current);
|
|
73
|
+
descriptors = Object.getOwnPropertyDescriptors(current);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
throw new TypeError(`${label} could not be inspected safely.`);
|
|
77
|
+
}
|
|
78
|
+
if (ownKeys.some((key) => typeof key !== 'string')) {
|
|
79
|
+
throw new TypeError(`${label} contains a symbol key.`);
|
|
80
|
+
}
|
|
81
|
+
if (ownKeys.some((key) => {
|
|
82
|
+
const descriptor = descriptors[String(key)];
|
|
83
|
+
return !descriptor || !Object.hasOwn(descriptor, 'value');
|
|
84
|
+
})) {
|
|
85
|
+
throw new TypeError(`${label} contains an accessor property.`);
|
|
86
|
+
}
|
|
87
|
+
ancestors.add(current);
|
|
88
|
+
if (Array.isArray(current)) {
|
|
89
|
+
if (prototype !== Array.prototype) {
|
|
90
|
+
throw new TypeError(`${label} contains an array with a custom prototype.`);
|
|
91
|
+
}
|
|
92
|
+
const length = descriptors.length?.value;
|
|
93
|
+
if (!Number.isSafeInteger(length) || length < 0 || length > maximumContainerItems) {
|
|
94
|
+
throw new TypeError(`${label} contains a container with more than ${maximumContainerItems} items.`);
|
|
95
|
+
}
|
|
96
|
+
const indexes = ownKeys.filter((key) => key !== 'length');
|
|
97
|
+
if (indexes.length !== length
|
|
98
|
+
|| indexes.some((key, index) => key !== String(index) || descriptors[key]?.enumerable !== true)) {
|
|
99
|
+
throw new TypeError(`${label} contains a sparse, extended, or non-enumerable array.`);
|
|
100
|
+
}
|
|
101
|
+
for (const key of indexes)
|
|
102
|
+
visit(descriptors[key].value, depth + 1);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
106
|
+
throw new TypeError(`${label} contains an object with a custom prototype.`);
|
|
107
|
+
}
|
|
108
|
+
const objectKeys = ownKeys;
|
|
109
|
+
if (objectKeys.length > maximumContainerItems) {
|
|
110
|
+
throw new TypeError(`${label} contains a container with more than ${maximumContainerItems} items.`);
|
|
111
|
+
}
|
|
112
|
+
if (objectKeys.some((key) => descriptors[key]?.enumerable !== true)) {
|
|
113
|
+
throw new TypeError(`${label} contains a non-enumerable object property.`);
|
|
114
|
+
}
|
|
115
|
+
keys += objectKeys.length;
|
|
116
|
+
if (keys > maximumKeys) {
|
|
117
|
+
throw new TypeError(`${label} exceeds the ${maximumKeys}-key limit.`);
|
|
118
|
+
}
|
|
119
|
+
for (const key of objectKeys) {
|
|
120
|
+
if (!isSafeJsonObjectKey(key))
|
|
121
|
+
throw new TypeError(`${label} contains an unsafe object key.`);
|
|
122
|
+
text(key);
|
|
123
|
+
visit(descriptors[key].value, depth + 1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
ancestors.delete(current);
|
|
127
|
+
};
|
|
128
|
+
visit(value, 0);
|
|
129
|
+
}
|
|
130
|
+
export function scanBoundedJson(raw, limits = {}, observeString) {
|
|
131
|
+
const maximumDepth = limits.maximumDepth ?? MAX_BOUNDED_JSON_DEPTH;
|
|
132
|
+
const maximumKeys = limits.maximumKeys ?? MAX_BOUNDED_JSON_KEYS;
|
|
133
|
+
const maximumValues = limits.maximumValues ?? MAX_BOUNDED_JSON_VALUES;
|
|
134
|
+
const maximumContainerItems = limits.maximumContainerItems ?? MAX_BOUNDED_JSON_CONTAINER_ITEMS;
|
|
135
|
+
let index = 0;
|
|
136
|
+
let keys = 0;
|
|
137
|
+
let values = 0;
|
|
138
|
+
let stringCodeUnits = 0;
|
|
139
|
+
const whitespace = () => {
|
|
140
|
+
while (index < raw.length) {
|
|
141
|
+
const code = raw.charCodeAt(index);
|
|
142
|
+
if (code !== 0x20 && code !== 0x09 && code !== 0x0a && code !== 0x0d)
|
|
143
|
+
break;
|
|
144
|
+
index += 1;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
const stringToken = () => {
|
|
148
|
+
const start = index;
|
|
149
|
+
index += 1;
|
|
150
|
+
while (index < raw.length) {
|
|
151
|
+
const character = raw[index];
|
|
152
|
+
if (character === '"') {
|
|
153
|
+
index += 1;
|
|
154
|
+
let value;
|
|
155
|
+
try {
|
|
156
|
+
value = JSON.parse(raw.slice(start, index));
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
syntaxError();
|
|
160
|
+
}
|
|
161
|
+
stringCodeUnits += value.length;
|
|
162
|
+
if (limits.maximumStringCodeUnits !== undefined && stringCodeUnits > limits.maximumStringCodeUnits) {
|
|
163
|
+
throw new TypeError('Artefact JSON exceeds the aggregate text limit.');
|
|
164
|
+
}
|
|
165
|
+
return { value, offset: start };
|
|
166
|
+
}
|
|
167
|
+
if (character === '\\') {
|
|
168
|
+
index += 1;
|
|
169
|
+
if (index >= raw.length)
|
|
170
|
+
syntaxError();
|
|
171
|
+
if (raw[index] === 'u') {
|
|
172
|
+
if (!/^[a-fA-F0-9]{4}$/u.test(raw.slice(index + 1, index + 5)))
|
|
173
|
+
syntaxError();
|
|
174
|
+
index += 5;
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
if (!/["\\/bfnrt]/u.test(raw[index]))
|
|
178
|
+
syntaxError();
|
|
179
|
+
index += 1;
|
|
180
|
+
}
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (character.charCodeAt(0) <= 0x1f)
|
|
184
|
+
syntaxError();
|
|
185
|
+
index += 1;
|
|
186
|
+
}
|
|
187
|
+
return syntaxError();
|
|
188
|
+
};
|
|
189
|
+
const literal = (token) => {
|
|
190
|
+
if (raw.slice(index, index + token.length) !== token)
|
|
191
|
+
syntaxError();
|
|
192
|
+
index += token.length;
|
|
193
|
+
};
|
|
194
|
+
const number = () => {
|
|
195
|
+
const match = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/u.exec(raw.slice(index));
|
|
196
|
+
if (!match)
|
|
197
|
+
syntaxError();
|
|
198
|
+
if (!Number.isFinite(Number(match[0]))) {
|
|
199
|
+
throw new TypeError('Artefact JSON contains a non-finite number.');
|
|
200
|
+
}
|
|
201
|
+
index += match[0].length;
|
|
202
|
+
};
|
|
203
|
+
const value = (depth, propertyKey) => {
|
|
204
|
+
values += 1;
|
|
205
|
+
if (values > maximumValues) {
|
|
206
|
+
throw new TypeError(`Artefact JSON exceeds the ${maximumValues}-value limit.`);
|
|
207
|
+
}
|
|
208
|
+
if (depth > maximumDepth) {
|
|
209
|
+
throw new TypeError(`Artefact JSON exceeds the ${maximumDepth}-level nesting limit.`);
|
|
210
|
+
}
|
|
211
|
+
whitespace();
|
|
212
|
+
const character = raw[index];
|
|
213
|
+
if (character === '"') {
|
|
214
|
+
const parsedString = stringToken();
|
|
215
|
+
observeString?.({ ...parsedString, kind: 'value', ...(propertyKey === undefined ? {} : { propertyKey }) });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (character === '{') {
|
|
219
|
+
index += 1;
|
|
220
|
+
whitespace();
|
|
221
|
+
const seen = new Set();
|
|
222
|
+
if (raw[index] === '}') {
|
|
223
|
+
index += 1;
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
while (index < raw.length) {
|
|
227
|
+
if (raw[index] !== '"')
|
|
228
|
+
syntaxError();
|
|
229
|
+
const parsedKey = stringToken();
|
|
230
|
+
const key = parsedKey.value;
|
|
231
|
+
if (seen.size >= maximumContainerItems) {
|
|
232
|
+
throw new TypeError(`Artefact JSON contains a container with more than ${maximumContainerItems} items.`);
|
|
233
|
+
}
|
|
234
|
+
keys += 1;
|
|
235
|
+
if (keys > maximumKeys) {
|
|
236
|
+
throw new TypeError(`Artefact JSON exceeds the ${maximumKeys}-key limit.`);
|
|
237
|
+
}
|
|
238
|
+
if (seen.has(key))
|
|
239
|
+
throw new TypeError('Artefact JSON contains a duplicate object key.');
|
|
240
|
+
if (!isSafeJsonObjectKey(key))
|
|
241
|
+
throw new TypeError('Artefact JSON contains an unsafe object key.');
|
|
242
|
+
seen.add(key);
|
|
243
|
+
observeString?.({ ...parsedKey, kind: 'key' });
|
|
244
|
+
whitespace();
|
|
245
|
+
if (raw[index] !== ':')
|
|
246
|
+
syntaxError();
|
|
247
|
+
index += 1;
|
|
248
|
+
value(depth + 1, key);
|
|
249
|
+
whitespace();
|
|
250
|
+
if (raw[index] === '}') {
|
|
251
|
+
index += 1;
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (raw[index] !== ',')
|
|
255
|
+
syntaxError();
|
|
256
|
+
index += 1;
|
|
257
|
+
whitespace();
|
|
258
|
+
}
|
|
259
|
+
syntaxError();
|
|
260
|
+
}
|
|
261
|
+
if (character === '[') {
|
|
262
|
+
index += 1;
|
|
263
|
+
whitespace();
|
|
264
|
+
if (raw[index] === ']') {
|
|
265
|
+
index += 1;
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
let items = 0;
|
|
269
|
+
while (index < raw.length) {
|
|
270
|
+
items += 1;
|
|
271
|
+
if (items > maximumContainerItems) {
|
|
272
|
+
throw new TypeError(`Artefact JSON contains a container with more than ${maximumContainerItems} items.`);
|
|
273
|
+
}
|
|
274
|
+
value(depth + 1);
|
|
275
|
+
whitespace();
|
|
276
|
+
if (raw[index] === ']') {
|
|
277
|
+
index += 1;
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (raw[index] !== ',')
|
|
281
|
+
syntaxError();
|
|
282
|
+
index += 1;
|
|
283
|
+
whitespace();
|
|
284
|
+
}
|
|
285
|
+
syntaxError();
|
|
286
|
+
}
|
|
287
|
+
if (character === 't') {
|
|
288
|
+
literal('true');
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (character === 'f') {
|
|
292
|
+
literal('false');
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (character === 'n') {
|
|
296
|
+
literal('null');
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
number();
|
|
300
|
+
};
|
|
301
|
+
whitespace();
|
|
302
|
+
value(0);
|
|
303
|
+
whitespace();
|
|
304
|
+
if (index !== raw.length)
|
|
305
|
+
syntaxError();
|
|
306
|
+
}
|
|
307
|
+
export function parseBoundedJson(raw, options) {
|
|
308
|
+
const label = options.label ?? 'Artefact input';
|
|
309
|
+
if (typeof raw !== 'string')
|
|
310
|
+
throw new TypeError(`${label} must be UTF-8 JSON text.`);
|
|
311
|
+
const bytes = new TextEncoder().encode(raw).byteLength;
|
|
312
|
+
if (bytes < 1 || bytes > options.maximumBytes) {
|
|
313
|
+
throw new TypeError(`${label} must be between 1 byte and ${options.maximumBytes} bytes.`);
|
|
314
|
+
}
|
|
315
|
+
scanBoundedJson(raw, options.limits);
|
|
316
|
+
try {
|
|
317
|
+
return JSON.parse(raw);
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return syntaxError();
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
export function parseBoundedJsonObject(raw, options) {
|
|
324
|
+
const label = options.label ?? 'Artefact input';
|
|
325
|
+
const parsed = parseBoundedJson(raw, options);
|
|
326
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
327
|
+
throw new TypeError(`${label} must contain one JSON object.`);
|
|
328
|
+
}
|
|
329
|
+
return parsed;
|
|
330
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
export class CliGrammarError extends Error {
|
|
2
|
+
constructor(message) { super(message); this.name = 'CliGrammarError'; }
|
|
3
|
+
}
|
|
4
|
+
function humanList(values, conjunction = 'or') {
|
|
5
|
+
if (values.length < 2)
|
|
6
|
+
return values[0] ?? '';
|
|
7
|
+
if (values.length === 2)
|
|
8
|
+
return `${values[0]} ${conjunction} ${values[1]}`;
|
|
9
|
+
return `${values.slice(0, -1).join(', ')}, ${conjunction} ${values.at(-1)}`;
|
|
10
|
+
}
|
|
11
|
+
function valueError(specification) {
|
|
12
|
+
if (specification.valueKind === 'file') {
|
|
13
|
+
return new CliGrammarError(`${specification.option} requires one bounded file path.`);
|
|
14
|
+
}
|
|
15
|
+
if (specification.valueKind === 'integer') {
|
|
16
|
+
const baseRange = specification.integerRanges[0];
|
|
17
|
+
return new CliGrammarError(baseRange
|
|
18
|
+
? `${specification.option} requires an integer from ${baseRange.minimum} to ${baseRange.maximum}.`
|
|
19
|
+
: `${specification.option} requires one bounded integer.`);
|
|
20
|
+
}
|
|
21
|
+
if (specification.values.length > 0) {
|
|
22
|
+
return new CliGrammarError(`${specification.option} requires ${humanList(specification.values)}.`);
|
|
23
|
+
}
|
|
24
|
+
return new CliGrammarError(`${specification.option} requires one bounded value.`);
|
|
25
|
+
}
|
|
26
|
+
function parseTokens(grammar, argv) {
|
|
27
|
+
const specificationByOption = new Map(grammar.options.map((specification) => [specification.option, specification]));
|
|
28
|
+
const options = new Map();
|
|
29
|
+
const positionals = [];
|
|
30
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
31
|
+
const argument = argv[index];
|
|
32
|
+
if (argument === '--') {
|
|
33
|
+
positionals.push(...argv.slice(index + 1));
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
if (!argument.startsWith('-')) {
|
|
37
|
+
positionals.push(argument);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const specification = specificationByOption.get(argument);
|
|
41
|
+
if (!specification || specification.metaAction !== null) {
|
|
42
|
+
throw new CliGrammarError(`Unknown option "${argument}".`);
|
|
43
|
+
}
|
|
44
|
+
const existing = options.get(argument);
|
|
45
|
+
if (existing && specification.occurrence === 'once') {
|
|
46
|
+
throw new CliGrammarError(`${argument} may be supplied only once.`);
|
|
47
|
+
}
|
|
48
|
+
if (existing && specification.occurrence === 'idempotent')
|
|
49
|
+
continue;
|
|
50
|
+
let value = '';
|
|
51
|
+
if (specification.arity === 1) {
|
|
52
|
+
const candidate = argv[++index];
|
|
53
|
+
if (candidate === undefined || candidate === ''
|
|
54
|
+
|| (!specification.acceptsOptionLikeValue && candidate.startsWith('-'))) {
|
|
55
|
+
throw valueError(specification);
|
|
56
|
+
}
|
|
57
|
+
value = candidate;
|
|
58
|
+
}
|
|
59
|
+
if (existing)
|
|
60
|
+
existing.push(value);
|
|
61
|
+
else
|
|
62
|
+
options.set(argument, [value]);
|
|
63
|
+
}
|
|
64
|
+
return { options, positionals };
|
|
65
|
+
}
|
|
66
|
+
function selectedIntegerRange(specification, options) {
|
|
67
|
+
return specification.integerRanges
|
|
68
|
+
.filter((range) => range.whenOptionPresent === null || options.has(range.whenOptionPresent))
|
|
69
|
+
.at(-1) ?? null;
|
|
70
|
+
}
|
|
71
|
+
function validateOptionValues(command, grammar, options) {
|
|
72
|
+
for (const specification of grammar.options) {
|
|
73
|
+
const values = options.get(specification.option);
|
|
74
|
+
if (!values || specification.arity === 0)
|
|
75
|
+
continue;
|
|
76
|
+
if (specification.valueKind === 'enum') {
|
|
77
|
+
if (values.some((value) => !specification.values.includes(value)))
|
|
78
|
+
throw valueError(specification);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (specification.valueKind === 'integer') {
|
|
82
|
+
const range = selectedIntegerRange(specification, options);
|
|
83
|
+
if (!range)
|
|
84
|
+
throw new Error(`Integer option ${command} ${specification.option} has no applicable range.`);
|
|
85
|
+
for (const value of values) {
|
|
86
|
+
if (!/^\d+$/u.test(value))
|
|
87
|
+
throw valueError(specification);
|
|
88
|
+
const parsed = Number(value);
|
|
89
|
+
if (!Number.isSafeInteger(parsed) || parsed < range.minimum || parsed > range.maximum) {
|
|
90
|
+
throw new CliGrammarError(`${specification.option} must be from ${range.minimum} to ${range.maximum}.`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (specification.valueKind === 'policy_list') {
|
|
96
|
+
for (const value of values) {
|
|
97
|
+
const policies = [...new Set(value.split(',').map((item) => item.trim().toLowerCase()).filter(Boolean))];
|
|
98
|
+
if (!policies.length || policies.some((policy) => !specification.values.includes(policy))) {
|
|
99
|
+
throw new CliGrammarError(`${specification.option} for ${command} supports: ${specification.values.join(', ')}.`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function allocatePositionals(command, grammar, rawPositionals) {
|
|
106
|
+
const specifications = grammar.positionals;
|
|
107
|
+
const maximum = specifications.reduce((sum, specification) => sum + specification.maximum, 0);
|
|
108
|
+
if (rawPositionals.length > maximum) {
|
|
109
|
+
throw new CliGrammarError(`${command} accepts at most ${maximum} positional value${maximum === 1 ? '' : 's'}.`);
|
|
110
|
+
}
|
|
111
|
+
const allocated = {};
|
|
112
|
+
let offset = 0;
|
|
113
|
+
for (const [specificationIndex, specification] of specifications.entries()) {
|
|
114
|
+
const remainingMinimum = specifications.slice(specificationIndex + 1)
|
|
115
|
+
.reduce((sum, later) => sum + later.minimum, 0);
|
|
116
|
+
const available = Math.max(0, rawPositionals.length - offset - remainingMinimum);
|
|
117
|
+
const count = Math.min(specification.maximum, available);
|
|
118
|
+
const values = Object.freeze(rawPositionals.slice(offset, offset + count));
|
|
119
|
+
offset += count;
|
|
120
|
+
if (values.length < specification.minimum) {
|
|
121
|
+
throw new CliGrammarError(`${command} requires ${specification.minimum} ${specification.name} value${specification.minimum === 1 ? '' : 's'}.`);
|
|
122
|
+
}
|
|
123
|
+
if (specification.valueKind === 'enum'
|
|
124
|
+
&& values.some((value) => !specification.values.includes(value))) {
|
|
125
|
+
throw new CliGrammarError(`${command} ${specification.name} must be ${humanList(specification.values)}.`);
|
|
126
|
+
}
|
|
127
|
+
allocated[specification.name] = values;
|
|
128
|
+
}
|
|
129
|
+
return Object.freeze(allocated);
|
|
130
|
+
}
|
|
131
|
+
function present(options, option) {
|
|
132
|
+
return options.has(option);
|
|
133
|
+
}
|
|
134
|
+
function validateConstraint(command, constraint, options) {
|
|
135
|
+
if (constraint.kind === 'required') {
|
|
136
|
+
const missing = constraint.options.filter((option) => !present(options, option));
|
|
137
|
+
if (missing.length > 0)
|
|
138
|
+
throw new CliGrammarError(`${command} requires ${missing.join(', ')}.`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (constraint.kind === 'mutually_exclusive') {
|
|
142
|
+
const selected = constraint.options.filter((option) => present(options, option));
|
|
143
|
+
if (selected.length > 1) {
|
|
144
|
+
throw new CliGrammarError(`${selected.join(' and ')} are mutually exclusive and may be supplied only once.`);
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (constraint.kind === 'excludes_all') {
|
|
149
|
+
if (!present(options, constraint.option))
|
|
150
|
+
return;
|
|
151
|
+
const selected = constraint.excludedOptions.filter((option) => present(options, option));
|
|
152
|
+
if (selected.length > 0) {
|
|
153
|
+
throw new CliGrammarError(`${constraint.option} cannot be combined with ${selected.join(', ')}.`);
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (constraint.kind === 'value_excludes') {
|
|
158
|
+
if (!options.get(constraint.option)?.includes(constraint.value))
|
|
159
|
+
return;
|
|
160
|
+
const selected = constraint.excludedOptions.filter((option) => present(options, option));
|
|
161
|
+
if (selected.length > 0) {
|
|
162
|
+
throw new CliGrammarError(`${constraint.option} ${constraint.value} cannot be combined with ${selected.join(', ')}.`);
|
|
163
|
+
}
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (!present(options, constraint.option))
|
|
167
|
+
return;
|
|
168
|
+
const selected = constraint.requiredOptions.filter((option) => present(options, option));
|
|
169
|
+
if (constraint.kind === 'requires_all' && selected.length !== constraint.requiredOptions.length) {
|
|
170
|
+
throw new CliGrammarError(`${constraint.option} requires ${constraint.requiredOptions.join(' and ')}.`);
|
|
171
|
+
}
|
|
172
|
+
if (constraint.kind === 'requires_any' && selected.length === 0) {
|
|
173
|
+
throw new CliGrammarError(`${constraint.option} requires ${constraint.requiredOptions.join(' or ')}.`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function validatePositionalRequirements(grammar, allocated, options) {
|
|
177
|
+
for (const specification of grammar.positionals) {
|
|
178
|
+
if (specification.requiredWhenOptions.some((option) => options.has(option))
|
|
179
|
+
&& (allocated[specification.name]?.length ?? 0) === 0) {
|
|
180
|
+
throw new CliGrammarError(`${specification.requiredWhenOptions.join(' or ')} requires a positional ${specification.name}.`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function freezeOptions(options) {
|
|
185
|
+
return new Map([...options].map(([option, values]) => [option, Object.freeze([...values])]));
|
|
186
|
+
}
|
|
187
|
+
export function parseCliGrammar(command, grammar, argv) {
|
|
188
|
+
const parsed = parseTokens(grammar, argv);
|
|
189
|
+
const options = freezeOptions(parsed.options);
|
|
190
|
+
validateOptionValues(command, grammar, options);
|
|
191
|
+
const allocated = allocatePositionals(command, grammar, parsed.positionals);
|
|
192
|
+
validatePositionalRequirements(grammar, allocated, options);
|
|
193
|
+
for (const constraint of grammar.constraints) {
|
|
194
|
+
validateConstraint(command, constraint, options);
|
|
195
|
+
}
|
|
196
|
+
for (const option of ['--config', '--profile']) {
|
|
197
|
+
const specification = grammar.options.find((candidate) => candidate.option === option);
|
|
198
|
+
if (specification?.scope === 'common' && options.has(option)) {
|
|
199
|
+
throw new CliGrammarError(`${option} must be resolved before command parsing.`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const allPositionals = Object.freeze([...parsed.positionals]);
|
|
203
|
+
return Object.freeze({
|
|
204
|
+
command,
|
|
205
|
+
allPositionals,
|
|
206
|
+
hasOption: (option) => options.has(option),
|
|
207
|
+
optionValue: (option) => options.get(option)?.at(-1) ?? null,
|
|
208
|
+
optionValues: (option) => options.get(option) ?? Object.freeze([]),
|
|
209
|
+
integerOption: (option) => {
|
|
210
|
+
const value = options.get(option)?.at(-1);
|
|
211
|
+
return value === undefined ? null : Number(value);
|
|
212
|
+
},
|
|
213
|
+
positionalValue: (name) => allocated[name]?.[0] ?? null,
|
|
214
|
+
positionalValues: (name) => allocated[name] ?? Object.freeze([]),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const MAX_CT_QUERY_LENGTH = 200;
|
|
2
|
+
const CT_QUERY_ERROR_CODE = 'INVALID_CT_QUERY';
|
|
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, };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Shared response-contract bounds for certificate-log search. The collector
|
|
2
|
+
// and browser normalizer import these values so a valid backend response is
|
|
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;
|