@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
|
@@ -62,6 +62,10 @@ export function requireRecord(value, label) {
|
|
|
62
62
|
throw new TypeError(`${label} must be an object.`);
|
|
63
63
|
return value;
|
|
64
64
|
}
|
|
65
|
+
/** Stable ordering for bounded record selection, independent of host collation. */
|
|
66
|
+
export function compareCodeUnits(left, right) {
|
|
67
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
68
|
+
}
|
|
65
69
|
export function requireBoundedString(value, label, maxLength) {
|
|
66
70
|
const normalised = strictBoundedString(value, maxLength);
|
|
67
71
|
if (!normalised) {
|
package/lib/bounded-json.mjs
CHANGED
|
@@ -1,330 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
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
|
-
}
|
|
1
|
+
// Retained public import; the runtime-neutral implementation lives in the domain layer.
|
|
2
|
+
export * from "../packages/analysis/bounded-json.mjs";
|
|
@@ -11,9 +11,9 @@ import { CISA_KEV_CATALOG } from "./generated/cisa-kev-catalog.mjs";
|
|
|
11
11
|
import { createObservation, normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
12
12
|
import { isCveIdentifier } from "../packages/contracts/vulnerability-identifiers.mjs";
|
|
13
13
|
import { BROWSER_LIBRARY_PROFILE_VERSION, MAX_LIBRARY_FINDINGS, } from "./lookup-child-profile-contract.mjs";
|
|
14
|
-
import { MAX_INLINE_SCRIPT_CHARS, MAX_INLINE_SCRIPT_TOTAL_CHARS, MAX_SCRIPT_ELEMENTS, MAX_STATIC_HTML_CHARS, analyzeStaticHtml, } from "./static-html-analysis.mjs";
|
|
14
|
+
import { MAX_INLINE_SCRIPT_CHARS, MAX_INLINE_SCRIPT_TOTAL_CHARS, MAX_SCRIPT_ELEMENTS, MAX_SCRIPT_REFERENCE_LENGTH, MAX_STATIC_HTML_CHARS, analyzeStaticHtml, } from "./static-html-analysis.mjs";
|
|
15
15
|
// Catalogue expressions are reviewed third-party inputs. Keep every individual
|
|
16
|
-
// regular-expression evaluation and
|
|
16
|
+
// regular-expression evaluation and aggregate reference/inline-signature work small
|
|
17
17
|
// enough that a target-controlled script cannot monopolise the Node event loop.
|
|
18
18
|
// Full-content hashes still use the complete already-capped script body.
|
|
19
19
|
const MAX_INLINE_LIBRARY_SCAN_CHARS = 1_024;
|
|
@@ -23,7 +23,6 @@ const MAX_INLINE_LIBRARY_WORKER_BYTES = 64 * 1024;
|
|
|
23
23
|
const MAX_LIBRARY_HTML_CHARS = MAX_STATIC_HTML_CHARS;
|
|
24
24
|
const MAX_ADVISORY_IDENTIFIERS = 16;
|
|
25
25
|
const MAX_WEAKNESS_CLASSES = 12;
|
|
26
|
-
const MAX_MATCHES_PER_PATTERN = 4;
|
|
27
26
|
const MAX_VERSION_LENGTH = 64;
|
|
28
27
|
const COMPONENT_RE = /^[a-z0-9._-]{1,80}$/i;
|
|
29
28
|
const VERSION_RE = /^[0-9][0-9.a-z_-]{0,63}$/i;
|
|
@@ -41,38 +40,61 @@ const KNOWN_EXPLOITED_IDENTIFIERS = new Set(CISA_KEV_CATALOG.identifiers);
|
|
|
41
40
|
const KNOWN_EXPLOITED_RELEASED_AT = normalizeExplicitIsoTimestamp(CISA_KEV_CATALOG.releasedAt);
|
|
42
41
|
if (KNOWN_EXPLOITED_RELEASED_AT === null)
|
|
43
42
|
throw new Error('Pinned known-exploited catalogue has an invalid release timestamp.');
|
|
44
|
-
const
|
|
43
|
+
const PATTERN_EXTRACTOR_CATALOGUE = Object.freeze(Object.entries(CATALOG_COMPONENTS).map(([component, value]) => {
|
|
45
44
|
const extractors = record(record(value).extractors);
|
|
45
|
+
const patterns = (name) => Object.freeze((Array.isArray(extractors[name]) ? extractors[name] : [])
|
|
46
|
+
.filter((pattern) => typeof pattern === 'string' && pattern.length > 0 && pattern.length <= 2_048)
|
|
47
|
+
.slice(0, 64));
|
|
46
48
|
return Object.freeze({
|
|
47
49
|
component,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
.slice(0, 64)),
|
|
51
|
-
replacements: Object.freeze((Array.isArray(extractors.filecontentreplace) ? extractors.filecontentreplace : [])
|
|
52
|
-
.filter((pattern) => typeof pattern === 'string' && pattern.length > 0 && pattern.length <= 2_048)
|
|
53
|
-
.slice(0, 64)),
|
|
50
|
+
uri: patterns('uri'), filename: patterns('filename'),
|
|
51
|
+
patterns: patterns('filecontent'), replacements: patterns('filecontentreplace'),
|
|
54
52
|
});
|
|
55
53
|
}));
|
|
56
|
-
const
|
|
54
|
+
const LIBRARY_REGEX_WORKER_SOURCE = String.raw `
|
|
57
55
|
const { parentPort, workerData } = require('node:worker_threads');
|
|
58
56
|
const versions = /^[0-9][0-9.a-z_-]{0,63}$/i;
|
|
59
57
|
parentPort.on('message', (job) => {
|
|
60
58
|
const control = new Int32Array(job.control);
|
|
61
59
|
const output = new Uint8Array(job.output);
|
|
62
|
-
const matches =
|
|
63
|
-
const add = (component, version) => {
|
|
60
|
+
const matches = new Map();
|
|
61
|
+
const add = (component, version, method) => {
|
|
64
62
|
const normalized = String(version).replace(/(?:\.|-)?min$/i, '').slice(0, 64);
|
|
65
|
-
|
|
63
|
+
const key = component + '\u0000' + normalized;
|
|
64
|
+
if (!versions.test(normalized)) return;
|
|
65
|
+
if (!matches.has(key) && matches.size < 64) matches.set(key, [component, normalized, new Set()]);
|
|
66
|
+
matches.get(key)?.[2].add(method);
|
|
66
67
|
};
|
|
67
68
|
try {
|
|
69
|
+
const input = JSON.parse(job.value);
|
|
70
|
+
for (const reference of input.references) {
|
|
71
|
+
const filename = (reference.replace(/\\/g, '/').split('/').pop() || '').split(/[?#]/, 1)[0];
|
|
72
|
+
for (const entry of workerData.catalogue) {
|
|
73
|
+
for (const [patterns, value, method, anchored] of [
|
|
74
|
+
[entry.uri, reference, 'script URL', false],
|
|
75
|
+
[entry.filename, filename, 'script filename', true],
|
|
76
|
+
]) {
|
|
77
|
+
if (!value) continue;
|
|
78
|
+
for (const pattern of patterns) {
|
|
79
|
+
let expression;
|
|
80
|
+
try { expression = new RegExp(anchored ? '^(?:' + pattern + ')$' : pattern, 'g'); } catch { continue; }
|
|
81
|
+
let match, count = 0;
|
|
82
|
+
while ((match = expression.exec(value)) && count < 4) {
|
|
83
|
+
if (typeof match[1] === 'string' && versions.test(match[1])) { add(entry.component, match[1], method); count += 1; }
|
|
84
|
+
if (match[0] === '') expression.lastIndex += 1;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
68
90
|
for (const entry of workerData.catalogue) {
|
|
69
91
|
for (const pattern of entry.patterns) {
|
|
70
92
|
let expression;
|
|
71
93
|
try { expression = new RegExp(pattern, 'g'); } catch { continue; }
|
|
72
94
|
let match;
|
|
73
95
|
let count = 0;
|
|
74
|
-
while ((match = expression.exec(
|
|
75
|
-
if (typeof match[1] === 'string') add(entry.component, match[1]);
|
|
96
|
+
while ((match = expression.exec(input.inline)) && count < 4) {
|
|
97
|
+
if (typeof match[1] === 'string') add(entry.component, match[1], 'inline signature');
|
|
76
98
|
if (match[0] === '') expression.lastIndex += 1;
|
|
77
99
|
count += 1;
|
|
78
100
|
}
|
|
@@ -84,16 +106,16 @@ parentPort.on('message', (job) => {
|
|
|
84
106
|
try { expression = new RegExp(descriptor[1], 'g'); } catch { continue; }
|
|
85
107
|
let match;
|
|
86
108
|
let count = 0;
|
|
87
|
-
while ((match = expression.exec(
|
|
109
|
+
while ((match = expression.exec(input.inline)) && count < 4) {
|
|
88
110
|
let replaced = match[0];
|
|
89
111
|
try { replaced = match[0].replace(new RegExp(descriptor[1]), descriptor[2]); } catch { break; }
|
|
90
|
-
add(entry.component, replaced);
|
|
112
|
+
add(entry.component, replaced, 'inline signature');
|
|
91
113
|
if (match[0] === '') expression.lastIndex += 1;
|
|
92
114
|
count += 1;
|
|
93
115
|
}
|
|
94
116
|
}
|
|
95
117
|
}
|
|
96
|
-
const encoded = Buffer.from(JSON.stringify(matches));
|
|
118
|
+
const encoded = Buffer.from(JSON.stringify([...matches.values()].flatMap(([component, version, methods]) => [...methods].map(method => [component, version, method]))));
|
|
97
119
|
if (encoded.length > output.length) Atomics.store(control, 0, -2);
|
|
98
120
|
else { output.set(encoded); Atomics.store(control, 0, encoded.length); }
|
|
99
121
|
} catch {
|
|
@@ -102,8 +124,9 @@ parentPort.on('message', (job) => {
|
|
|
102
124
|
parentPort.postMessage({ id: job.id });
|
|
103
125
|
});
|
|
104
126
|
`;
|
|
105
|
-
const
|
|
106
|
-
|
|
127
|
+
const scanLibraryPatterns = createInlineLibraryScanner(() => new Worker(LIBRARY_REGEX_WORKER_SOURCE, { eval: true, workerData: { catalogue: PATTERN_EXTRACTOR_CATALOGUE } }), {
|
|
128
|
+
// JSON can escape each admitted character to six characters; include separators and keys.
|
|
129
|
+
maximumCharacters: 6 * (MAX_INLINE_LIBRARY_SCAN_TOTAL_CHARS + MAX_SCRIPT_ELEMENTS * MAX_SCRIPT_REFERENCE_LENGTH) + MAX_SCRIPT_ELEMENTS * 10 + 128,
|
|
107
130
|
outputBytes: MAX_INLINE_LIBRARY_WORKER_BYTES,
|
|
108
131
|
deadlineMs: MAX_INLINE_LIBRARY_SCAN_MS,
|
|
109
132
|
// One worker/output buffer, with at most sixteen bounded pending inputs.
|
|
@@ -166,26 +189,6 @@ function matchingVulnerabilities(component, version) {
|
|
|
166
189
|
}
|
|
167
190
|
return matched;
|
|
168
191
|
}
|
|
169
|
-
function matchPattern(pattern, value) {
|
|
170
|
-
if (typeof pattern !== 'string' || pattern.length === 0 || pattern.length > 2_048)
|
|
171
|
-
return [];
|
|
172
|
-
let expression;
|
|
173
|
-
try {
|
|
174
|
-
expression = new RegExp(pattern, 'g');
|
|
175
|
-
}
|
|
176
|
-
catch {
|
|
177
|
-
return [];
|
|
178
|
-
}
|
|
179
|
-
const matches = [];
|
|
180
|
-
let match;
|
|
181
|
-
while ((match = expression.exec(value)) && matches.length < MAX_MATCHES_PER_PATTERN) {
|
|
182
|
-
if (typeof match[1] === 'string' && VERSION_RE.test(match[1]))
|
|
183
|
-
matches.push(match[1]);
|
|
184
|
-
if (match[0] === '')
|
|
185
|
-
expression.lastIndex += 1;
|
|
186
|
-
}
|
|
187
|
-
return matches;
|
|
188
|
-
}
|
|
189
192
|
function addDetected(detected, component, version, method) {
|
|
190
193
|
const normalizedVersion = version.replace(/(?:\.|-)?min$/i, '').slice(0, MAX_VERSION_LENGTH);
|
|
191
194
|
if (!COMPONENT_RE.test(component) || !VERSION_RE.test(normalizedVersion))
|
|
@@ -200,38 +203,10 @@ function addDetected(detected, component, version, method) {
|
|
|
200
203
|
return;
|
|
201
204
|
detected.set(key, { component, version: normalizedVersion, detections: new Set([method]) });
|
|
202
205
|
}
|
|
203
|
-
function
|
|
204
|
-
|
|
205
|
-
const component = record(rawComponent);
|
|
206
|
-
const extractors = record(component.extractors);
|
|
207
|
-
const patterns = Array.isArray(extractors[extractorName]) ? extractors[extractorName] : [];
|
|
208
|
-
for (const pattern of patterns.slice(0, 64)) {
|
|
209
|
-
for (const version of matchPattern(pattern, value))
|
|
210
|
-
addDetected(detected, componentName, version, method);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
function scanFilename(detected, reference) {
|
|
215
|
-
const filename = (reference.replace(/\\/g, '/').split('/').pop() || '').split(/[?#]/, 1)[0];
|
|
216
|
-
if (!filename)
|
|
217
|
-
return;
|
|
218
|
-
for (const [componentName, rawComponent] of Object.entries(CATALOG_COMPONENTS)) {
|
|
219
|
-
const component = record(rawComponent);
|
|
220
|
-
const extractors = record(component.extractors);
|
|
221
|
-
const patterns = Array.isArray(extractors.filename) ? extractors.filename : [];
|
|
222
|
-
for (const pattern of patterns.slice(0, 64)) {
|
|
223
|
-
if (typeof pattern !== 'string' || pattern.length > 2_048)
|
|
224
|
-
continue;
|
|
225
|
-
for (const version of matchPattern(`^(?:${pattern})$`, filename)) {
|
|
226
|
-
addDetected(detected, componentName, version, 'script filename');
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
async function scanInlineSignatures(detected, value, signal) {
|
|
232
|
-
if (!value)
|
|
206
|
+
async function scanSignatures(detected, value, references, signal) {
|
|
207
|
+
if (!value && !references.length)
|
|
233
208
|
return { timedOut: false, unavailable: false };
|
|
234
|
-
const result = await
|
|
209
|
+
const result = await scanLibraryPatterns(JSON.stringify({ inline: value, references }), signal);
|
|
235
210
|
signal?.throwIfAborted();
|
|
236
211
|
if (result.output === null)
|
|
237
212
|
return result;
|
|
@@ -239,12 +214,13 @@ async function scanInlineSignatures(detected, value, signal) {
|
|
|
239
214
|
const parsed = JSON.parse(result.output);
|
|
240
215
|
if (!Array.isArray(parsed))
|
|
241
216
|
return { timedOut: false, unavailable: true };
|
|
242
|
-
for (const item of parsed.slice(0, MAX_LIBRARY_FINDINGS * 4)) {
|
|
243
|
-
if (!Array.isArray(item) || item.length !==
|
|
217
|
+
for (const item of parsed.slice(0, MAX_LIBRARY_FINDINGS * 4 * 3)) {
|
|
218
|
+
if (!Array.isArray(item) || item.length !== 3)
|
|
244
219
|
continue;
|
|
245
|
-
const [component, version] = item;
|
|
246
|
-
if (typeof component === 'string' && typeof version === 'string'
|
|
247
|
-
|
|
220
|
+
const [component, version, method] = item;
|
|
221
|
+
if (typeof component === 'string' && typeof version === 'string'
|
|
222
|
+
&& (method === 'inline signature' || method === 'script URL' || method === 'script filename')) {
|
|
223
|
+
addDetected(detected, component, version, method);
|
|
248
224
|
}
|
|
249
225
|
}
|
|
250
226
|
return { timedOut: false, unavailable: false };
|
|
@@ -339,11 +315,11 @@ async function analyzeBrowserLibraries(input = {}) {
|
|
|
339
315
|
let inlineSignatureCharactersExamined = 0;
|
|
340
316
|
let inlineSignatureLimitReached = false;
|
|
341
317
|
const inlineSignatureSamples = [];
|
|
318
|
+
const references = [];
|
|
342
319
|
for (const script of htmlAnalysis.scripts) {
|
|
343
320
|
if (script.reference) {
|
|
344
321
|
referencesExamined += 1;
|
|
345
|
-
|
|
346
|
-
scanFilename(detected, script.reference);
|
|
322
|
+
references.push(script.reference);
|
|
347
323
|
}
|
|
348
324
|
else if (script.inlineContent && script.mediaType !== 'application/ld+json') {
|
|
349
325
|
inlineScriptsExamined += 1;
|
|
@@ -355,7 +331,7 @@ async function analyzeBrowserLibraries(input = {}) {
|
|
|
355
331
|
inlineSignatureSamples.push(signatureScan.signatureContent);
|
|
356
332
|
}
|
|
357
333
|
}
|
|
358
|
-
const inlineSignatureResult = await
|
|
334
|
+
const inlineSignatureResult = await scanSignatures(detected, inlineSignatureSamples.join('\u0000'), references, input.signal);
|
|
359
335
|
input.signal?.throwIfAborted();
|
|
360
336
|
const inlineSignatureUnavailable = inlineSignatureResult.timedOut || inlineSignatureResult.unavailable;
|
|
361
337
|
const projected = [...detected.values()].map(findingFromDetection);
|
|
@@ -395,9 +371,9 @@ async function analyzeBrowserLibraries(input = {}) {
|
|
|
395
371
|
if (inlineSignatureLimitReached)
|
|
396
372
|
limitations.push(`Passive library signature matching evaluated a deterministic ${MAX_INLINE_LIBRARY_SCAN_TOTAL_CHARS}-character cumulative sample across inline scripts; full-content hashes still used the complete bounded script content.`);
|
|
397
373
|
if (inlineSignatureResult.timedOut)
|
|
398
|
-
limitations.push(`Passive library signature matching exceeded its ${MAX_INLINE_LIBRARY_SCAN_MS} ms isolated-worker deadline; hash evidence was still evaluated.`);
|
|
374
|
+
limitations.push(`Passive library URL, filename and inline signature matching exceeded its ${MAX_INLINE_LIBRARY_SCAN_MS} ms isolated-worker deadline; hash evidence was still evaluated.`);
|
|
399
375
|
if (inlineSignatureResult.unavailable)
|
|
400
|
-
limitations.push('Passive library signature matching was unavailable in its isolated worker; hash evidence was still evaluated.');
|
|
376
|
+
limitations.push('Passive library URL, filename and inline signature matching was unavailable in its isolated worker; hash evidence was still evaluated.');
|
|
401
377
|
if (findingLimitReached)
|
|
402
378
|
limitations.push(`Only the first ${MAX_LIBRARY_FINDINGS} library findings were retained.`);
|
|
403
379
|
if (omittedCveIdentifiers)
|
|
@@ -428,8 +404,10 @@ async function analyzeBrowserLibraries(input = {}) {
|
|
|
428
404
|
inlineScriptsExamined,
|
|
429
405
|
inlineCharactersExamined: htmlAnalysis.inlineCharactersExamined,
|
|
430
406
|
inlineSignatureCharactersExamined,
|
|
431
|
-
inlineSignatureTimedOut: inlineSignatureResult.timedOut,
|
|
432
|
-
inlineSignatureUnavailable: inlineSignatureResult.unavailable,
|
|
407
|
+
inlineSignatureTimedOut: inlineSignatureSamples.length > 0 && inlineSignatureResult.timedOut,
|
|
408
|
+
inlineSignatureUnavailable: inlineSignatureSamples.length > 0 && inlineSignatureResult.unavailable,
|
|
409
|
+
referenceSignatureTimedOut: references.length > 0 && inlineSignatureResult.timedOut,
|
|
410
|
+
referenceSignatureUnavailable: references.length > 0 && inlineSignatureResult.unavailable,
|
|
433
411
|
catalogComponents: Object.keys(CATALOG_COMPONENTS).length,
|
|
434
412
|
findings: allFindings.length,
|
|
435
413
|
advisoryMatches: allFindings.reduce((sum, finding) => sum + finding.advisoryCount, 0),
|
package/lib/classify.mjs
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { isIP } from 'node:net';
|
|
17
17
|
import { domainToASCII } from 'node:url';
|
|
18
18
|
import { parse } from 'tldts';
|
|
19
|
-
import { canonicalRegistrableDomain } from "
|
|
19
|
+
import { canonicalRegistrableDomain } from "../packages/analysis/registrable-domain.mjs";
|
|
20
20
|
// A CR/LF here would let one query become multiple lines once it reaches
|
|
21
21
|
// lib/whois.mts's raw `socket.write(query + '\r\n')` - a normal URL-encoded
|
|
22
22
|
// query string (?q=example.com%0D%0AHELP) decodes back into literal control
|
package/lib/ct-query.mjs
CHANGED
|
@@ -1,27 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const CT_QUERY_ERROR_MESSAGE = `Certificate Transparency query must be at most ${MAX_CT_QUERY_LENGTH} characters and contain no control or invisible formatting characters.`;
|
|
4
|
-
const UNSAFE_QUERY_CHARACTER_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/u;
|
|
5
|
-
class CtQueryError extends Error {
|
|
6
|
-
code = CT_QUERY_ERROR_CODE;
|
|
7
|
-
constructor() {
|
|
8
|
-
super(CT_QUERY_ERROR_MESSAGE);
|
|
9
|
-
this.name = 'CtQueryError';
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
function normalizeCtQuery(value) {
|
|
13
|
-
if (value === undefined || value === null)
|
|
14
|
-
return '';
|
|
15
|
-
if (typeof value !== 'string'
|
|
16
|
-
|| value.length > MAX_CT_QUERY_LENGTH
|
|
17
|
-
|| UNSAFE_QUERY_CHARACTER_RE.test(value)) {
|
|
18
|
-
throw new CtQueryError();
|
|
19
|
-
}
|
|
20
|
-
return value.trim();
|
|
21
|
-
}
|
|
22
|
-
function isCtQueryError(value) {
|
|
23
|
-
return value instanceof Error
|
|
24
|
-
&& 'code' in value
|
|
25
|
-
&& value.code === CT_QUERY_ERROR_CODE;
|
|
26
|
-
}
|
|
27
|
-
export { CT_QUERY_ERROR_CODE, CT_QUERY_ERROR_MESSAGE, CtQueryError, isCtQueryError, MAX_CT_QUERY_LENGTH, normalizeCtQuery, };
|
|
1
|
+
// Retained public import; the runtime-neutral implementation lives in the domain layer.
|
|
2
|
+
export * from "../packages/analysis/ct-query.mjs";
|
|
@@ -1,9 +1,2 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
// not accidentally clipped by a stale client-side mirror.
|
|
4
|
-
export const MAX_CT_RESPONSE_RESULTS = 500;
|
|
5
|
-
export const MAX_CT_RESPONSE_HOSTNAMES_PER_MATCH = 50;
|
|
6
|
-
export const MAX_CT_RESPONSE_TIMESTAMP_LENGTH = 64;
|
|
7
|
-
export const MAX_CT_RESPONSE_CERTIFICATE_GROUPS = 100;
|
|
8
|
-
export const MAX_CT_RESPONSE_DOMAINS_PER_GROUP = 20;
|
|
9
|
-
export const MAX_CT_RESPONSE_HOSTNAMES_PER_GROUP = 30;
|
|
1
|
+
// Retained public import; the runtime-neutral implementation lives in the domain layer.
|
|
2
|
+
export * from "../packages/analysis/ct-response-bounds.mjs";
|