@slicedearth/whoisleuth-cli 2.4.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 +2 -2
- package/cli/argument-values.mjs +55 -0
- package/cli/arguments.mjs +24 -470
- package/cli/artifact-validation/case-response.mjs +21 -6
- package/cli/artifact-validation/lookup-evidence.mjs +3 -3
- package/cli/artifact-validation/signed-review.mjs +59 -13
- package/cli/artifact-verify.mjs +2 -0
- 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 +2 -2
- package/cli/bulk.mjs +2 -3
- package/cli/case-command.mjs +20 -3
- package/cli/case-pack.mjs +12 -2
- 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 +329 -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 +3 -3
- 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/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/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 +33 -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 +107 -0
- package/cli/interchange-report.mjs +4 -2
- package/cli/investigation-artifacts.mjs +1 -1
- 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 +197 -17
- package/cli/lookalike-calibration.mjs +1 -1
- package/cli/lookup-brief.mjs +2 -2
- package/cli/lookup-command-runner.mjs +18 -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 +39 -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 +131 -0
- package/cli/review-command-definitions.mjs +273 -0
- package/cli/review-command-runner.mjs +68 -158
- package/cli/risk-calibration.mjs +8 -8
- package/cli/runner.mjs +20 -32
- package/cli/saved-lookup.mjs +2 -2
- package/cli/selected-input-worker.mjs +63 -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 +2 -2
- package/cli/workflow-arguments.mjs +86 -0
- package/cli/workflow-command-definitions.mjs +78 -0
- package/cli/workflow-command-runner.mjs +8 -1
- package/docs/cli-reference.md +27 -0
- package/docs/cli.md +223 -2
- package/docs/contextual-reviews.md +155 -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/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 +12 -8
- package/lib/dns-inheritance-review.mjs +53 -63
- package/lib/dns-intelligence.mjs +9 -7
- 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 +22 -5
- package/lib/domain-posture.mjs +16 -15
- package/lib/domain-registration.mjs +392 -0
- package/lib/evidence-export-privacy.mjs +22 -15
- package/lib/evidence-export.mjs +3 -3
- package/lib/evidence-report.mjs +4 -1
- package/lib/external-intelligence-risk.mjs +2 -124
- 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 +7 -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-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 +69 -40
- package/lib/lookup-response.mjs +5 -15
- package/lib/lookup-source-progress.mjs +8 -22
- package/lib/lookup.mjs +1 -1
- 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 +27 -0
- package/lib/smtp-transport-review.mjs +48 -19
- package/lib/sslbl-certificates.generated.mjs +141 -139
- package/lib/static-html-analysis.mjs +1 -1
- package/lib/structured-data-identity.mjs +1 -1
- 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/typosquat-generator.mjs +11 -2
- package/lib/website-enrichment.mjs +388 -0
- package/lib/whois-authority.mjs +43 -5
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-parser.mjs +5 -3
- package/package.json +13 -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-evidence-links.mjs +135 -0
- package/packages/cases/case-evidence-model.mjs +78 -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-recheck-model.mjs +90 -32
- 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 +40 -112
- package/packages/cases/case-record-projection.mjs +8 -1
- package/packages/cases/case-record-version-input.mjs +19 -0
- package/packages/cases/case-report.mjs +63 -27
- package/packages/cases/case-requested-evidence.mjs +146 -0
- package/packages/cases/case-response-actions.mjs +103 -1
- package/packages/cases/case-response-model.mjs +9 -1
- package/packages/cases/case-response-outcomes.mjs +29 -8
- package/packages/cases/case-response-packet-render.mjs +13 -9
- package/packages/cases/case-response-packet.mjs +25 -11
- package/packages/cases/case-response-records.mjs +0 -1
- package/packages/cases/case-response-review-inputs.mjs +14 -3
- package/packages/cases/case-storage-model.mjs +1 -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/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 +28 -10
- package/packages/contracts/case-portability.mjs +118 -25
- package/packages/contracts/cli-command-semantics.mjs +2 -0
- package/packages/contracts/cli-grammar.mjs +6 -0
- package/packages/contracts/cli-lookup-brief.mjs +2 -0
- package/packages/contracts/context-review.mjs +24 -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/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/mail-authentication.mjs +5 -0
- package/packages/contracts/message-intake.mjs +31 -0
- package/packages/contracts/offline-comparison.mjs +4 -1
- package/packages/contracts/passive-mail.mjs +10 -0
- package/packages/contracts/web-capture.mjs +2 -2
- package/packages/contracts/workspace-portability.mjs +63 -17
- package/packages/evidence/collection-quality.mjs +67 -0
- package/packages/evidence/dns-query-outcome.mjs +72 -0
- package/packages/evidence/email-recipient.mjs +18 -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/interchange/defensive-indicator-export.mjs +264 -0
- package/packages/interchange/external-findings-import.mjs +4 -12
- 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 +22 -15
- package/packages/investigation/acquisition-decision-packet.mjs +1 -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 +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-manifest.mjs +2 -2
- package/packages/investigation/link-intake.mjs +179 -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 +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/workspace/brand-profile-model.mjs +2 -2
- package/packages/workspace/bulk-review-model.mjs +28 -26
- package/packages/workspace/bulk-session-model.mjs +23 -13
- package/packages/workspace/campaign-model.mjs +1 -1
- package/packages/workspace/detection-rule-model.mjs +1 -1
- 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 +14 -5
- package/packages/workspace/watchlist-history.mjs +28 -12
- package/packages/workspace/watchlist-store.mjs +17 -5
- 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
|
@@ -21,7 +21,9 @@ export function normalizeWhoisChain(value) {
|
|
|
21
21
|
...(typeof hop.queriedAt === 'string' ? { queriedAt: hop.queriedAt.slice(0, 64) } : {}),
|
|
22
22
|
...(typeof hop.queryProfile === 'string' ? { queryProfile: hop.queryProfile.slice(0, 80) } : {}),
|
|
23
23
|
...(typeof hop.responseEncoding === 'string' ? { responseEncoding: hop.responseEncoding.slice(0, 40) } : {}),
|
|
24
|
-
|
|
24
|
+
// Parser and authority analysis share protocol line endings, while the
|
|
25
|
+
// original hop retains its wire text. Lone and embedded controls remain.
|
|
26
|
+
...(typeof hop.response === 'string' ? { response: hop.response.slice(0, MAX_WHOIS_BYTES).replace(/\r\n/g, '\n') } : {}),
|
|
25
27
|
...(typeof hop.error === 'string' ? { error: hop.error.slice(0, 1000) } : {}),
|
|
26
28
|
});
|
|
27
29
|
}
|
package/lib/whois-parser.mjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// WHOIS response parsing. Transport, referral-chain and stable public exports
|
|
2
2
|
// remain owned by the facade in whois.mts.
|
|
3
3
|
import { registryDateIso } from "./registry-dates.mjs";
|
|
4
|
-
import { analyzeWhoisChainAuthority } from "./whois-authority.mjs";
|
|
4
|
+
import { analyzeWhoisChainAuthority, whoisResponseMatchesDomain } from "./whois-authority.mjs";
|
|
5
5
|
import { normalizeWhoisChain } from "./whois-normalization.mjs";
|
|
6
6
|
import { applyWhoisCommonFormats } from "./whois-common-formats.mjs";
|
|
7
7
|
import { applyWhoisDialects } from "./whois-dialects.mjs";
|
|
8
8
|
import { WHOIS_FIELD_PATTERNS, normalizedWhoisContact, } from "./whois-parser-support.mjs";
|
|
9
9
|
import { boundedWhoisValue, whoisFieldLimit, } from "./whois-values.mjs";
|
|
10
|
-
function parseWhoisChain(chain) {
|
|
10
|
+
function parseWhoisChain(chain, requestedDomain) {
|
|
11
11
|
const source = normalizeWhoisChain(chain);
|
|
12
12
|
const fields = {};
|
|
13
13
|
const truncatedFields = new Set();
|
|
@@ -18,6 +18,8 @@ function parseWhoisChain(chain) {
|
|
|
18
18
|
const text = hop.response;
|
|
19
19
|
if (!text)
|
|
20
20
|
return;
|
|
21
|
+
if (hopIndex > 0 && requestedDomain !== undefined && !whoisResponseMatchesDomain(text, requestedDomain))
|
|
22
|
+
return;
|
|
21
23
|
// hopIndex 0 is always whois.iana.org, whose "domain:"/"created:"/
|
|
22
24
|
// "changed:" fields describe the TLD's own root delegation record, not
|
|
23
25
|
// the queried domain - e.g. "created: 1992-08-14" for .gt is Guatemala's
|
|
@@ -118,7 +120,7 @@ function parseWhoisChain(chain) {
|
|
|
118
120
|
// Existence is decided authority-aware, not by a global "any hop said no
|
|
119
121
|
// match" flag: positive registry evidence is never overridden by a later
|
|
120
122
|
// registrar hop that failed, rate-limited, or returned "no match".
|
|
121
|
-
const authority = analyzeWhoisChainAuthority(source);
|
|
123
|
+
const authority = analyzeWhoisChainAuthority(source, requestedDomain);
|
|
122
124
|
return {
|
|
123
125
|
...fields,
|
|
124
126
|
nameservers: [...nameservers],
|
package/package.json
CHANGED
|
@@ -33,38 +33,37 @@
|
|
|
33
33
|
"bugs": {
|
|
34
34
|
"url": "https://github.com/slicedearth/whoisleuth/issues"
|
|
35
35
|
},
|
|
36
|
-
"version": "2.
|
|
36
|
+
"version": "2.5.1",
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@
|
|
38
|
+
"@nuintun/qrcode": "5.0.3",
|
|
39
|
+
"@peculiar/x509": "2.1.0",
|
|
40
|
+
"fast-png": "8.0.0",
|
|
39
41
|
"fflate": "0.8.3",
|
|
40
42
|
"maxmind": "5.0.7",
|
|
41
43
|
"parse5": "8.0.1",
|
|
44
|
+
"pdfjs-dist": "6.3.289",
|
|
45
|
+
"postal-mime": "3.0.0",
|
|
42
46
|
"reflect-metadata": "0.2.2",
|
|
43
|
-
"
|
|
44
|
-
"
|
|
47
|
+
"saxes": "6.0.0",
|
|
48
|
+
"tldts": "7.4.14",
|
|
49
|
+
"undici": "8.11.0"
|
|
45
50
|
},
|
|
46
51
|
"files": [
|
|
47
52
|
"bin/**/*.mjs",
|
|
48
53
|
"cli/**/*.mjs",
|
|
49
54
|
"lib/**/*.mjs",
|
|
50
55
|
"frontend/src/lib/**/*.js",
|
|
51
|
-
"packages
|
|
52
|
-
"
|
|
53
|
-
"packages/contracts/**/*.mjs",
|
|
54
|
-
"packages/evidence/**/*.mjs",
|
|
55
|
-
"packages/interchange/**/*.mjs",
|
|
56
|
-
"packages/investigation/**/*.mjs",
|
|
57
|
-
"packages/monitoring/**/*.mjs",
|
|
58
|
-
"packages/workspace/**/*.mjs",
|
|
56
|
+
"packages/**/*.mjs",
|
|
57
|
+
"README.md",
|
|
59
58
|
"docs/cli.md",
|
|
60
59
|
"docs/cli-reference.md",
|
|
60
|
+
"docs/contextual-reviews.md",
|
|
61
61
|
"DISCLOSURE",
|
|
62
62
|
"LICENSE",
|
|
63
|
-
"LICENSES/*.txt",
|
|
64
63
|
"NOTICE",
|
|
65
|
-
"README.md",
|
|
66
64
|
"SECURITY.md",
|
|
67
65
|
"TRADEMARKS.md",
|
|
66
|
+
"LICENSES/Retire.js-Apache-2.0.txt",
|
|
68
67
|
"third-party-notices.txt"
|
|
69
68
|
],
|
|
70
69
|
"publishConfig": {
|
|
@@ -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
|
+
}
|