@slicedearth/whoisleuth-cli 1.45.0 → 2.0.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 +7 -3
- package/NOTICE +5 -4
- package/README.md +74 -124
- package/SECURITY.md +6 -3
- package/bin/whoisleuth.mjs +7 -4
- package/cli/archive-inspect.mjs +28 -10
- package/cli/arguments.mjs +450 -173
- package/cli/artifact-structure.mjs +2507 -0
- package/cli/artifact-verify.mjs +286 -64
- package/cli/bounded-json.mjs +1 -0
- package/cli/bulk-checkpoint.mjs +35 -23
- package/cli/bulk-command-runner.mjs +2 -5
- package/cli/bulk.mjs +9 -2
- package/cli/case-pack.mjs +435 -46
- package/cli/ci-report.mjs +6 -1
- package/cli/collection-preflight.mjs +2 -2
- package/cli/command-catalogue.mjs +12 -3
- package/cli/command-reference.mjs +948 -106
- package/cli/completion.mjs +504 -218
- package/cli/config-profile.mjs +13 -3
- package/cli/ct-event-intake.mjs +32 -2
- package/cli/discover.mjs +7 -1
- package/cli/discovery-observation-snapshot.mjs +24 -28
- package/cli/discovery-scan-command-runner.mjs +13 -14
- package/cli/discovery-scan.mjs +17 -3
- package/cli/discovery-snapshot.mjs +15 -7
- package/cli/discovery-workflow.mjs +2 -3
- package/cli/doctor.mjs +2 -2
- package/cli/domain-control-monitor.mjs +215 -36
- package/cli/domain-control-observations.mjs +34 -17
- package/cli/errors.mjs +8 -3
- package/cli/evidence-command-runner.mjs +17 -11
- package/cli/evidence-signing.mjs +68 -50
- package/cli/export-evidence.mjs +11 -4
- package/cli/external-observation-mapping.mjs +3 -3
- package/cli/formatters/html.mjs +3 -3
- package/cli/formatters/json.mjs +20 -32
- package/cli/formatters/terminal.mjs +586 -22
- package/cli/input.mjs +62 -0
- package/cli/interactive-launcher.mjs +226 -0
- package/cli/interchange-report.mjs +226 -0
- package/cli/investigation-manifest.mjs +16 -17
- package/cli/investigation-plan.mjs +167 -16
- package/cli/investigation-run.mjs +80 -24
- package/cli/local-mmdb-review.mjs +45 -14
- package/cli/lookalike-calibration.mjs +3 -1
- package/cli/lookup-brief.mjs +13 -8
- package/cli/lookup-browser.mjs +844 -0
- package/cli/lookup-command-runner.mjs +96 -33
- package/cli/lookup-diff.mjs +11 -2
- package/cli/lookup-plan.mjs +1 -0
- package/cli/mail-review.mjs +206 -94
- package/cli/manual.mjs +5 -5
- package/cli/network-command-runner.mjs +78 -0
- package/cli/offline-evidence-review.mjs +45 -14
- package/cli/open-asset-model-bridge.mjs +1 -1
- package/cli/output-file.mjs +70 -14
- package/cli/page-compare.mjs +18 -10
- package/cli/progress-events.mjs +2 -1
- package/cli/progress.mjs +3 -3
- package/cli/registry-cohort.mjs +343 -61
- package/cli/registry-doctor.mjs +67 -28
- package/cli/registry-support.mjs +2 -1
- package/cli/retained-artifact-diff.mjs +375 -0
- package/cli/risk-calibration.mjs +89 -40
- package/cli/runner.mjs +322 -246
- package/cli/saved-lookup.mjs +439 -49
- package/cli/sharing-review.mjs +18 -7
- package/cli/source-reliability.mjs +75 -36
- package/cli/terminal-presentation.mjs +43 -18
- package/docs/cli-reference.md +125 -1334
- package/docs/cli.md +87 -236
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -0
- package/frontend/src/lib/analysis/domain-control-records.js +3 -0
- package/lib/application-version.mjs +3 -0
- package/lib/availability.mjs +72 -52
- package/lib/bounded-contract-normalizers.mjs +2 -4
- package/lib/bounded-file.mjs +91 -4
- package/lib/bounded-json.mjs +287 -0
- package/lib/browser-library-profile.mjs +1 -1
- package/lib/certificate-extension-profile.mjs +2 -1
- package/lib/classify.mjs +56 -5
- package/lib/client-behavior-profile.mjs +1 -1
- package/lib/credential-surface-profile.mjs +1 -1
- package/lib/cryptographic-assurance.mjs +173 -0
- package/lib/ct-query.mjs +3 -3
- package/lib/ct-search.mjs +120 -64
- package/lib/distributed-operation-budget.mjs +4 -2
- package/lib/dns-delegation-health.mjs +9 -6
- package/lib/dns-intelligence.mjs +4 -3
- package/lib/dns-resolver-selection.mjs +6 -4
- package/lib/dnssec-chain-validation.mjs +1052 -0
- package/lib/dnssec-evidence-validation.mjs +23 -10
- package/lib/domain-assurance.mjs +12 -12
- package/lib/domain-change-packet.mjs +7 -5
- package/lib/domain-control-flight-recorder.mjs +442 -78
- package/lib/domain-control-manifest.mjs +314 -171
- package/lib/domain-posture-analysis.mjs +5 -2
- package/lib/domain-posture.mjs +7 -4
- package/lib/encrypted-dns-contract.mjs +2 -3
- package/lib/evidence-export.mjs +1243 -88
- package/lib/evidence-report-markdown.mjs +3 -0
- package/lib/evidence-report.mjs +5 -0
- package/lib/external-intelligence-risk.mjs +26 -10
- package/lib/favicon.mjs +8 -5
- package/lib/feature-policy.mjs +1 -1
- package/lib/homepage-metadata-contract.mjs +290 -0
- package/lib/hostname.mjs +1 -1
- package/lib/html-signals.mjs +89 -5
- package/lib/http-evidence-bounds.mjs +3 -0
- package/lib/http-intelligence.mjs +332 -6
- package/lib/interchange-fidelity-registry.mjs +124 -0
- package/lib/local-geoip-evidence.mjs +2 -1
- package/lib/lookup-cache.mjs +6 -4
- package/lib/lookup-network-evidence-bounds.mjs +9 -0
- package/lib/lookup.mjs +27 -4
- package/lib/observed-network-context.mjs +1 -1
- package/lib/outbound-identity.mjs +2 -3
- package/lib/page-fingerprints.mjs +2 -2
- package/lib/page-role-profile.mjs +1 -1
- package/lib/perceptual-hash.mjs +12 -4
- package/lib/portable-generator.mjs +29 -0
- package/lib/provider-policy-admission.mjs +11 -4
- package/lib/rdap-bootstrap.mjs +76 -16
- package/lib/rdap-client.mjs +20 -12
- package/lib/rdap-normalization.mjs +324 -80
- package/lib/rdap-registrar.mjs +5 -1
- package/lib/rdap-transport.mjs +2 -2
- package/lib/rdap-validation.mjs +65 -14
- package/lib/rdap.mjs +2 -2
- package/lib/registrable-domain.mjs +36 -0
- package/lib/registry-capabilities.mjs +3 -0
- package/lib/registry-capability-catalogue.mjs +12 -2
- package/lib/registry-comparison.mjs +28 -11
- package/lib/registry-insights.mjs +7 -0
- package/lib/response-policy.mjs +27 -4
- package/lib/risk-calibration-summary.mjs +340 -0
- package/lib/risk-scoring.mjs +4 -3
- package/lib/rpki-evidence.mjs +9 -8
- package/lib/safe-fetch.mjs +121 -50
- package/lib/security-txt.mjs +27 -4
- package/lib/service-binding-dns.mjs +6 -1
- package/lib/smtp-transport-review.mjs +787 -0
- package/lib/sslbl-certificates.generated.mjs +136 -134
- package/lib/sslbl-intelligence.mjs +6 -8
- package/lib/static-html-analysis.mjs +301 -4
- package/lib/structured-data-identity.mjs +51 -19
- package/lib/threat-intelligence-fixtures.mjs +2 -4
- package/lib/threat-intelligence-manifests.mjs +2 -7
- package/lib/threat-intelligence-runtime.mjs +5 -15
- package/lib/threat-intelligence-types.mjs +21 -1
- package/lib/threatfox-intelligence.mjs +7 -3
- package/lib/tls-intelligence.mjs +15 -9
- package/lib/tlsa-evidence.mjs +37 -7
- package/lib/urlhaus-intelligence.mjs +4 -2
- package/lib/urlscan-intelligence.mjs +6 -5
- package/lib/website-security-posture.mjs +1 -1
- package/lib/website-technology.mjs +1 -1
- package/lib/whois-authority.mjs +43 -8
- package/lib/whois-chain.mjs +25 -3
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-transport.mjs +12 -3
- package/lib/zone-intent-review.mjs +1 -1
- package/package.json +12 -4
- package/packages/cases/case-brand-profile-references.mjs +85 -0
- package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +43 -14
- package/packages/cases/case-investigation-branch-model.mjs +132 -0
- package/packages/cases/case-migration-model.mjs +476 -0
- package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
- package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -19
- package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
- package/packages/cases/case-record-model.mjs +9 -0
- package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +106 -33
- package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +143 -16
- package/packages/cases/case-response-model.mjs +1604 -0
- package/packages/cases/case-response-packet.mjs +1312 -0
- package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
- package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
- package/packages/cases/opaque-reference-id.mjs +9 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +366 -0
- package/packages/comparison/comparison-ledger-contract.mjs +241 -0
- package/packages/comparison/comparison-ledger-json.mjs +80 -0
- package/packages/comparison/comparison-ledger-serialization.mjs +52 -0
- package/packages/comparison/favicon-similarity.mjs +78 -0
- package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
- package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
- package/packages/contracts/analyst-review-state-contract.mjs +61 -0
- package/packages/contracts/analyst-review-state.mjs +50 -0
- package/packages/contracts/case-portability.mjs +1310 -0
- package/packages/contracts/cli-command-catalogue.mjs +2 -0
- package/packages/contracts/cli-lookup.mjs +394 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
- package/packages/contracts/domain-control-manifest.mjs +1027 -0
- package/packages/contracts/domain-control-monitor.mjs +310 -0
- package/packages/contracts/domain-control-review.mjs +828 -0
- package/packages/contracts/domain-name.mjs +4 -0
- package/packages/contracts/external-observation-interchange.mjs +224 -0
- package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
- package/packages/contracts/investigation-portability.mjs +331 -0
- package/packages/contracts/offline-comparison.mjs +160 -0
- package/packages/contracts/risk-calibration.mjs +1288 -0
- package/packages/contracts/schema-compatibility.mjs +71 -0
- package/packages/contracts/schema-lifecycle.mjs +1702 -0
- package/packages/contracts/sslbl-snapshot.mjs +17 -0
- package/packages/contracts/workspace-portability.mjs +1347 -0
- package/packages/evidence/artifact-integrity.mjs +65 -0
- package/packages/evidence/decision-fact.mjs +735 -0
- package/packages/evidence/domain-control-runtime.mjs +547 -0
- package/packages/evidence/domain-name.mjs +41 -0
- package/packages/evidence/observation.mjs +384 -0
- package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +86 -20
- package/packages/interchange/retained-text.mjs +8 -0
- package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +11 -8
- package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +28 -9
- package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +67 -18
- package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +10 -9
- package/packages/investigation/bulk-review-policy.mjs +1 -0
- package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +50 -18
- package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
- package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
- package/packages/investigation/lookup-claim-passport.mjs +232 -0
- package/packages/investigation/lookup-claim-readiness.mjs +287 -0
- package/packages/investigation/lookup-investigation-brief.mjs +172 -0
- package/packages/monitoring/analyst-review-state.mjs +680 -0
- package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +97 -50
- package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +15 -15
- package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +179 -39
- package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +12 -5
- package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
- package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
- package/packages/workspace/hostile-input.mjs +172 -0
- package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +51 -46
- package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +15 -15
- package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
- package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
- package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
- package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
- package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
- package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +18 -12
- package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
- package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +195 -84
- package/third-party-notices.txt +112 -5834
- package/frontend/src/lib/analysis/artifact-integrity.js +0 -21
- package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
- package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
- package/frontend/src/lib/analysis/case-migration-model.js +0 -253
- package/frontend/src/lib/analysis/case-record-model.js +0 -7
- package/frontend/src/lib/analysis/case-response-model.js +0 -669
- package/frontend/src/lib/analysis/case-response-packet.js +0 -573
- package/frontend/src/lib/analysis/lookup-decision-support.js +0 -487
- package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
- package/frontend/src/lib/analysis/lookup-presentation.js +0 -58
- package/frontend/src/lib/analysis/lookup-response.js +0 -2
- package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
- package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
- package/frontend/src/lib/analysis/scoring.js +0 -55
- package/frontend/src/lib/analysis/utils.js +0 -232
- package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
- package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
- package/frontend/src/lib/analysis/visualization-matrix.js +0 -88
- package/frontend/src/lib/analysis/visualization-models.js +0 -369
- package/frontend/src/lib/analysis/visualization-time-series.js +0 -144
- package/lib/bounded-json-response.mjs +0 -157
- package/lib/lookup-response-contract.mjs +0 -346
- package/lib/observation.mjs +0 -114
- /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
- /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
- /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
package/lib/bounded-file.mjs
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { constants } from 'node:fs';
|
|
2
|
-
import { open } from 'node:fs/promises';
|
|
2
|
+
import { lstat, open, realpath } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
3
4
|
const MAX_BOUNDED_FILE_BYTES = 64 * 1024 * 1024;
|
|
4
5
|
function boundedSize(value, fallback) {
|
|
5
6
|
return Number.isSafeInteger(value) && Number(value) >= 0 && Number(value) <= MAX_BOUNDED_FILE_BYTES
|
|
6
7
|
? Number(value)
|
|
7
8
|
: fallback;
|
|
8
9
|
}
|
|
9
|
-
|
|
10
|
+
function throwIfAborted(signal) {
|
|
11
|
+
if (!signal?.aborted)
|
|
12
|
+
return;
|
|
13
|
+
throw signal.reason instanceof Error
|
|
14
|
+
? signal.reason
|
|
15
|
+
: new DOMException('The operation was aborted.', 'AbortError');
|
|
16
|
+
}
|
|
17
|
+
async function readBoundedRegularFileIdentity(filePath, options, dependencies = {}) {
|
|
10
18
|
const label = options.label?.trim().slice(0, 120) || 'Input file';
|
|
11
19
|
const maximumBytes = boundedSize(options.maximumBytes, 0);
|
|
12
20
|
const minimumBytes = boundedSize(options.minimumBytes, 0);
|
|
@@ -19,6 +27,7 @@ export async function readBoundedRegularFile(filePath, options, dependencies = {
|
|
|
19
27
|
if (expectedBytes !== null && (expectedBytes < minimumBytes || expectedBytes > maximumBytes)) {
|
|
20
28
|
throw new TypeError(`${label} expected size is outside its byte boundary.`);
|
|
21
29
|
}
|
|
30
|
+
throwIfAborted(options.signal);
|
|
22
31
|
// Non-blocking open prevents a named pipe from stalling the process before
|
|
23
32
|
// the descriptor can be classified. Final-component symlinks remain denied
|
|
24
33
|
// unless an analyst-facing input explicitly opts in; artefacts resolved
|
|
@@ -28,7 +37,9 @@ export async function readBoundedRegularFile(filePath, options, dependencies = {
|
|
|
28
37
|
| (options.allowSymbolicLink ? 0 : constants.O_NOFOLLOW);
|
|
29
38
|
const handle = await (dependencies.openFile ?? open)(filePath, flags);
|
|
30
39
|
try {
|
|
40
|
+
throwIfAborted(options.signal);
|
|
31
41
|
const before = await handle.stat();
|
|
42
|
+
throwIfAborted(options.signal);
|
|
32
43
|
if (!before.isFile())
|
|
33
44
|
throw new TypeError(`${label} must be a regular file.`);
|
|
34
45
|
if (before.size < minimumBytes)
|
|
@@ -44,12 +55,15 @@ export async function readBoundedRegularFile(filePath, options, dependencies = {
|
|
|
44
55
|
const storage = Buffer.allocUnsafe(Math.min(maximumBytes + 1, before.size + 1));
|
|
45
56
|
let offset = 0;
|
|
46
57
|
while (offset < storage.length) {
|
|
58
|
+
throwIfAborted(options.signal);
|
|
47
59
|
const { bytesRead } = await handle.read(storage, offset, storage.length - offset, null);
|
|
48
60
|
if (bytesRead === 0)
|
|
49
61
|
break;
|
|
50
62
|
offset += bytesRead;
|
|
51
63
|
}
|
|
64
|
+
throwIfAborted(options.signal);
|
|
52
65
|
const after = await handle.stat();
|
|
66
|
+
throwIfAborted(options.signal);
|
|
53
67
|
if (offset > maximumBytes
|
|
54
68
|
|| after.size !== before.size
|
|
55
69
|
|| offset !== after.size
|
|
@@ -57,13 +71,86 @@ export async function readBoundedRegularFile(filePath, options, dependencies = {
|
|
|
57
71
|
|| after.ctimeMs !== before.ctimeMs) {
|
|
58
72
|
throw new TypeError(`${label} changed while it was being read.`);
|
|
59
73
|
}
|
|
60
|
-
return
|
|
74
|
+
return {
|
|
75
|
+
bytes: Buffer.from(storage.subarray(0, offset)),
|
|
76
|
+
device: before.dev,
|
|
77
|
+
inode: before.ino,
|
|
78
|
+
};
|
|
61
79
|
}
|
|
62
80
|
finally {
|
|
63
81
|
await handle.close();
|
|
64
82
|
}
|
|
65
83
|
}
|
|
84
|
+
export async function readBoundedRegularFile(filePath, options, dependencies = {}) {
|
|
85
|
+
return (await readBoundedRegularFileIdentity(filePath, options, dependencies)).bytes;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Decode admitted bytes without replacement-character normalisation. The BOM
|
|
89
|
+
* is deliberately retained so re-encoding the returned string preserves the
|
|
90
|
+
* exact bytes used by artefact identity and manifest checks.
|
|
91
|
+
*/
|
|
92
|
+
export function decodeBoundedUtf8(bytes, label = 'Input') {
|
|
93
|
+
try {
|
|
94
|
+
return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
throw new TypeError(`${label.trim().slice(0, 120) || 'Input'} must contain valid UTF-8 text.`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
66
100
|
export async function readBoundedRegularTextFile(filePath, options, dependencies = {}) {
|
|
67
|
-
return (await readBoundedRegularFile(filePath, options, dependencies)
|
|
101
|
+
return decodeBoundedUtf8(await readBoundedRegularFile(filePath, options, dependencies), options.label);
|
|
102
|
+
}
|
|
103
|
+
function safeRelativeFilePath(value, label) {
|
|
104
|
+
if (!value || path.isAbsolute(value) || value.includes('\\')
|
|
105
|
+
|| value.split('/').some((part) => !part || part === '.' || part === '..')) {
|
|
106
|
+
throw new TypeError(`${label} must be a safe relative file path.`);
|
|
107
|
+
}
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Reads one regular file beneath a canonical root while rejecting final and
|
|
112
|
+
* intermediate symbolic links. The ordinary descriptor-level size/change
|
|
113
|
+
* checks still apply after path confinement is established.
|
|
114
|
+
*/
|
|
115
|
+
export async function readBoundedRegularFileWithin(rootDirectory, relativeFilePath, options, dependencies = {}) {
|
|
116
|
+
const label = options.label?.trim().slice(0, 120) || 'Input file';
|
|
117
|
+
const relativePath = safeRelativeFilePath(relativeFilePath, label);
|
|
118
|
+
const canonicalRoot = await realpath(rootDirectory);
|
|
119
|
+
const targetPath = path.resolve(canonicalRoot, relativePath);
|
|
120
|
+
const confined = path.relative(canonicalRoot, targetPath);
|
|
121
|
+
if (!confined || confined.startsWith(`..${path.sep}`) || confined === '..' || path.isAbsolute(confined)) {
|
|
122
|
+
throw new TypeError(`${label} escapes its configured root.`);
|
|
123
|
+
}
|
|
124
|
+
let cursor = canonicalRoot;
|
|
125
|
+
const components = relativePath.split('/');
|
|
126
|
+
let admittedDevice = -1;
|
|
127
|
+
let admittedInode = -1;
|
|
128
|
+
for (const [index, component] of components.entries()) {
|
|
129
|
+
cursor = path.join(cursor, component);
|
|
130
|
+
const metadata = await lstat(cursor);
|
|
131
|
+
if (metadata.isSymbolicLink())
|
|
132
|
+
throw new TypeError(`${label} must not traverse a symbolic link.`);
|
|
133
|
+
if (index === components.length - 1) {
|
|
134
|
+
admittedDevice = metadata.dev;
|
|
135
|
+
admittedInode = metadata.ino;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const canonicalTarget = await realpath(targetPath);
|
|
139
|
+
if (canonicalTarget !== targetPath)
|
|
140
|
+
throw new TypeError(`${label} must not traverse a symbolic link.`);
|
|
141
|
+
const identity = await readBoundedRegularFileIdentity(targetPath, {
|
|
142
|
+
...options,
|
|
143
|
+
allowSymbolicLink: false,
|
|
144
|
+
}, dependencies);
|
|
145
|
+
const finalMetadata = await lstat(targetPath);
|
|
146
|
+
if (finalMetadata.isSymbolicLink()
|
|
147
|
+
|| identity.device !== admittedDevice
|
|
148
|
+
|| identity.inode !== admittedInode
|
|
149
|
+
|| identity.device !== finalMetadata.dev
|
|
150
|
+
|| identity.inode !== finalMetadata.ino
|
|
151
|
+
|| await realpath(targetPath) !== canonicalTarget) {
|
|
152
|
+
throw new TypeError(`${label} changed while it was being read.`);
|
|
153
|
+
}
|
|
154
|
+
return identity.bytes;
|
|
68
155
|
}
|
|
69
156
|
export { MAX_BOUNDED_FILE_BYTES };
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
export const MAX_BOUNDED_JSON_DEPTH = 48;
|
|
2
|
+
export const MAX_BOUNDED_JSON_KEYS = 50_000;
|
|
3
|
+
export const MAX_BOUNDED_JSON_VALUES = 100_000;
|
|
4
|
+
export const MAX_BOUNDED_JSON_CONTAINER_ITEMS = 10_000;
|
|
5
|
+
export const UNSAFE_JSON_OBJECT_KEYS = Object.freeze(['__proto__']);
|
|
6
|
+
function syntaxError() {
|
|
7
|
+
throw new TypeError('Artefact input is not valid JSON.');
|
|
8
|
+
}
|
|
9
|
+
export function isSafeJsonObjectKey(value) {
|
|
10
|
+
return !UNSAFE_JSON_OBJECT_KEYS.includes(value);
|
|
11
|
+
}
|
|
12
|
+
export function assertBoundedJsonStructure(value, label = 'JSON value', limits = {}) {
|
|
13
|
+
const maximumDepth = limits.maximumDepth ?? MAX_BOUNDED_JSON_DEPTH;
|
|
14
|
+
const maximumKeys = limits.maximumKeys ?? MAX_BOUNDED_JSON_KEYS;
|
|
15
|
+
const maximumValues = limits.maximumValues ?? MAX_BOUNDED_JSON_VALUES;
|
|
16
|
+
const maximumContainerItems = limits.maximumContainerItems ?? MAX_BOUNDED_JSON_CONTAINER_ITEMS;
|
|
17
|
+
let keys = 0;
|
|
18
|
+
let values = 0;
|
|
19
|
+
const ancestors = new Set();
|
|
20
|
+
const visit = (current, depth) => {
|
|
21
|
+
values += 1;
|
|
22
|
+
if (values > maximumValues) {
|
|
23
|
+
throw new TypeError(`${label} exceeds the ${maximumValues}-value limit.`);
|
|
24
|
+
}
|
|
25
|
+
if (depth > maximumDepth) {
|
|
26
|
+
throw new TypeError(`${label} exceeds the ${maximumDepth}-level nesting limit.`);
|
|
27
|
+
}
|
|
28
|
+
if (current === null || typeof current === 'string' || typeof current === 'boolean')
|
|
29
|
+
return;
|
|
30
|
+
if (typeof current === 'number') {
|
|
31
|
+
if (!Number.isFinite(current))
|
|
32
|
+
throw new TypeError(`${label} contains a non-JSON number.`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (typeof current !== 'object')
|
|
36
|
+
throw new TypeError(`${label} contains a non-JSON value.`);
|
|
37
|
+
if (ancestors.has(current))
|
|
38
|
+
throw new TypeError(`${label} contains a cyclic object reference.`);
|
|
39
|
+
let prototype;
|
|
40
|
+
let ownKeys;
|
|
41
|
+
let descriptors;
|
|
42
|
+
try {
|
|
43
|
+
prototype = Object.getPrototypeOf(current);
|
|
44
|
+
ownKeys = Reflect.ownKeys(current);
|
|
45
|
+
descriptors = Object.getOwnPropertyDescriptors(current);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new TypeError(`${label} could not be inspected safely.`);
|
|
49
|
+
}
|
|
50
|
+
if (ownKeys.some((key) => typeof key !== 'string')) {
|
|
51
|
+
throw new TypeError(`${label} contains a symbol key.`);
|
|
52
|
+
}
|
|
53
|
+
if (ownKeys.some((key) => {
|
|
54
|
+
const descriptor = descriptors[String(key)];
|
|
55
|
+
return !descriptor || !Object.hasOwn(descriptor, 'value');
|
|
56
|
+
})) {
|
|
57
|
+
throw new TypeError(`${label} contains an accessor property.`);
|
|
58
|
+
}
|
|
59
|
+
ancestors.add(current);
|
|
60
|
+
if (Array.isArray(current)) {
|
|
61
|
+
if (prototype !== Array.prototype) {
|
|
62
|
+
throw new TypeError(`${label} contains an array with a custom prototype.`);
|
|
63
|
+
}
|
|
64
|
+
const length = descriptors.length?.value;
|
|
65
|
+
if (!Number.isSafeInteger(length) || length < 0 || length > maximumContainerItems) {
|
|
66
|
+
throw new TypeError(`${label} contains a container with more than ${maximumContainerItems} items.`);
|
|
67
|
+
}
|
|
68
|
+
const indexes = ownKeys.filter((key) => key !== 'length');
|
|
69
|
+
if (indexes.length !== length
|
|
70
|
+
|| indexes.some((key, index) => key !== String(index) || descriptors[key]?.enumerable !== true)) {
|
|
71
|
+
throw new TypeError(`${label} contains a sparse, extended, or non-enumerable array.`);
|
|
72
|
+
}
|
|
73
|
+
for (const key of indexes)
|
|
74
|
+
visit(descriptors[key].value, depth + 1);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
78
|
+
throw new TypeError(`${label} contains an object with a custom prototype.`);
|
|
79
|
+
}
|
|
80
|
+
const objectKeys = ownKeys;
|
|
81
|
+
if (objectKeys.length > maximumContainerItems) {
|
|
82
|
+
throw new TypeError(`${label} contains a container with more than ${maximumContainerItems} items.`);
|
|
83
|
+
}
|
|
84
|
+
if (objectKeys.some((key) => descriptors[key]?.enumerable !== true)) {
|
|
85
|
+
throw new TypeError(`${label} contains a non-enumerable object property.`);
|
|
86
|
+
}
|
|
87
|
+
keys += objectKeys.length;
|
|
88
|
+
if (keys > maximumKeys) {
|
|
89
|
+
throw new TypeError(`${label} exceeds the ${maximumKeys}-key limit.`);
|
|
90
|
+
}
|
|
91
|
+
for (const key of objectKeys) {
|
|
92
|
+
if (!isSafeJsonObjectKey(key))
|
|
93
|
+
throw new TypeError(`${label} contains an unsafe object key.`);
|
|
94
|
+
visit(descriptors[key].value, depth + 1);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
ancestors.delete(current);
|
|
98
|
+
};
|
|
99
|
+
visit(value, 0);
|
|
100
|
+
}
|
|
101
|
+
export function scanBoundedJson(raw, limits = {}) {
|
|
102
|
+
const maximumDepth = limits.maximumDepth ?? MAX_BOUNDED_JSON_DEPTH;
|
|
103
|
+
const maximumKeys = limits.maximumKeys ?? MAX_BOUNDED_JSON_KEYS;
|
|
104
|
+
const maximumValues = limits.maximumValues ?? MAX_BOUNDED_JSON_VALUES;
|
|
105
|
+
const maximumContainerItems = limits.maximumContainerItems ?? MAX_BOUNDED_JSON_CONTAINER_ITEMS;
|
|
106
|
+
let index = 0;
|
|
107
|
+
let keys = 0;
|
|
108
|
+
let values = 0;
|
|
109
|
+
const whitespace = () => {
|
|
110
|
+
while (index < raw.length && /[\t\n\r ]/u.test(raw[index]))
|
|
111
|
+
index += 1;
|
|
112
|
+
};
|
|
113
|
+
const stringToken = () => {
|
|
114
|
+
const start = index;
|
|
115
|
+
index += 1;
|
|
116
|
+
while (index < raw.length) {
|
|
117
|
+
const character = raw[index];
|
|
118
|
+
if (character === '"') {
|
|
119
|
+
index += 1;
|
|
120
|
+
try {
|
|
121
|
+
return JSON.parse(raw.slice(start, index));
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
syntaxError();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (character === '\\') {
|
|
128
|
+
index += 1;
|
|
129
|
+
if (index >= raw.length)
|
|
130
|
+
syntaxError();
|
|
131
|
+
if (raw[index] === 'u') {
|
|
132
|
+
if (!/^[a-fA-F0-9]{4}$/u.test(raw.slice(index + 1, index + 5)))
|
|
133
|
+
syntaxError();
|
|
134
|
+
index += 5;
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
if (!/["\\/bfnrt]/u.test(raw[index]))
|
|
138
|
+
syntaxError();
|
|
139
|
+
index += 1;
|
|
140
|
+
}
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (character.charCodeAt(0) <= 0x1f)
|
|
144
|
+
syntaxError();
|
|
145
|
+
index += 1;
|
|
146
|
+
}
|
|
147
|
+
return syntaxError();
|
|
148
|
+
};
|
|
149
|
+
const literal = (token) => {
|
|
150
|
+
if (raw.slice(index, index + token.length) !== token)
|
|
151
|
+
syntaxError();
|
|
152
|
+
index += token.length;
|
|
153
|
+
};
|
|
154
|
+
const number = () => {
|
|
155
|
+
const match = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/u.exec(raw.slice(index));
|
|
156
|
+
if (!match)
|
|
157
|
+
syntaxError();
|
|
158
|
+
if (!Number.isFinite(Number(match[0]))) {
|
|
159
|
+
throw new TypeError('Artefact JSON contains a non-finite number.');
|
|
160
|
+
}
|
|
161
|
+
index += match[0].length;
|
|
162
|
+
};
|
|
163
|
+
const value = (depth) => {
|
|
164
|
+
values += 1;
|
|
165
|
+
if (values > maximumValues) {
|
|
166
|
+
throw new TypeError(`Artefact JSON exceeds the ${maximumValues}-value limit.`);
|
|
167
|
+
}
|
|
168
|
+
if (depth > maximumDepth) {
|
|
169
|
+
throw new TypeError(`Artefact JSON exceeds the ${maximumDepth}-level nesting limit.`);
|
|
170
|
+
}
|
|
171
|
+
whitespace();
|
|
172
|
+
const character = raw[index];
|
|
173
|
+
if (character === '"') {
|
|
174
|
+
stringToken();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (character === '{') {
|
|
178
|
+
index += 1;
|
|
179
|
+
whitespace();
|
|
180
|
+
const seen = new Set();
|
|
181
|
+
if (raw[index] === '}') {
|
|
182
|
+
index += 1;
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
while (index < raw.length) {
|
|
186
|
+
if (raw[index] !== '"')
|
|
187
|
+
syntaxError();
|
|
188
|
+
const key = stringToken();
|
|
189
|
+
if (seen.size >= maximumContainerItems) {
|
|
190
|
+
throw new TypeError(`Artefact JSON contains a container with more than ${maximumContainerItems} items.`);
|
|
191
|
+
}
|
|
192
|
+
keys += 1;
|
|
193
|
+
if (keys > maximumKeys) {
|
|
194
|
+
throw new TypeError(`Artefact JSON exceeds the ${maximumKeys}-key limit.`);
|
|
195
|
+
}
|
|
196
|
+
if (seen.has(key))
|
|
197
|
+
throw new TypeError('Artefact JSON contains a duplicate object key.');
|
|
198
|
+
if (!isSafeJsonObjectKey(key))
|
|
199
|
+
throw new TypeError('Artefact JSON contains an unsafe object key.');
|
|
200
|
+
seen.add(key);
|
|
201
|
+
whitespace();
|
|
202
|
+
if (raw[index] !== ':')
|
|
203
|
+
syntaxError();
|
|
204
|
+
index += 1;
|
|
205
|
+
value(depth + 1);
|
|
206
|
+
whitespace();
|
|
207
|
+
if (raw[index] === '}') {
|
|
208
|
+
index += 1;
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (raw[index] !== ',')
|
|
212
|
+
syntaxError();
|
|
213
|
+
index += 1;
|
|
214
|
+
whitespace();
|
|
215
|
+
}
|
|
216
|
+
syntaxError();
|
|
217
|
+
}
|
|
218
|
+
if (character === '[') {
|
|
219
|
+
index += 1;
|
|
220
|
+
whitespace();
|
|
221
|
+
if (raw[index] === ']') {
|
|
222
|
+
index += 1;
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
let items = 0;
|
|
226
|
+
while (index < raw.length) {
|
|
227
|
+
items += 1;
|
|
228
|
+
if (items > maximumContainerItems) {
|
|
229
|
+
throw new TypeError(`Artefact JSON contains a container with more than ${maximumContainerItems} items.`);
|
|
230
|
+
}
|
|
231
|
+
value(depth + 1);
|
|
232
|
+
whitespace();
|
|
233
|
+
if (raw[index] === ']') {
|
|
234
|
+
index += 1;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (raw[index] !== ',')
|
|
238
|
+
syntaxError();
|
|
239
|
+
index += 1;
|
|
240
|
+
whitespace();
|
|
241
|
+
}
|
|
242
|
+
syntaxError();
|
|
243
|
+
}
|
|
244
|
+
if (character === 't') {
|
|
245
|
+
literal('true');
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (character === 'f') {
|
|
249
|
+
literal('false');
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (character === 'n') {
|
|
253
|
+
literal('null');
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
number();
|
|
257
|
+
};
|
|
258
|
+
whitespace();
|
|
259
|
+
value(0);
|
|
260
|
+
whitespace();
|
|
261
|
+
if (index !== raw.length)
|
|
262
|
+
syntaxError();
|
|
263
|
+
}
|
|
264
|
+
export function parseBoundedJson(raw, options) {
|
|
265
|
+
const label = options.label ?? 'Artefact input';
|
|
266
|
+
if (typeof raw !== 'string')
|
|
267
|
+
throw new TypeError(`${label} must be UTF-8 JSON text.`);
|
|
268
|
+
const bytes = new TextEncoder().encode(raw).byteLength;
|
|
269
|
+
if (bytes < 1 || bytes > options.maximumBytes) {
|
|
270
|
+
throw new TypeError(`${label} must be between 1 byte and ${options.maximumBytes} bytes.`);
|
|
271
|
+
}
|
|
272
|
+
scanBoundedJson(raw, options.limits);
|
|
273
|
+
try {
|
|
274
|
+
return JSON.parse(raw);
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return syntaxError();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
export function parseBoundedJsonObject(raw, options) {
|
|
281
|
+
const label = options.label ?? 'Artefact input';
|
|
282
|
+
const parsed = parseBoundedJson(raw, options);
|
|
283
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
284
|
+
throw new TypeError(`${label} must contain one JSON object.`);
|
|
285
|
+
}
|
|
286
|
+
return parsed;
|
|
287
|
+
}
|
|
@@ -7,7 +7,7 @@ import { createHash } from 'node:crypto';
|
|
|
7
7
|
import { Worker } from 'node:worker_threads';
|
|
8
8
|
import { RETIRE_BROWSER_CATALOG } from "./generated/retire-browser-catalog.mjs";
|
|
9
9
|
import { CISA_KEV_CATALOG } from "./generated/cisa-kev-catalog.mjs";
|
|
10
|
-
import { createObservation } from "
|
|
10
|
+
import { createObservation } from "../packages/evidence/observation.mjs";
|
|
11
11
|
import { MAX_INLINE_SCRIPT_CHARS, MAX_INLINE_SCRIPT_TOTAL_CHARS, MAX_SCRIPT_ELEMENTS, MAX_STATIC_HTML_CHARS, analyzeStaticHtml, } from "./static-html-analysis.mjs";
|
|
12
12
|
// Catalogue expressions are reviewed third-party inputs. Keep every individual
|
|
13
13
|
// regular-expression evaluation and the aggregate inline-signature work small
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// retained; malformed extensions fail soft and leave an explicit partial flag.
|
|
4
4
|
import 'reflect-metadata';
|
|
5
5
|
import { CertificatePolicyExtension, CRLDistributionPointsExtension, X509Certificate, } from '@peculiar/x509';
|
|
6
|
-
|
|
6
|
+
import { MAX_LOOKUP_TLS_CERTIFICATE_POLICIES } from "./lookup-network-evidence-bounds.mjs";
|
|
7
|
+
export const MAX_CERTIFICATE_POLICIES = MAX_LOOKUP_TLS_CERTIFICATE_POLICIES;
|
|
7
8
|
export const MAX_CRL_DISTRIBUTION_POINTS = 32;
|
|
8
9
|
function emptyProfile(partial = false) {
|
|
9
10
|
return Object.freeze({
|
package/lib/classify.mjs
CHANGED
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
// original input hostname and the resolved registrable domain are returned so
|
|
15
15
|
// callers can look up the latter while still showing the user what they typed.
|
|
16
16
|
import { isIP } from 'node:net';
|
|
17
|
+
import { domainToASCII } from 'node:url';
|
|
17
18
|
import { parse } from 'tldts';
|
|
19
|
+
import { canonicalRegistrableDomain } from "./registrable-domain.mjs";
|
|
18
20
|
// A CR/LF here would let one query become multiple lines once it reaches
|
|
19
21
|
// lib/whois.mts's raw `socket.write(query + '\r\n')` - a normal URL-encoded
|
|
20
22
|
// query string (?q=example.com%0D%0AHELP) decodes back into literal control
|
|
@@ -24,6 +26,9 @@ const CONTROL_CHAR_RE = /[\x00-\x1f\x7f]/;
|
|
|
24
26
|
// 32-bit ASN ceiling (RFC 6793 four-octet ASNs). AS0 and AS4294967295 are
|
|
25
27
|
// reserved but syntactically valid; anything above is not an ASN at all.
|
|
26
28
|
const MAX_ASN = 4294967295;
|
|
29
|
+
const MAX_DIRECT_LOOKUP_TARGET_LENGTH = 1024;
|
|
30
|
+
const DIRECT_LOOKUP_RESERVED_SUFFIXES = new Set(['example', 'test']);
|
|
31
|
+
const DIRECT_LOOKUP_EXCLUDED_PUBLIC_SUFFIXES = new Set(['onion']);
|
|
27
32
|
// A registrable-domain label: LDH (letter/digit/hyphen), 1-63 characters, no
|
|
28
33
|
// leading or trailing hyphen. tldts already rejects empty labels, over-long
|
|
29
34
|
// labels, leading/trailing hyphens, and public-suffix-only inputs (all yield
|
|
@@ -33,6 +38,52 @@ const MAX_ASN = 4294967295;
|
|
|
33
38
|
function isValidRegistrableLabel(label) {
|
|
34
39
|
return /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i.test(label);
|
|
35
40
|
}
|
|
41
|
+
// Direct CLI shorthand is deliberately narrower than normal query
|
|
42
|
+
// classification. classifyQuery() accepts pasted URLs and other convenient
|
|
43
|
+
// input forms after normalisation, while a value occupying command position
|
|
44
|
+
// must be unambiguously a target before it may select the networked Lookup
|
|
45
|
+
// command. This predicate therefore accepts only a complete hostname, literal
|
|
46
|
+
// IP address, or bounded ASN and never attempts to repair URL-like input.
|
|
47
|
+
function isDirectLookupTarget(raw) {
|
|
48
|
+
if (typeof raw !== 'string'
|
|
49
|
+
|| !raw
|
|
50
|
+
|| raw.length > MAX_DIRECT_LOOKUP_TARGET_LENGTH
|
|
51
|
+
|| raw.startsWith('-')
|
|
52
|
+
|| raw.trim() !== raw
|
|
53
|
+
|| /\s/u.test(raw)
|
|
54
|
+
|| CONTROL_CHAR_RE.test(raw)) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
if (isIP(raw) !== 0)
|
|
58
|
+
return !raw.includes('%') && !raw.startsWith('[');
|
|
59
|
+
const asnMatch = raw.match(/^(?:AS)?(\d+)$/i);
|
|
60
|
+
if (asnMatch) {
|
|
61
|
+
const number = Number(asnMatch[1]);
|
|
62
|
+
return Number.isInteger(number) && number >= 0 && number <= MAX_ASN;
|
|
63
|
+
}
|
|
64
|
+
if (/[\\/@:?#%\[\]]/u.test(raw) || raw.endsWith('..'))
|
|
65
|
+
return false;
|
|
66
|
+
const hostname = raw.endsWith('.') ? raw.slice(0, -1) : raw;
|
|
67
|
+
if (!hostname.includes('.'))
|
|
68
|
+
return false;
|
|
69
|
+
const ascii = domainToASCII(hostname).toLowerCase();
|
|
70
|
+
if (!ascii || ascii.length > 253 || !ascii.split('.').every(isValidRegistrableLabel))
|
|
71
|
+
return false;
|
|
72
|
+
const parsed = parse(ascii);
|
|
73
|
+
const publicSuffix = parsed.publicSuffix || '';
|
|
74
|
+
if (DIRECT_LOOKUP_EXCLUDED_PUBLIC_SUFFIXES.has(publicSuffix)
|
|
75
|
+
|| publicSuffix === 'arpa'
|
|
76
|
+
|| publicSuffix.endsWith('.arpa'))
|
|
77
|
+
return false;
|
|
78
|
+
if (parsed.isIcann !== true && !DIRECT_LOOKUP_RESERVED_SUFFIXES.has(publicSuffix))
|
|
79
|
+
return false;
|
|
80
|
+
try {
|
|
81
|
+
return classifyQuery(raw).type === 'domain';
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
36
87
|
function classifyQuery(raw) {
|
|
37
88
|
const trimmed = raw.trim();
|
|
38
89
|
if (CONTROL_CHAR_RE.test(trimmed)) {
|
|
@@ -75,13 +126,13 @@ function classifyQuery(raw) {
|
|
|
75
126
|
if (!inputHostname || !inputHostname.includes('.') || inputHostname.length > 253) {
|
|
76
127
|
throw new Error(`"${trimmed}" is not a valid domain, IP, or ASN.`);
|
|
77
128
|
}
|
|
78
|
-
|
|
129
|
+
if (!inputHostname.split('.').every(isValidRegistrableLabel)) {
|
|
130
|
+
throw new Error(`"${trimmed}" contains an invalid domain label (underscores, empty, or malformed labels are not registrable).`);
|
|
131
|
+
}
|
|
132
|
+
const registrableDomain = canonicalRegistrableDomain(inputHostname);
|
|
79
133
|
if (!registrableDomain) {
|
|
80
134
|
throw new Error(`"${trimmed}" is not a registrable domain (no public-suffix match).`);
|
|
81
135
|
}
|
|
82
|
-
if (!registrableDomain.split('.').every(isValidRegistrableLabel)) {
|
|
83
|
-
throw new Error(`"${trimmed}" contains an invalid domain label (underscores, empty, or malformed labels are not registrable).`);
|
|
84
|
-
}
|
|
85
136
|
return {
|
|
86
137
|
type: 'domain',
|
|
87
138
|
value: registrableDomain,
|
|
@@ -90,4 +141,4 @@ function classifyQuery(raw) {
|
|
|
90
141
|
isSubdomain: inputHostname !== registrableDomain,
|
|
91
142
|
};
|
|
92
143
|
}
|
|
93
|
-
export { classifyQuery, MAX_ASN };
|
|
144
|
+
export { classifyQuery, isDirectLookupTarget, MAX_ASN };
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// HTML tokenizer pass. Scripts are never fetched or executed. Only fixed
|
|
3
3
|
// indicator identifiers, descriptions, evidence classes, and bounded counts
|
|
4
4
|
// leave this module; script references and contents are discarded.
|
|
5
|
-
import { createObservation } from "
|
|
5
|
+
import { createObservation } from "../packages/evidence/observation.mjs";
|
|
6
6
|
import { analyzeStaticHtml, } from "./static-html-analysis.mjs";
|
|
7
7
|
const CLIENT_BEHAVIOR_PROFILE_VERSION = 1;
|
|
8
8
|
const MAX_CLIENT_BEHAVIOR_INDICATORS = 12;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// shared tokenizer projects only fixed categories and bounded counts; field
|
|
4
4
|
// names, values, labels, placeholders, arbitrary attributes, and action URLs
|
|
5
5
|
// never enter this profile.
|
|
6
|
-
import { createObservation } from "
|
|
6
|
+
import { createObservation } from "../packages/evidence/observation.mjs";
|
|
7
7
|
import { MAX_STATIC_FORMS, MAX_STATIC_INPUTS, analyzeStaticHtml, } from "./static-html-analysis.mjs";
|
|
8
8
|
const CREDENTIAL_SURFACE_PROFILE_VERSION = 1;
|
|
9
9
|
function analyzeCredentialSurfaceProfile(input = {}) {
|