@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
package/lib/risk-scoring.mjs
CHANGED
|
@@ -1,345 +1,2 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
const RISK_STATES = new Set(['registered', 'for_sale', 'expiring']);
|
|
4
|
-
const STATE_LABELS = Object.freeze({
|
|
5
|
-
for_sale: 'for sale',
|
|
6
|
-
expiring: 'expiring/pending delete',
|
|
7
|
-
registered: 'registered',
|
|
8
|
-
});
|
|
9
|
-
// Version 8 keeps unreviewed whole-page similarity as visible review context
|
|
10
|
-
// without assigning it Risk points. Reviewed favicon and official-asset
|
|
11
|
-
// observations remain bounded within the brand-presentation family, generic
|
|
12
|
-
// operational properties remain supporting context, and missing evidence never
|
|
13
|
-
// contributes points.
|
|
14
|
-
export const RISK_MODEL_VERSION = 8;
|
|
15
|
-
export const RISK_REVIEW_THRESHOLD = 70;
|
|
16
|
-
const CURRENT_RISK_MODEL = Object.freeze({
|
|
17
|
-
modelVersion: RISK_MODEL_VERSION,
|
|
18
|
-
includePageBaselineMatch: false,
|
|
19
|
-
});
|
|
20
|
-
const RISK_MODEL_V7 = Object.freeze({
|
|
21
|
-
modelVersion: 7,
|
|
22
|
-
includePageBaselineMatch: true,
|
|
23
|
-
});
|
|
24
|
-
const RISK_STATE_BASE = Object.freeze({
|
|
25
|
-
registered: 6,
|
|
26
|
-
for_sale: 4,
|
|
27
|
-
expiring: 5,
|
|
28
|
-
});
|
|
29
|
-
const FAMILY_CAPS = Object.freeze({
|
|
30
|
-
'domain-resemblance': 20,
|
|
31
|
-
'brand-presentation': 24,
|
|
32
|
-
'credential-lure': 18,
|
|
33
|
-
'external-intelligence': 18,
|
|
34
|
-
'operational-support': 12,
|
|
35
|
-
});
|
|
36
|
-
const REMOVABLE_RISK_FAMILY_MAP = Object.freeze({
|
|
37
|
-
'brand-presentation': true,
|
|
38
|
-
'credential-lure': true,
|
|
39
|
-
'domain-resemblance': true,
|
|
40
|
-
'external-intelligence': true,
|
|
41
|
-
'operational-support': true,
|
|
42
|
-
});
|
|
43
|
-
const REMOVABLE_RISK_FAMILIES = Object.freeze(Object.keys(REMOVABLE_RISK_FAMILY_MAP));
|
|
44
|
-
const HIGH_CONTEXT_MUTATIONS = new Set([
|
|
45
|
-
'unicode_homoglyph',
|
|
46
|
-
'unicode_homoglyph_depth_2',
|
|
47
|
-
'dictionary',
|
|
48
|
-
'dictionary_token_replacement',
|
|
49
|
-
]);
|
|
50
|
-
const MEDIUM_CONTEXT_MUTATIONS = new Set(['ascii_homoglyph', 'bitsquatting', 'tld_embedding', 'tld_typo', 'tld_substitution']);
|
|
51
|
-
const LOW_CONTEXT_MUTATIONS = new Set([
|
|
52
|
-
'character_addition',
|
|
53
|
-
'character_omission',
|
|
54
|
-
'character_duplication',
|
|
55
|
-
'character_transposition',
|
|
56
|
-
'pluralization',
|
|
57
|
-
'www_prefix',
|
|
58
|
-
'hyphenation',
|
|
59
|
-
'separator_omission',
|
|
60
|
-
'word_reordering',
|
|
61
|
-
'keyboard_substitution',
|
|
62
|
-
'keyboard_insertion',
|
|
63
|
-
'vowel_swap',
|
|
64
|
-
]);
|
|
65
|
-
export const RISK_MUTATION_TYPES = Object.freeze([
|
|
66
|
-
...HIGH_CONTEXT_MUTATIONS,
|
|
67
|
-
...MEDIUM_CONTEXT_MUTATIONS,
|
|
68
|
-
...LOW_CONTEXT_MUTATIONS,
|
|
69
|
-
]);
|
|
70
|
-
export function normalizeRiskModelVersion(value) {
|
|
71
|
-
return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 && value <= 1000 ? value : null;
|
|
72
|
-
}
|
|
73
|
-
function formatAge(days) {
|
|
74
|
-
if (days < 60)
|
|
75
|
-
return `${Math.round(days)}d old`;
|
|
76
|
-
const years = days / 365.25;
|
|
77
|
-
return years < 1 ? `${Math.round(days / 30)}mo old` : `${years.toFixed(1)}y old`;
|
|
78
|
-
}
|
|
79
|
-
function mutationContext(mutationTypes) {
|
|
80
|
-
if (!Array.isArray(mutationTypes))
|
|
81
|
-
return null;
|
|
82
|
-
const bounded = mutationTypes.slice(0, 30);
|
|
83
|
-
if (bounded.some((value) => typeof value === 'string' && HIGH_CONTEXT_MUTATIONS.has(value))) {
|
|
84
|
-
return { label: 'High-similarity or phishing-term candidate context', delta: 18 };
|
|
85
|
-
}
|
|
86
|
-
if (bounded.some((value) => typeof value === 'string' && MEDIUM_CONTEXT_MUTATIONS.has(value))) {
|
|
87
|
-
return { label: 'Lookalike candidate context', delta: 12 };
|
|
88
|
-
}
|
|
89
|
-
if (bounded.some((value) => typeof value === 'string' && LOW_CONTEXT_MUTATIONS.has(value))) {
|
|
90
|
-
return { label: 'Generated variation candidate context', delta: 8 };
|
|
91
|
-
}
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
function hasBoolean(value) {
|
|
95
|
-
return typeof value === 'boolean';
|
|
96
|
-
}
|
|
97
|
-
function scoreQuality(input) {
|
|
98
|
-
const observedFamilies = ['registration'];
|
|
99
|
-
const expectedFamilies = ['registration', 'operational-support'];
|
|
100
|
-
if (Array.isArray(input.mutationTypes) || hasBoolean(input.idnReferenceMatch))
|
|
101
|
-
observedFamilies.push('domain-resemblance');
|
|
102
|
-
if (input.hasActiveBrandProfile === true)
|
|
103
|
-
expectedFamilies.push('domain-resemblance', 'brand-presentation');
|
|
104
|
-
if ([input.faviconMatch, input.faviconNearMatch, input.reusesOfficialAssets, input.pageBaselineMatch].some(hasBoolean)) {
|
|
105
|
-
observedFamilies.push('brand-presentation');
|
|
106
|
-
}
|
|
107
|
-
if ([input.hasPasswordField, input.hasExternalFormAction].some(hasBoolean) || typeof input.phishingLanguageMatch === 'string') {
|
|
108
|
-
observedFamilies.push('credential-lure');
|
|
109
|
-
}
|
|
110
|
-
if (calibrateExternalIntelligenceRisk(input.threatIntelligence, input.domain).eligibleProviderCount > 0) {
|
|
111
|
-
observedFamilies.push('external-intelligence');
|
|
112
|
-
}
|
|
113
|
-
if ([input.hasMx, input.hasSpf, input.hasDmarc].some(hasBoolean)
|
|
114
|
-
|| typeof input.activityStatus === 'string'
|
|
115
|
-
|| typeof input.domainAgeDays === 'number')
|
|
116
|
-
observedFamilies.push('operational-support');
|
|
117
|
-
if (input.scanDepth === 'deep')
|
|
118
|
-
expectedFamilies.push('credential-lure');
|
|
119
|
-
return buildScoreEvidenceQuality(input, {
|
|
120
|
-
expectedDepth: 'deep',
|
|
121
|
-
observedFamilies,
|
|
122
|
-
expectedFamilies,
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
// Risk prioritizes a registered lookalike/typosquat domain for analyst review.
|
|
126
|
-
// It is a heuristic indicator, never a maliciousness or safety verdict.
|
|
127
|
-
function explainRiskScoreInternal(input, excludedFamily, model) {
|
|
128
|
-
const state = input.availability ?? input.state;
|
|
129
|
-
if (typeof state !== 'string' || !RISK_STATES.has(state))
|
|
130
|
-
return null;
|
|
131
|
-
const factors = [];
|
|
132
|
-
const familyTotals = new Map();
|
|
133
|
-
const add = (family, label, requestedDelta) => {
|
|
134
|
-
if (family === excludedFamily)
|
|
135
|
-
return 0;
|
|
136
|
-
const current = familyTotals.get(family) ?? 0;
|
|
137
|
-
const cap = FAMILY_CAPS[family] ?? Number.POSITIVE_INFINITY;
|
|
138
|
-
const delta = Math.max(0, Math.min(requestedDelta, Math.max(0, cap - current)));
|
|
139
|
-
const capped = delta !== requestedDelta;
|
|
140
|
-
factors.push({ family, label: capped ? `${label} (family cap reached)` : label, delta });
|
|
141
|
-
familyTotals.set(family, current + delta);
|
|
142
|
-
return delta;
|
|
143
|
-
};
|
|
144
|
-
add('registration', `Base context for “${STATE_LABELS[state] ?? state}”`, RISK_STATE_BASE[state] ?? 0);
|
|
145
|
-
const mutation = mutationContext(input.mutationTypes);
|
|
146
|
-
if (mutation)
|
|
147
|
-
add('domain-resemblance', mutation.label, mutation.delta);
|
|
148
|
-
if (input.idnReferenceMatch === true)
|
|
149
|
-
add('domain-resemblance', 'IDN skeleton matches an official Brand Profile domain', 20);
|
|
150
|
-
if (input.faviconMatch === true)
|
|
151
|
-
add('brand-presentation', 'Favicon matches an official Brand Profile site', 18);
|
|
152
|
-
else if (input.faviconNearMatch === true)
|
|
153
|
-
add('brand-presentation', 'Favicon resembles an official Brand Profile site', 14);
|
|
154
|
-
if (input.reusesOfficialAssets === true)
|
|
155
|
-
add('brand-presentation', 'Official asset-host relationship observed', 6);
|
|
156
|
-
if (model.includePageBaselineMatch && input.pageBaselineMatch === true) {
|
|
157
|
-
add('brand-presentation', 'Multiple independently compared page-identity components match the official baseline', 14);
|
|
158
|
-
}
|
|
159
|
-
if (typeof input.phishingLanguageMatch === 'string' && input.phishingLanguageMatch.trim()) {
|
|
160
|
-
add('credential-lure', 'Suspicious urgency language observed', 8);
|
|
161
|
-
}
|
|
162
|
-
if (input.hasPasswordField === true && input.hasExternalFormAction === true) {
|
|
163
|
-
add('credential-lure', 'Password form submits to an external origin', 10);
|
|
164
|
-
}
|
|
165
|
-
else if (input.hasPasswordField === true) {
|
|
166
|
-
add('credential-lure', 'Login/password form present', 5);
|
|
167
|
-
}
|
|
168
|
-
else if (input.hasExternalFormAction === true) {
|
|
169
|
-
add('credential-lure', 'External form destination observed without a password field', 0);
|
|
170
|
-
}
|
|
171
|
-
const external = calibrateExternalIntelligenceRisk(input.threatIntelligence, input.domain);
|
|
172
|
-
if (external.factor)
|
|
173
|
-
add('external-intelligence', external.factor.label, external.contribution);
|
|
174
|
-
const primaryFamilies = ['domain-resemblance', 'brand-presentation', 'credential-lure', 'external-intelligence']
|
|
175
|
-
.filter((family) => (familyTotals.get(family) ?? 0) > 0);
|
|
176
|
-
if (primaryFamilies.length >= 2) {
|
|
177
|
-
add('corroboration', `Corroborating context across ${primaryFamilies.length} independent evidence families`, primaryFamilies.length >= 3 ? 18 : 10);
|
|
178
|
-
}
|
|
179
|
-
const hasIndependentContext = primaryFamilies.length > 0;
|
|
180
|
-
const supporting = (label, delta) => {
|
|
181
|
-
add('operational-support', hasIndependentContext ? label : `${label}; neutral without independent suspicious context`, hasIndependentContext ? delta : 0);
|
|
182
|
-
};
|
|
183
|
-
if (input.activityStatus === 'active')
|
|
184
|
-
supporting('Active website observed', 4);
|
|
185
|
-
if (input.hasMx === true)
|
|
186
|
-
supporting('Mail server configured', 4);
|
|
187
|
-
if (input.hasSpf === true && input.hasDmarc === true)
|
|
188
|
-
supporting('SPF and DMARC records present', 1);
|
|
189
|
-
else if (input.hasSpf === true || input.hasDmarc === true)
|
|
190
|
-
supporting('SPF or DMARC record present', 0);
|
|
191
|
-
if (typeof input.domainAgeDays === 'number' && Number.isFinite(input.domainAgeDays) && input.domainAgeDays >= 0) {
|
|
192
|
-
if (input.domainAgeDays < 90)
|
|
193
|
-
supporting(`Recently registered (${formatAge(input.domainAgeDays)})`, 6);
|
|
194
|
-
else if (input.domainAgeDays < 365)
|
|
195
|
-
supporting(`Registered under a year ago (${formatAge(input.domainAgeDays)})`, 2);
|
|
196
|
-
}
|
|
197
|
-
if (typeof input.privacyProtected === 'boolean') {
|
|
198
|
-
add('registration', 'Registration privacy is neutral context', 0);
|
|
199
|
-
}
|
|
200
|
-
const rawScore = Math.round(factors.reduce((total, factor) => total + factor.delta, 0));
|
|
201
|
-
const score = Math.max(0, Math.min(100, rawScore));
|
|
202
|
-
const familyOrder = [
|
|
203
|
-
'registration', 'domain-resemblance', 'brand-presentation', 'credential-lure',
|
|
204
|
-
'external-intelligence', 'corroboration', 'operational-support',
|
|
205
|
-
];
|
|
206
|
-
return {
|
|
207
|
-
modelVersion: model.modelVersion,
|
|
208
|
-
score,
|
|
209
|
-
rawScore,
|
|
210
|
-
capped: score !== rawScore,
|
|
211
|
-
factors,
|
|
212
|
-
families: familyOrder
|
|
213
|
-
.filter((id) => familyTotals.has(id))
|
|
214
|
-
.map((id) => ({ id, contribution: familyTotals.get(id) ?? 0, cap: FAMILY_CAPS[id] ?? null })),
|
|
215
|
-
evidenceQuality: scoreQuality(input),
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
export function explainRiskScore(input) {
|
|
219
|
-
return explainRiskScoreInternal(input, null, CURRENT_RISK_MODEL);
|
|
220
|
-
}
|
|
221
|
-
export function buildRiskScoreSensitivity(input) {
|
|
222
|
-
const baseline = explainRiskScoreInternal(input, null, CURRENT_RISK_MODEL);
|
|
223
|
-
if (!baseline)
|
|
224
|
-
return null;
|
|
225
|
-
const removableFamilies = REMOVABLE_RISK_FAMILIES
|
|
226
|
-
.filter((family) => baseline.families.some((item) => item.id === family && item.contribution > 0));
|
|
227
|
-
const scenarios = removableFamilies.map((excludedFamily) => {
|
|
228
|
-
const result = explainRiskScoreInternal(input, excludedFamily, CURRENT_RISK_MODEL);
|
|
229
|
-
const score = result?.score ?? baseline.score;
|
|
230
|
-
return Object.freeze({ excludedFamily, score, difference: score - baseline.score });
|
|
231
|
-
}).sort((left, right) => left.score - right.score || left.excludedFamily.localeCompare(right.excludedFamily));
|
|
232
|
-
const minimumScenarioScore = scenarios[0]?.score ?? baseline.score;
|
|
233
|
-
const thresholdState = baseline.score < RISK_REVIEW_THRESHOLD
|
|
234
|
-
? 'below'
|
|
235
|
-
: minimumScenarioScore < RISK_REVIEW_THRESHOLD ? 'crosses' : 'stable_above';
|
|
236
|
-
return Object.freeze({
|
|
237
|
-
version: 1,
|
|
238
|
-
baselineScore: baseline.score,
|
|
239
|
-
reviewThreshold: RISK_REVIEW_THRESHOLD,
|
|
240
|
-
minimumScenarioScore,
|
|
241
|
-
thresholdState,
|
|
242
|
-
scenarios: Object.freeze(scenarios),
|
|
243
|
-
limitations: Object.freeze([
|
|
244
|
-
'Each scenario removes one observed evidence family and recalculates family caps and corroboration; it does not predict missing evidence.',
|
|
245
|
-
'The sensitivity review qualifies prioritisation only and does not determine maliciousness, safety, ownership, or intent.',
|
|
246
|
-
]),
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
export function computeRiskScore(input) {
|
|
250
|
-
return explainRiskScore(input)?.score ?? null;
|
|
251
|
-
}
|
|
252
|
-
// Retained only for deterministic offline comparison of reviewed calibration
|
|
253
|
-
// datasets. Runtime lookups always use the current model above.
|
|
254
|
-
export function explainRiskScoreV7(input) {
|
|
255
|
-
return explainRiskScoreInternal(input, null, RISK_MODEL_V7);
|
|
256
|
-
}
|
|
257
|
-
// Retained only for deterministic offline comparison of reviewed calibration
|
|
258
|
-
// datasets. Runtime lookups always use the current model above.
|
|
259
|
-
export function explainRiskScoreV6(input) {
|
|
260
|
-
const state = input.availability ?? input.state;
|
|
261
|
-
if (typeof state !== 'string' || !RISK_STATES.has(state))
|
|
262
|
-
return null;
|
|
263
|
-
const legacyBase = { registered: 10, for_sale: 5, expiring: 8 };
|
|
264
|
-
const factors = [];
|
|
265
|
-
const totals = new Map();
|
|
266
|
-
const add = (family, label, delta) => {
|
|
267
|
-
factors.push({ family, label, delta });
|
|
268
|
-
totals.set(family, (totals.get(family) ?? 0) + delta);
|
|
269
|
-
};
|
|
270
|
-
add('registration', `Base score for “${STATE_LABELS[state] ?? state}”`, legacyBase[state] ?? 0);
|
|
271
|
-
const contextual = new Set();
|
|
272
|
-
const context = mutationContext(input.mutationTypes);
|
|
273
|
-
if (context) {
|
|
274
|
-
add('domain-resemblance', context.label, context.delta);
|
|
275
|
-
contextual.add('domain-resemblance');
|
|
276
|
-
}
|
|
277
|
-
if (input.faviconMatch === true) {
|
|
278
|
-
add('brand-presentation', 'Favicon matches an official Brand Profile site', 18);
|
|
279
|
-
contextual.add('brand-presentation');
|
|
280
|
-
}
|
|
281
|
-
else if (input.faviconNearMatch === true) {
|
|
282
|
-
add('brand-presentation', 'Favicon resembles an official Brand Profile site', 14);
|
|
283
|
-
contextual.add('brand-presentation');
|
|
284
|
-
}
|
|
285
|
-
if (input.reusesOfficialAssets === true) {
|
|
286
|
-
add('brand-presentation', 'Official asset-host relationship observed', 6);
|
|
287
|
-
contextual.add('brand-presentation');
|
|
288
|
-
}
|
|
289
|
-
if (typeof input.phishingLanguageMatch === 'string' && input.phishingLanguageMatch.trim()) {
|
|
290
|
-
add('credential-lure', 'Suspicious urgency language observed', 8);
|
|
291
|
-
contextual.add('credential-lure');
|
|
292
|
-
}
|
|
293
|
-
if (input.hasPasswordField === true) {
|
|
294
|
-
add('credential-lure', 'Login/password form present', 5);
|
|
295
|
-
contextual.add('credential-lure');
|
|
296
|
-
}
|
|
297
|
-
if (contextual.size >= 2)
|
|
298
|
-
add('corroboration', `Corroborating context across ${contextual.size} distinct evidence families`, contextual.size >= 3 ? 20 : 10);
|
|
299
|
-
const external = calibrateExternalIntelligenceRisk(input.threatIntelligence, input.domain);
|
|
300
|
-
if (external.factor)
|
|
301
|
-
add('external-intelligence', external.factor.label, external.contribution);
|
|
302
|
-
if (input.activityStatus === 'active')
|
|
303
|
-
add('operational-support', 'Active site in use', 8);
|
|
304
|
-
if (input.hasMx === true)
|
|
305
|
-
add('operational-support', 'Mail server configured', 8);
|
|
306
|
-
if (input.hasSpf === true && input.hasDmarc === true)
|
|
307
|
-
add('operational-support', 'SPF and DMARC records present', 3);
|
|
308
|
-
else if (input.hasSpf === true || input.hasDmarc === true)
|
|
309
|
-
add('operational-support', 'SPF or DMARC record present', 1);
|
|
310
|
-
if (input.privacyProtected === true)
|
|
311
|
-
add('registration', 'WHOIS privacy protected', 3);
|
|
312
|
-
if (typeof input.domainAgeDays === 'number' && Number.isFinite(input.domainAgeDays) && input.domainAgeDays >= 0) {
|
|
313
|
-
if (input.domainAgeDays < 90)
|
|
314
|
-
add('operational-support', `Recently registered (${formatAge(input.domainAgeDays)})`, 10);
|
|
315
|
-
else if (input.domainAgeDays < 365)
|
|
316
|
-
add('operational-support', `Registered under a year ago (${formatAge(input.domainAgeDays)})`, 4);
|
|
317
|
-
}
|
|
318
|
-
const rawScore = Math.round(factors.reduce((total, factor) => total + factor.delta, 0));
|
|
319
|
-
const score = Math.max(0, Math.min(100, rawScore));
|
|
320
|
-
return {
|
|
321
|
-
modelVersion: 6,
|
|
322
|
-
score,
|
|
323
|
-
rawScore,
|
|
324
|
-
capped: score !== rawScore,
|
|
325
|
-
factors,
|
|
326
|
-
families: [...totals].map(([id, contribution]) => ({ id, contribution, cap: null })),
|
|
327
|
-
evidenceQuality: scoreQuality(input),
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
export function riskTone(score) {
|
|
331
|
-
if (score === null)
|
|
332
|
-
return 'neutral';
|
|
333
|
-
if (score >= RISK_REVIEW_THRESHOLD)
|
|
334
|
-
return 'danger';
|
|
335
|
-
if (score >= 40)
|
|
336
|
-
return 'warn';
|
|
337
|
-
return 'neutral';
|
|
338
|
-
}
|
|
339
|
-
export function formatScoreBreakdown(explained, separator = '\n') {
|
|
340
|
-
if (!explained)
|
|
341
|
-
return '';
|
|
342
|
-
const parts = explained.factors.map((factor) => `${factor.label} ${factor.delta >= 0 ? '+' : ''}${Math.round(factor.delta)}`);
|
|
343
|
-
parts.push(`Total ${explained.score} · Risk model v${explained.modelVersion} · evidence ${explained.evidenceQuality.state}`);
|
|
344
|
-
return parts.join(separator);
|
|
345
|
-
}
|
|
1
|
+
// Retained public import; the runtime-neutral implementation lives in the domain layer.
|
|
2
|
+
export * from "../packages/analysis/risk-scoring.mjs";
|
|
@@ -1,107 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const MAX_FAMILIES = 12;
|
|
4
|
-
const SOURCE_ID_RE = /^[a-z][a-z0-9_-]{0,63}$/u;
|
|
5
|
-
const COMPLETE_STATES = new Set(['complete', 'not_found', 'success']);
|
|
6
|
-
const LIMITED_STATES = new Set(['partial', 'unknown']);
|
|
7
|
-
const UNAVAILABLE_STATES = new Set(['error', 'rate_limited', 'unavailable']);
|
|
8
|
-
const SKIPPED_STATES = new Set(['skipped', 'unsupported']);
|
|
9
|
-
const ACCEPTED_SOURCE_STATES = new Set([
|
|
10
|
-
...COMPLETE_STATES,
|
|
11
|
-
...LIMITED_STATES,
|
|
12
|
-
...UNAVAILABLE_STATES,
|
|
13
|
-
...SKIPPED_STATES,
|
|
14
|
-
]);
|
|
15
|
-
function boundedFamilyList(value) {
|
|
16
|
-
const output = new Set();
|
|
17
|
-
for (const item of value.slice(0, MAX_FAMILIES * 2)) {
|
|
18
|
-
const normalized = String(item).trim().toLowerCase().replace(/[^a-z0-9_-]+/gu, '-').slice(0, 64);
|
|
19
|
-
if (SOURCE_ID_RE.test(normalized))
|
|
20
|
-
output.add(normalized);
|
|
21
|
-
if (output.size >= MAX_FAMILIES)
|
|
22
|
-
break;
|
|
23
|
-
}
|
|
24
|
-
return [...output].sort();
|
|
25
|
-
}
|
|
26
|
-
function sourceCoverage(value) {
|
|
27
|
-
if (!Array.isArray(value))
|
|
28
|
-
return [];
|
|
29
|
-
const output = [];
|
|
30
|
-
const seen = new Set();
|
|
31
|
-
for (const candidate of value.slice(0, MAX_SOURCES * 4)) {
|
|
32
|
-
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate))
|
|
33
|
-
continue;
|
|
34
|
-
const item = candidate;
|
|
35
|
-
const source = typeof item.source === 'string' ? item.source.trim().toLowerCase() : '';
|
|
36
|
-
const state = typeof item.state === 'string' ? item.state.trim().toLowerCase() : '';
|
|
37
|
-
if (!SOURCE_ID_RE.test(source) || seen.has(source))
|
|
38
|
-
continue;
|
|
39
|
-
if (!ACCEPTED_SOURCE_STATES.has(state))
|
|
40
|
-
continue;
|
|
41
|
-
seen.add(source);
|
|
42
|
-
output.push({ source, state });
|
|
43
|
-
if (output.length >= MAX_SOURCES)
|
|
44
|
-
break;
|
|
45
|
-
}
|
|
46
|
-
return output;
|
|
47
|
-
}
|
|
48
|
-
function scanDepth(value) {
|
|
49
|
-
return value === 'deep' || value === 'fast' ? value : 'unknown';
|
|
50
|
-
}
|
|
51
|
-
function observedFreshness(value) {
|
|
52
|
-
if (typeof value !== 'string' || value.length > 64)
|
|
53
|
-
return 'unknown';
|
|
54
|
-
return Number.isFinite(Date.parse(value)) ? 'observed' : 'unknown';
|
|
55
|
-
}
|
|
56
|
-
export function buildScoreEvidenceQuality(input, options) {
|
|
57
|
-
const depth = scanDepth(input.scanDepth);
|
|
58
|
-
const coverage = sourceCoverage(input.sourceCoverage);
|
|
59
|
-
const completeSources = coverage.filter((item) => COMPLETE_STATES.has(item.state)).length;
|
|
60
|
-
const limitedSources = coverage.filter((item) => LIMITED_STATES.has(item.state)).length;
|
|
61
|
-
const unavailableSources = coverage.filter((item) => UNAVAILABLE_STATES.has(item.state)).length;
|
|
62
|
-
const skippedSources = coverage.filter((item) => SKIPPED_STATES.has(item.state)).length;
|
|
63
|
-
const observedFamilies = boundedFamilyList(options.observedFamilies ?? []);
|
|
64
|
-
const expectedFamilies = boundedFamilyList(options.expectedFamilies ?? []);
|
|
65
|
-
const observedSet = new Set(observedFamilies);
|
|
66
|
-
const missingFamilies = expectedFamilies.filter((family) => !observedSet.has(family));
|
|
67
|
-
const limitations = [];
|
|
68
|
-
let state = 'complete';
|
|
69
|
-
if (!coverage.length || depth === 'unknown')
|
|
70
|
-
state = 'unknown';
|
|
71
|
-
else if (unavailableSources || limitedSources)
|
|
72
|
-
state = 'partial';
|
|
73
|
-
else if (options.expectedDepth === 'deep' && depth !== 'deep')
|
|
74
|
-
state = 'limited';
|
|
75
|
-
if (missingFamilies.length && state === 'complete')
|
|
76
|
-
state = 'limited';
|
|
77
|
-
if (options.expectedDepth === 'deep' && depth !== 'deep') {
|
|
78
|
-
limitations.push('A fast or unknown-depth observation does not evaluate every website and mail scoring family.');
|
|
79
|
-
}
|
|
80
|
-
if (limitedSources || unavailableSources) {
|
|
81
|
-
limitations.push('One or more score-relevant sources were partial, unavailable, or indeterminate.');
|
|
82
|
-
}
|
|
83
|
-
if (skippedSources) {
|
|
84
|
-
limitations.push('One or more reported sources were skipped or unsupported and did not contribute to the score.');
|
|
85
|
-
}
|
|
86
|
-
if (!coverage.length)
|
|
87
|
-
limitations.push('Source-level coverage was not supplied with this score.');
|
|
88
|
-
if (missingFamilies.length)
|
|
89
|
-
limitations.push(`No usable observation was supplied for: ${missingFamilies.join(', ')}.`);
|
|
90
|
-
const boundedLimitations = [
|
|
91
|
-
...limitations.slice(0, 5),
|
|
92
|
-
'Evidence coverage qualifies the score; it never adds points or turns missing evidence into a negative finding.',
|
|
93
|
-
];
|
|
94
|
-
return Object.freeze({
|
|
95
|
-
version: SCORE_EVIDENCE_QUALITY_VERSION,
|
|
96
|
-
state,
|
|
97
|
-
scanDepth: depth,
|
|
98
|
-
freshness: observedFreshness(input.observedAt),
|
|
99
|
-
completeSources,
|
|
100
|
-
limitedSources,
|
|
101
|
-
unavailableSources,
|
|
102
|
-
skippedSources,
|
|
103
|
-
observedFamilies: Object.freeze(observedFamilies),
|
|
104
|
-
missingFamilies: Object.freeze(missingFamilies),
|
|
105
|
-
limitations: Object.freeze(boundedLimitations),
|
|
106
|
-
});
|
|
107
|
-
}
|
|
1
|
+
// Retained public import; the runtime-neutral implementation lives in the domain layer.
|
|
2
|
+
export * from "../packages/analysis/scoring-evidence-quality.mjs";
|
package/lib/semantic-version.mjs
CHANGED
|
@@ -1,54 +1,2 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
// errors retain their local context without reimplementing SemVer parsing.
|
|
4
|
-
export const MAX_SEMANTIC_VERSION_LENGTH = 128;
|
|
5
|
-
function fail(label, detail) {
|
|
6
|
-
throw new TypeError(`${label} ${detail}`);
|
|
7
|
-
}
|
|
8
|
-
function validateIdentifierList(value, label, kind, forbidNumericLeadingZero) {
|
|
9
|
-
const identifiers = value.split('.');
|
|
10
|
-
if (identifiers.some((identifier) => identifier.length === 0)) {
|
|
11
|
-
fail(label, `${kind} contains an empty identifier.`);
|
|
12
|
-
}
|
|
13
|
-
for (const identifier of identifiers) {
|
|
14
|
-
if (!/^[0-9A-Za-z-]+$/u.test(identifier)) {
|
|
15
|
-
fail(label, `${kind} contains an invalid identifier.`);
|
|
16
|
-
}
|
|
17
|
-
if (forbidNumericLeadingZero && /^[0-9]+$/u.test(identifier) && identifier.length > 1 && identifier.startsWith('0')) {
|
|
18
|
-
fail(label, `${kind} numeric identifiers must not contain leading zeroes.`);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
export function normalizeBoundedSemanticVersion(value, label = 'Semantic') {
|
|
23
|
-
if (typeof value !== 'string'
|
|
24
|
-
|| value.length === 0
|
|
25
|
-
|| value.length > MAX_SEMANTIC_VERSION_LENGTH
|
|
26
|
-
|| value.trim() !== value) {
|
|
27
|
-
fail(label, 'version must be a bounded semantic-version string.');
|
|
28
|
-
}
|
|
29
|
-
const buildParts = value.split('+');
|
|
30
|
-
if (buildParts.length > 2)
|
|
31
|
-
fail(label, 'version contains more than one build-metadata separator.');
|
|
32
|
-
const [precedence, buildMetadata] = buildParts;
|
|
33
|
-
if (!precedence)
|
|
34
|
-
fail(label, 'version must include numeric precedence.');
|
|
35
|
-
const prereleaseSeparator = precedence.indexOf('-');
|
|
36
|
-
const core = prereleaseSeparator === -1 ? precedence : precedence.slice(0, prereleaseSeparator);
|
|
37
|
-
const prerelease = prereleaseSeparator === -1 ? undefined : precedence.slice(prereleaseSeparator + 1);
|
|
38
|
-
const coreParts = core.split('.');
|
|
39
|
-
if (coreParts.length !== 3 || coreParts.some((part) => !/^(?:0|[1-9][0-9]*)$/u.test(part))) {
|
|
40
|
-
fail(label, 'version must contain major, minor, and patch numbers without leading zeroes.');
|
|
41
|
-
}
|
|
42
|
-
if (prerelease !== undefined)
|
|
43
|
-
validateIdentifierList(prerelease, label, 'prerelease', true);
|
|
44
|
-
if (buildMetadata !== undefined)
|
|
45
|
-
validateIdentifierList(buildMetadata, label, 'build metadata', false);
|
|
46
|
-
return value;
|
|
47
|
-
}
|
|
48
|
-
export function normalizeBoundedStableSemanticVersion(value, label = 'Stable semantic') {
|
|
49
|
-
const version = normalizeBoundedSemanticVersion(value, label);
|
|
50
|
-
if (version.includes('-') || version.includes('+')) {
|
|
51
|
-
fail(label, 'version must not include prerelease or build identifiers.');
|
|
52
|
-
}
|
|
53
|
-
return version;
|
|
54
|
-
}
|
|
1
|
+
// Retained public import; the runtime-neutral implementation lives in the domain layer.
|
|
2
|
+
export * from "../packages/analysis/semantic-version.mjs";
|
|
@@ -376,6 +376,8 @@ function parseServiceBindingDnsResponse(messageValue, expected) {
|
|
|
376
376
|
throw new ServiceBindingDnsError('DNS response question does not match the query');
|
|
377
377
|
}
|
|
378
378
|
const records = [];
|
|
379
|
+
const aliases = new Map();
|
|
380
|
+
const answerOwners = new Set();
|
|
379
381
|
let truncated = false;
|
|
380
382
|
for (let index = 0; index < recordCount; index += 1) {
|
|
381
383
|
const ownerResult = decodeDnsName(message, offset);
|
|
@@ -388,9 +390,18 @@ function parseServiceBindingDnsResponse(messageValue, expected) {
|
|
|
388
390
|
offset += 10;
|
|
389
391
|
const rdataEnd = offset + rdataLength;
|
|
390
392
|
assertRange(message, offset, rdataLength);
|
|
393
|
+
if (index < answerCount && rrClass === DNS_CLASS_IN && rrType === 5) {
|
|
394
|
+
const alias = decodeDnsName(message, offset);
|
|
395
|
+
if (alias.nextOffset !== rdataEnd || alias.name === '.'
|
|
396
|
+
|| (aliases.has(ownerResult.name) && aliases.get(ownerResult.name) !== alias.name)) {
|
|
397
|
+
throw new ServiceBindingDnsError('DNS answer contains an invalid or ambiguous alias');
|
|
398
|
+
}
|
|
399
|
+
aliases.set(ownerResult.name, alias.name);
|
|
400
|
+
}
|
|
391
401
|
if (index < answerCount
|
|
392
402
|
&& rrType === recordTypeCode(expected.type)
|
|
393
403
|
&& rrClass === DNS_CLASS_IN) {
|
|
404
|
+
answerOwners.add(ownerResult.name);
|
|
394
405
|
const parsed = parseServiceBindingRecord(message, ownerResult.name, expected.type, ttl, offset, rdataEnd);
|
|
395
406
|
truncated = parsed.truncated || truncated;
|
|
396
407
|
if (records.length < MAX_SERVICE_BINDING_RECORDS)
|
|
@@ -402,6 +413,22 @@ function parseServiceBindingDnsResponse(messageValue, expected) {
|
|
|
402
413
|
}
|
|
403
414
|
if (offset !== message.length)
|
|
404
415
|
throw new ServiceBindingDnsError('DNS response has trailing bytes');
|
|
416
|
+
const allowedOwners = new Set();
|
|
417
|
+
let owner = question.name;
|
|
418
|
+
while (true) {
|
|
419
|
+
if (allowedOwners.has(owner))
|
|
420
|
+
throw new ServiceBindingDnsError('DNS answer contains a cyclic alias chain');
|
|
421
|
+
allowedOwners.add(owner);
|
|
422
|
+
const next = aliases.get(owner);
|
|
423
|
+
if (!next)
|
|
424
|
+
break;
|
|
425
|
+
if (answerOwners.has(owner))
|
|
426
|
+
throw new ServiceBindingDnsError('DNS alias owner also contains service-binding records');
|
|
427
|
+
owner = next;
|
|
428
|
+
}
|
|
429
|
+
if ([...answerOwners].some(value => !allowedOwners.has(value))) {
|
|
430
|
+
throw new ServiceBindingDnsError('DNS service-binding answer owner is not bound to the query');
|
|
431
|
+
}
|
|
405
432
|
const unique = new Map();
|
|
406
433
|
for (const record of records)
|
|
407
434
|
unique.set(JSON.stringify(record), record);
|
|
@@ -499,6 +526,8 @@ function defaultTcpExchange(query, resolver, options) {
|
|
|
499
526
|
}
|
|
500
527
|
});
|
|
501
528
|
socket.once('connect', () => {
|
|
529
|
+
if (settled)
|
|
530
|
+
return;
|
|
502
531
|
const length = Buffer.alloc(2);
|
|
503
532
|
length.writeUInt16BE(query.length, 0);
|
|
504
533
|
socket.write(Buffer.concat([length, query]));
|
|
@@ -507,6 +536,7 @@ function defaultTcpExchange(query, resolver, options) {
|
|
|
507
536
|
}
|
|
508
537
|
function defaultDnsExchange(query, resolver, options) {
|
|
509
538
|
return new Promise((resolve, reject) => {
|
|
539
|
+
options.signal?.throwIfAborted();
|
|
510
540
|
const socket = createSocket(resolver.family === 6 ? 'udp6' : 'udp4');
|
|
511
541
|
let settled = false;
|
|
512
542
|
const timer = setTimeout(() => {
|
|
@@ -517,6 +547,7 @@ function defaultDnsExchange(query, resolver, options) {
|
|
|
517
547
|
return;
|
|
518
548
|
settled = true;
|
|
519
549
|
clearTimeout(timer);
|
|
550
|
+
options.signal?.removeEventListener('abort', cancelled);
|
|
520
551
|
try {
|
|
521
552
|
socket.close();
|
|
522
553
|
}
|
|
@@ -528,6 +559,12 @@ function defaultDnsExchange(query, resolver, options) {
|
|
|
528
559
|
else
|
|
529
560
|
resolve(response);
|
|
530
561
|
};
|
|
562
|
+
const cancelled = () => finish(options.signal?.reason ?? new Error('DNS UDP query cancelled'));
|
|
563
|
+
options.signal?.addEventListener('abort', cancelled, { once: true });
|
|
564
|
+
if (options.signal?.aborted) {
|
|
565
|
+
cancelled();
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
531
568
|
socket.once('error', (error) => finish(error));
|
|
532
569
|
socket.once('message', (message) => {
|
|
533
570
|
if (message.length > MAX_DNS_MESSAGE_BYTES) {
|
|
@@ -538,6 +575,8 @@ function defaultDnsExchange(query, resolver, options) {
|
|
|
538
575
|
}
|
|
539
576
|
});
|
|
540
577
|
socket.connect(resolver.port, resolver.address, () => {
|
|
578
|
+
if (settled)
|
|
579
|
+
return;
|
|
541
580
|
socket.send(query, (error) => {
|
|
542
581
|
if (error)
|
|
543
582
|
finish(error);
|
|
@@ -565,6 +604,7 @@ function missingResponseError(rcode) {
|
|
|
565
604
|
return new ServiceBindingDnsError(`DNS resolver returned ${names[rcode] || `rcode ${rcode}`}`, 'EDNSFAIL');
|
|
566
605
|
}
|
|
567
606
|
async function resolveServiceBindingRecords(nameValue, type, options = {}) {
|
|
607
|
+
options.signal?.throwIfAborted();
|
|
568
608
|
const name = normalizeQueryName(nameValue);
|
|
569
609
|
const transactionId = options.transactionId ?? randomInt(0x1_0000);
|
|
570
610
|
const query = buildServiceBindingDnsQuery(name, type, transactionId);
|
|
@@ -581,17 +621,20 @@ async function resolveServiceBindingRecords(nameValue, type, options = {}) {
|
|
|
581
621
|
const deadline = Date.now() + timeoutMs;
|
|
582
622
|
let lastError = null;
|
|
583
623
|
for (const resolver of resolvers) {
|
|
624
|
+
options.signal?.throwIfAborted();
|
|
584
625
|
const udpTimeoutMs = deadline - Date.now();
|
|
585
626
|
if (udpTimeoutMs <= 0)
|
|
586
627
|
break;
|
|
587
628
|
try {
|
|
588
|
-
let response = await exchange(query, resolver, { timeoutMs: udpTimeoutMs });
|
|
629
|
+
let response = await exchange(query, resolver, { timeoutMs: udpTimeoutMs, ...(options.signal ? { signal: options.signal } : {}) });
|
|
630
|
+
options.signal?.throwIfAborted();
|
|
589
631
|
if (isTruncatedDnsResponse(response, transactionId)) {
|
|
590
632
|
const tcpTimeoutMs = deadline - Date.now();
|
|
591
633
|
if (tcpTimeoutMs <= 0) {
|
|
592
634
|
throw new ServiceBindingDnsError('DNS service-binding query timed out', 'ETIMEOUT');
|
|
593
635
|
}
|
|
594
|
-
response = await (options.tcpExchange || defaultTcpExchange)(query, resolver, { timeoutMs: tcpTimeoutMs });
|
|
636
|
+
response = await (options.tcpExchange || defaultTcpExchange)(query, resolver, { timeoutMs: tcpTimeoutMs, ...(options.signal ? { signal: options.signal } : {}) });
|
|
637
|
+
options.signal?.throwIfAborted();
|
|
595
638
|
}
|
|
596
639
|
const parsed = parseServiceBindingDnsResponse(response, { transactionId, name, type });
|
|
597
640
|
if (parsed.truncatedResponse)
|
|
@@ -604,6 +647,7 @@ async function resolveServiceBindingRecords(nameValue, type, options = {}) {
|
|
|
604
647
|
return { records: parsed.records, truncated: parsed.truncated };
|
|
605
648
|
}
|
|
606
649
|
catch (error) {
|
|
650
|
+
options.signal?.throwIfAborted();
|
|
607
651
|
lastError = error;
|
|
608
652
|
const code = error && typeof error === 'object' ? String(error.code || '') : '';
|
|
609
653
|
if (code === 'ENODATA' || code === 'ENOTFOUND')
|