@slicedearth/whoisleuth-cli 2.5.1 → 2.7.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 +9 -6
- package/NOTICE +3 -2
- package/README.md +60 -67
- package/cli/artifact-validation/case-response.mjs +6 -814
- package/cli/artifact-validation/lookup-evidence.mjs +30 -3
- package/cli/artifact-validation/signed-review.mjs +2 -51
- package/cli/artifact-verify.mjs +41 -3
- package/cli/bulk-command-runner.mjs +4 -6
- package/cli/case-command.mjs +92 -16
- package/cli/case-pack.mjs +14 -3
- package/cli/ci-report.mjs +17 -5
- package/cli/command-definition.mjs +7 -6
- package/cli/discovery-scan-command-runner.mjs +4 -6
- package/cli/doctor.mjs +37 -3
- package/cli/domain-feed.mjs +61 -0
- package/cli/formatters/html.mjs +3 -0
- package/cli/formatters/json.mjs +9 -14
- package/cli/formatters/terminal-lookup.mjs +24 -1
- package/cli/history-command-definitions.mjs +1 -1
- package/cli/indicator-set-command.mjs +1 -2
- package/cli/intake-command.mjs +43 -3
- package/cli/investigation-artifacts.mjs +2 -3
- package/cli/investigation-package-review.mjs +2 -0
- package/cli/investigation-plan.mjs +15 -0
- package/cli/local-mmdb-review.mjs +27 -4
- package/cli/lookup-brief.mjs +11 -5
- package/cli/lookup-command-runner.mjs +21 -13
- package/cli/offline-evidence-review.mjs +51 -5
- package/cli/review-arguments.mjs +3 -2
- package/cli/review-command-definitions.mjs +23 -17
- package/cli/review-command-runner.mjs +6 -3
- package/cli/risk-calibration.mjs +6 -4
- package/cli/saved-lookup.mjs +22 -7
- package/cli/selected-input-worker.mjs +12 -2
- package/cli/source-reliability.mjs +12 -10
- package/cli/support-arguments.mjs +1 -1
- package/cli/support-command-definitions.mjs +5 -4
- package/cli/support-command-runner.mjs +9 -7
- package/cli/watchlist-review.mjs +43 -0
- package/cli/workflow-arguments.mjs +14 -0
- package/cli/workflow-command-definitions.mjs +27 -1
- package/cli/workflow-command-runner.mjs +32 -0
- package/docs/cli-reference.md +49 -11
- package/docs/cli.md +200 -26
- package/docs/contextual-reviews.md +38 -5
- package/docs/infrastructure-observations.md +135 -0
- package/docs/offline-intake-context.md +213 -0
- package/lib/credential-form-attribution.mjs +136 -0
- package/lib/credential-surface-profile.mjs +6 -2
- package/lib/dmarc-discovery.mjs +4 -2
- package/lib/dns-delegation-health.mjs +23 -7
- package/lib/dns-inheritance-review.mjs +14 -1
- package/lib/dns-intelligence.mjs +18 -4
- package/lib/domain-posture-analysis.mjs +53 -2
- package/lib/domain-posture-parsers.mjs +9 -0
- package/lib/domain-posture.mjs +29 -10
- package/lib/evidence-export-privacy.mjs +5 -2
- package/lib/evidence-export.mjs +10 -4
- package/lib/evidence-report.mjs +3 -1
- package/lib/favicon.mjs +13 -4
- package/lib/generated/cisa-kev-catalog.mjs +17 -4
- package/lib/generated/registrar-standing-catalogue.mjs +7 -7
- package/lib/generated/retire-browser-catalog.mjs +178 -5
- package/lib/html-signals.mjs +6 -0
- package/lib/idn-confusables.mjs +5 -1
- package/lib/lookup-child-profile-contract.mjs +28 -11
- package/lib/lookup-collection-receipt.mjs +52 -0
- package/lib/lookup-diagnostics.mjs +2 -12
- package/lib/lookup-progress.mjs +29 -31
- package/lib/lookup-response-contract.mjs +10 -0
- package/lib/lookup-response.mjs +15 -14
- package/lib/lookup-source-progress.mjs +4 -2
- package/lib/lookup.mjs +33 -2
- package/lib/operation-budget.mjs +4 -0
- package/lib/rdap-capabilities.mjs +43 -9
- package/lib/registry-insights.mjs +17 -6
- package/lib/service-binding-dns.mjs +19 -2
- package/lib/sslbl-certificates.generated.mjs +142 -140
- package/lib/static-html-analysis.mjs +32 -4
- package/lib/technology-evidence-role.mjs +1 -51
- package/lib/tls-intelligence.mjs +30 -5
- package/lib/website-enrichment.mjs +39 -23
- package/lib/whois-authority.mjs +56 -1
- package/lib/whois-chain.mjs +14 -4
- package/lib/whois-common-formats.mjs +3 -1
- package/lib/whois-contacts.mjs +13 -2
- package/lib/whois-contracts.mjs +2 -0
- package/lib/whois-normalization.mjs +4 -4
- package/package.json +9 -7
- package/packages/analysis/bounded-json.mjs +12 -2
- package/packages/analysis/risk-scoring.mjs +12 -9
- package/packages/cases/case-attachment-model.mjs +36 -1
- package/packages/cases/case-checkpoint-receipts.mjs +68 -0
- package/packages/cases/case-containment-handoff.mjs +195 -0
- package/packages/cases/case-evidence-links.mjs +44 -0
- package/packages/cases/case-evidence-model.mjs +51 -7
- package/packages/cases/case-migration-model.mjs +9 -1
- package/packages/cases/case-object-coverage.mjs +119 -0
- package/packages/cases/case-packet-correction.mjs +106 -0
- package/packages/cases/case-recheck-model.mjs +35 -5
- package/packages/cases/case-record-operations.mjs +87 -9
- package/packages/cases/case-record-projection.mjs +43 -5
- package/packages/cases/case-record-version-input.mjs +23 -1
- package/packages/cases/case-report.mjs +35 -4
- package/packages/cases/case-requested-evidence.mjs +1 -1
- package/packages/cases/case-response-actions.mjs +57 -1
- package/packages/cases/case-response-model.mjs +7 -0
- package/packages/cases/case-response-object.mjs +119 -0
- package/packages/cases/case-response-outcomes.mjs +81 -14
- package/packages/cases/case-response-packet-render.mjs +22 -3
- package/packages/cases/case-response-packet-validation.mjs +880 -0
- package/packages/cases/case-response-packet-vocabulary.mjs +12 -1
- package/packages/cases/case-response-packet.mjs +32 -17
- package/packages/cases/case-response-review-inputs.mjs +66 -8
- package/packages/cases/case-type-presentation.mjs +10 -0
- package/packages/cases/case-workflow-metadata.mjs +1 -17
- package/packages/comparison/capture-context.mjs +3 -2
- package/packages/contracts/analyst-interchange.mjs +7 -5
- package/packages/contracts/analyst-review-state-contract.mjs +1 -1
- package/packages/contracts/candidate-watch-review.mjs +54 -0
- package/packages/contracts/case-portability.mjs +292 -43
- package/packages/contracts/cli-command-semantics.mjs +3 -0
- package/packages/contracts/cli-doctor.mjs +4 -0
- package/packages/contracts/cli-lookup.mjs +57 -46
- package/packages/contracts/context-review.mjs +2 -1
- package/packages/contracts/external-observation-interchange.mjs +96 -28
- package/packages/contracts/lookup-evidence.mjs +13 -2
- package/packages/contracts/message-intake.mjs +7 -1
- package/packages/contracts/registry-insights.mjs +6 -0
- package/packages/contracts/risk-calibration.mjs +44 -29
- package/packages/contracts/schema-lifecycle.mjs +3 -1
- package/packages/contracts/web-capture.mjs +1 -1
- package/packages/contracts/workspace-portability.mjs +86 -16
- package/packages/evidence/collection-quality.mjs +1 -1
- package/packages/evidence/credential-form-attribution.mjs +60 -0
- package/packages/evidence/dmarc-policy-context.mjs +80 -0
- package/packages/evidence/image-regions.mjs +15 -0
- package/packages/evidence/lookup-collection-receipt.mjs +140 -0
- package/packages/evidence/lookup-collection-review.mjs +74 -0
- package/packages/evidence/record-identity.mjs +60 -16
- package/packages/evidence/technology-evidence-role.mjs +51 -0
- package/packages/interchange/external-findings-import.mjs +40 -21
- package/packages/interchange/json-output.mjs +14 -0
- package/packages/interchange/managed-indicator-export.mjs +3 -2
- package/packages/interchange/managed-indicator-set.mjs +13 -0
- package/packages/investigation/acquisition-decision-packet.mjs +78 -0
- package/packages/investigation/bulk-mail-exposure.mjs +2 -2
- package/packages/investigation/context-review.mjs +3 -3
- package/packages/investigation/document-intake.mjs +9 -3
- package/packages/investigation/domain-history-review.mjs +50 -12
- package/packages/investigation/domain-transition-review.mjs +2 -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 +2 -1
- package/packages/investigation/investigation-manifest.mjs +33 -7
- package/packages/investigation/link-intake.mjs +38 -8
- package/packages/investigation/message-intake.mjs +55 -15
- package/packages/monitoring/domain-feed.mjs +321 -0
- package/packages/workspace/brand-candidate-workflow.mjs +351 -0
- package/packages/workspace/brand-keyword-campaign.mjs +241 -0
- package/packages/workspace/brand-profile-model.mjs +46 -2
- package/packages/workspace/bulk-session-model.mjs +12 -0
- package/packages/workspace/candidate-watch-handoff.mjs +195 -0
- package/packages/workspace/detection-rule-model.mjs +10 -2
- package/packages/workspace/shortlist-model.mjs +10 -2
- package/packages/workspace/watchlist-history.mjs +31 -4
- package/packages/workspace/watchlist-store.mjs +136 -7
- package/third-party-notices.txt +30 -30
|
@@ -17,8 +17,9 @@ The CLI uses the same models:
|
|
|
17
17
|
whoisleuth review-evidence selected-input.json --json --strict-exit
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
Each input has `schema`, `version
|
|
21
|
-
|
|
20
|
+
Each input has `schema`, a supported `version` and `evidence`. Most inputs use
|
|
21
|
+
version 1; domain history also supports version 2 as described below. Unknown
|
|
22
|
+
versions and fields are rejected. Inputs are bounded to 16 MiB; each supplied context list permits
|
|
22
23
|
200 records. `--strict-exit` returns 4 for a partial contextual review. Neither
|
|
23
24
|
adapter contacts a destination or executes selected software.
|
|
24
25
|
|
|
@@ -57,8 +58,12 @@ later. These stages never execute a command, submit a form or open a destination
|
|
|
57
58
|
|
|
58
59
|
The Case view compares retained snapshots using the existing source, hostname,
|
|
59
60
|
scan-depth and model-version rules. Registration, DNS, mail and web changes are
|
|
60
|
-
shown alongside source-qualified certificate pins.
|
|
61
|
-
|
|
61
|
+
shown alongside source-qualified certificate pins.
|
|
62
|
+
Registration and nameserver changes target the Case registration domain; mail
|
|
63
|
+
and web changes target the explicit observation hostname. Certificate pins keep
|
|
64
|
+
their own hostname, or an unknown target when none was retained. These targets
|
|
65
|
+
are preserved in review downloads, retained reports and print previews.
|
|
66
|
+
Equal-time observations are not ordered into a change sequence. Retired dependencies must involve the Case
|
|
62
67
|
domain or one of its hostnames. An expected window qualifies the review; it does
|
|
63
68
|
not approve a change automatically or establish transfer or takeover.
|
|
64
69
|
Review prompts connect comparable page, mail, delegation and registration changes
|
|
@@ -66,12 +71,23 @@ to the retained concern. Incomplete comparisons remain visible. A changed creati
|
|
|
66
71
|
date asks for reassessment of earlier decisions without asserting a new owner,
|
|
67
72
|
resetting a baseline or discarding history; expiry alone never establishes deletion.
|
|
68
73
|
|
|
74
|
+
Open **Retirement review checklist** for DNS-alias, recovery-email and retained
|
|
75
|
+
integration-reference prompts. Review the configuration you control, then use
|
|
76
|
+
the existing declaration form to record the affected hostnames, evidence family,
|
|
77
|
+
retirement time and source. The draft shows these details before you prepare a
|
|
78
|
+
review. Prefer a configuration or change-reference label; do not enter recovery
|
|
79
|
+
addresses, credentials or complete URLs. Reports with declared retired dependencies
|
|
80
|
+
include the same suggested next reviews in the browser and CLI. Suggestions do not
|
|
81
|
+
record checks as completed or establish takeover availability. Saving preserves
|
|
82
|
+
the source-qualified declaration, not confirmation that configuration was changed.
|
|
83
|
+
|
|
69
84
|
Schema: `whoisleuth.domain-history.input`. Its `evidence` contains:
|
|
70
85
|
|
|
71
86
|
- `caseExport`: an ordinary supported editable Case export, with `version` and
|
|
72
87
|
`cases`; the exact existing Case reader rejects data loss or silent repair.
|
|
73
88
|
- `caseId`: the Case selected from that export.
|
|
74
|
-
- `declarations`: `expectedChanges` and `retiredDependencies` arrays
|
|
89
|
+
- `declarations`: `expectedChanges` and `retiredDependencies` arrays; version 2
|
|
90
|
+
also requires `registrationBoundaries`. Version 1 remains readable unchanged.
|
|
75
91
|
|
|
76
92
|
An expected change is `{family, start, end, reason}`. A retired dependency is
|
|
77
93
|
`{asset, dependency, family, retiredAt, source}`. Families are `registration`,
|
|
@@ -79,6 +95,23 @@ An expected change is `{family, start, end, reason}`. A retired dependency is
|
|
|
79
95
|
reusable input download includes the selected Case and is not duplicated in
|
|
80
96
|
workspace storage when saving the report.
|
|
81
97
|
|
|
98
|
+
Version-2 registration boundaries are analyst declarations, not confirmed
|
|
99
|
+
registration epochs. Each has `{kind, occurredAt, source, rationale,
|
|
100
|
+
snapshotIds, evidencePinIds}`. Kinds are `registration_reported`,
|
|
101
|
+
`deletion_reported`, `reregistration_reported`, `transfer_reported` and
|
|
102
|
+
`review_boundary`. Supply an explicit ISO time and at least one exact reference
|
|
103
|
+
to a snapshot or evidence pin retained in the selected Case. References must be
|
|
104
|
+
unique; unavailable references fail rather than selecting a replacement.
|
|
105
|
+
Up to 40 boundaries and 20 references of each type per boundary are accepted.
|
|
106
|
+
|
|
107
|
+
The Case history form exposes the same declarations. Reports count retained
|
|
108
|
+
observations before and at/after a declared time without assigning ownership or
|
|
109
|
+
asserting that deletion, re-registration or transfer occurred. Earlier evidence,
|
|
110
|
+
decisions, exceptions and open follow-ups are not rewritten. Reports retain the
|
|
111
|
+
declared source and exact evidence references, and can be deliberately saved as
|
|
112
|
+
ordinary Case files. The reusable input remains a separate download containing
|
|
113
|
+
the selected Case, not an additional workspace copy.
|
|
114
|
+
|
|
82
115
|
## Platform objects
|
|
83
116
|
|
|
84
117
|
Schema: `whoisleuth.platform-continuity.input`. `evidence` is an array:
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Source-qualified infrastructure snapshots
|
|
2
|
+
|
|
3
|
+
The retained infrastructure inventory combines admitted browser-local evidence,
|
|
4
|
+
not all infrastructure belonging to an organisation. It makes no requests on
|
|
5
|
+
import, opening a view, filtering, selecting a host or comparing history.
|
|
6
|
+
|
|
7
|
+
Import a `whoisleuth.infrastructure-observation` version 1 document through the
|
|
8
|
+
existing external-findings import. The exact bounded snapshot is retained as one
|
|
9
|
+
Case evidence pin. Review the preview before confirming the mutation. The source
|
|
10
|
+
descriptions are supplied provenance, not authenticated source identities.
|
|
11
|
+
Legacy DNS and certificate imports remain readable: an old Case-domain DNS pin
|
|
12
|
+
does not acquire a precise queried hostname or complete collection scope.
|
|
13
|
+
|
|
14
|
+
The document records an explicit target, selected full hostnames and DNS types,
|
|
15
|
+
collection mode, observation time and coverage. Each DNS response retains its
|
|
16
|
+
queried name, response owner, type, outcome and source. An alias link uses its
|
|
17
|
+
record owner; an address returned after an alias belongs to the returned address
|
|
18
|
+
owner, not automatically to the initial query. Only retained alias links are
|
|
19
|
+
drawn. Links from different dates do not prove one contemporaneous alias chain.
|
|
20
|
+
No-data, NXDOMAIN, failed and not-checked outcomes remain different observations
|
|
21
|
+
and do not decide registration availability.
|
|
22
|
+
|
|
23
|
+
Certificate-log and TLS observations keep distinct sources and exact SHA-256
|
|
24
|
+
identities. Full names remain visible; `*.example.test` is a wildcard pattern,
|
|
25
|
+
not an enumerated hostname, and cannot be selected for Lookup or Bulk.
|
|
26
|
+
Certificate names alone do not establish current resolution or activity.
|
|
27
|
+
|
|
28
|
+
DNS operator, observed edge/CDN/proxy, application platform, runtime, embedded
|
|
29
|
+
dependency, address registration, routing origin and independently observed
|
|
30
|
+
origin are distinct source-qualified roles. Routing requires a routing source;
|
|
31
|
+
IP-registration data cannot substitute for it. Edge indicators cannot identify
|
|
32
|
+
a hidden origin. Multiple providers and conflicting observations remain separate.
|
|
33
|
+
The snapshot reader does not derive physical location, responsibility, ownership,
|
|
34
|
+
safety or maliciousness from these roles.
|
|
35
|
+
|
|
36
|
+
## Review and deliberately act
|
|
37
|
+
|
|
38
|
+
Open the inventory from saved-work search. Type, source and date filters apply to
|
|
39
|
+
the admitted set before pagination. Inspect one snapshot to review every retained
|
|
40
|
+
DNS outcome, certificate name and provider role, or select two snapshots in
|
|
41
|
+
chronological order to compare. Export preserves the exact snapshot format.
|
|
42
|
+
Topology search applies to the complete admitted one-hop relationship set, not
|
|
43
|
+
the current source page. Its diagram has a 50-relationship budget; the paginated
|
|
44
|
+
source list and exact snapshot review retain access to evidence beyond the diagram.
|
|
45
|
+
|
|
46
|
+
Follow a retained domain to an address or certificate, then open its supporting
|
|
47
|
+
Case. **Return to retained infrastructure** restores the exact query, filters,
|
|
48
|
+
page positions and selected identity. **Tab references → Navigation trail** also
|
|
49
|
+
lets you return to earlier successful pivots. This trail lives only in the
|
|
50
|
+
current tab and workspace; it creates no observed relationships. Sources are
|
|
51
|
+
read again rather than copied into navigation state. A removed identity stays
|
|
52
|
+
unavailable, and clearing references or locking the workspace expires the links.
|
|
53
|
+
Certificate identities here are leaf-certificate SHA-256, not SPKI digests.
|
|
54
|
+
|
|
55
|
+
Lookup links fill the selected target for review without submitting it. Explicitly
|
|
56
|
+
selected hostnames can be prepared in existing Bulk. That handoff does not scan,
|
|
57
|
+
recursively expand targets, change a watchlist or enrol a host in monitoring.
|
|
58
|
+
Use the existing reviewed Lookup/Bulk/Monitor collection plans and explicit
|
|
59
|
+
save actions to collect or retain another observation.
|
|
60
|
+
|
|
61
|
+
## Historical comparison and CLI
|
|
62
|
+
|
|
63
|
+
Source, mode and exact selection scope must match, and the later snapshot must
|
|
64
|
+
have a strictly later observation time. Newly observed means newly present in
|
|
65
|
+
retained evidence, not newly created. A value not returned by a complete,
|
|
66
|
+
comparable selected collection is labelled **not returned**, never confirmed
|
|
67
|
+
absence or disappearance. Failed, partial, limited, changed-source and
|
|
68
|
+
incomparable snapshots cannot generate removal findings. Equal-time observations
|
|
69
|
+
have no temporal order. Provider-reported history stays labelled separately from
|
|
70
|
+
supplied local observations.
|
|
71
|
+
|
|
72
|
+
Comparison rows retain the exact DNS query, response owner and record type, or
|
|
73
|
+
the certificate SHA-256, alongside their source and observation clocks. Labels
|
|
74
|
+
are presentation only: relationship groups and graph exports use canonical
|
|
75
|
+
entity identities, including when long nameserver-set labels look identical.
|
|
76
|
+
|
|
77
|
+
Derived relationship-group, graph-node and cluster IDs can change when their
|
|
78
|
+
identity calculation changes, including for short labels. They are not durable
|
|
79
|
+
cross-export keys; use the canonical `entityId` where provided. Historical groups
|
|
80
|
+
without it use their complete retained value. Graph fields named `canonical`
|
|
81
|
+
and `value` are bounded display text, not a replacement for that identity. Inspect
|
|
82
|
+
the contributing Case evidence for full nameserver members. Component selections
|
|
83
|
+
and cluster adjustments are transient and are not persisted review history.
|
|
84
|
+
|
|
85
|
+
In GraphML and GEXF, the display label for `node_canonical` is now
|
|
86
|
+
`Bounded display value` (previously `Canonical value`). The field key, JSON
|
|
87
|
+
property, entity identities and values are unchanged; XML consumers should use
|
|
88
|
+
the field key rather than its display label.
|
|
89
|
+
|
|
90
|
+
The existing offline command shares the website reader and comparison owner:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
whoisleuth review-evidence infrastructure-observation.json --json
|
|
94
|
+
whoisleuth review-evidence infrastructure-comparison.json --json
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The comparison input is exactly:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"schema": "whoisleuth.infrastructure-comparison.input",
|
|
102
|
+
"version": 1,
|
|
103
|
+
"earlier": { "...": "complete version-1 snapshot" },
|
|
104
|
+
"later": { "...": "complete version-1 snapshot" }
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The placeholders above are illustrative, not valid evidence. See the reserved
|
|
109
|
+
fixture in `test/fixtures/infrastructure-observations/` for an exact snapshot.
|
|
110
|
+
The CLI performs no DNS, registry, BGP, TLS, HTTP or provider request.
|
|
111
|
+
|
|
112
|
+
Changed or incomplete latest retained pairs surface as existing local Review
|
|
113
|
+
Items with supporting Case navigation. Complete earlier snapshots remain
|
|
114
|
+
available as stronger baselines; later failures do not rewrite them. There is no
|
|
115
|
+
new history store or scheduling engine.
|
|
116
|
+
|
|
117
|
+
## Resource and compatibility boundary
|
|
118
|
+
|
|
119
|
+
One snapshot is limited to 128 KiB, 128 selected hostnames, six DNS types, 16
|
|
120
|
+
source identities, 512 DNS response rows with at most 32 values each, 32
|
|
121
|
+
certificate observations with at most 128 exact names each, 128 role observations
|
|
122
|
+
and 12 bounded limitations. Bounds reject oversized documents rather than
|
|
123
|
+
silently prune accepted snapshot evidence. Existing Case/workspace quotas still
|
|
124
|
+
apply. Projection/diagram budgets may admit a smaller disposable view, with
|
|
125
|
+
incomplete coverage disclosed; exact snapshot review/export remains available.
|
|
126
|
+
|
|
127
|
+
Version 1 has exact keys and refuses future versions. Existing external-findings
|
|
128
|
+
version 4 and typed version-1 row imports preserve their historical meaning;
|
|
129
|
+
version 5 adds the optional exact infrastructure snapshot. No external discovery
|
|
130
|
+
API, new provider, dependency, collector or automatic request is required.
|
|
131
|
+
Current snapshot and version-5 findings exports use compact UTF-8 JSON with a
|
|
132
|
+
terminal line feed so the emitted representation stays within its parser ceiling.
|
|
133
|
+
An offline comparison input is bounded to two snapshot budgets plus 1 KiB of
|
|
134
|
+
framing. Historical version-4 fixture bytes and reader meaning remain unchanged;
|
|
135
|
+
new typed row conversions emit version 5.
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# Selected-input indicators and internal containment
|
|
2
|
+
|
|
3
|
+
Message intake remains local and inert. It does not resolve indicators, open
|
|
4
|
+
links, consult reputation services, execute attachments or apply controls.
|
|
5
|
+
|
|
6
|
+
## Source-linked indicator review
|
|
7
|
+
|
|
8
|
+
Version 2 of `whoisleuth.message-intake` adds literal IPv4/IPv6 observations and
|
|
9
|
+
explicitly labelled MD5, SHA-1 and SHA-256 strings from selected plain text,
|
|
10
|
+
message body text, supported calendar fields and extracted document text.
|
|
11
|
+
Private and reserved addresses are valid textual observations, not permission
|
|
12
|
+
to contact them. A labelled hash string is a supplied claim, not proof that a
|
|
13
|
+
file has that digest. Bare opaque hexadecimal values are not interpreted.
|
|
14
|
+
|
|
15
|
+
Each indicator identifies its input/message/document part and page when known.
|
|
16
|
+
The review retains the source-byte and part digests separately. Identical values
|
|
17
|
+
from different parts remain separately attributed; repeated identical values
|
|
18
|
+
within one part are deduplicated. The UI can copy a source citation. Indicators
|
|
19
|
+
are not added to Lookup targets and do not become a risk score or finding.
|
|
20
|
+
|
|
21
|
+
Complete URL spans, address local parts, recognised credential-bearing lines,
|
|
22
|
+
message headers and QR payloads are not scanned for indicators. MIME HTML
|
|
23
|
+
extraction also excludes script and style nodes. Explicit plaintext is literal
|
|
24
|
+
supplied text: an IP inside a pasted code snippet can be an inert observation;
|
|
25
|
+
the code is never executed. Phone candidates have their own stricter markup
|
|
26
|
+
exclusions. HAR and identity-event imports keep their existing restricted
|
|
27
|
+
projections: no broad scan of their original fields occurs. Extraction is
|
|
28
|
+
limited to 512 observations, 4,096 candidate checks and 2,097,152 text code
|
|
29
|
+
units across an input. Reached bounds are partial coverage, never proof that
|
|
30
|
+
no further indicators exist.
|
|
31
|
+
|
|
32
|
+
Version-1 reports remain readable without invented indicator coverage. Their
|
|
33
|
+
original source digests do not acquire new observations retrospectively.
|
|
34
|
+
|
|
35
|
+
### Supplied phone candidates
|
|
36
|
+
|
|
37
|
+
Plain-text intake can also identify possible telephone numbers, including a
|
|
38
|
+
support number in a supplied snippet with no link. Candidates are review leads:
|
|
39
|
+
they do not authenticate a caller, identify an operator or establish wrongdoing.
|
|
40
|
+
No number is dialled, resolved or checked against a reputation service.
|
|
41
|
+
|
|
42
|
+
Review and select each observation deliberately before including it in a saved
|
|
43
|
+
review. Unselected candidates remain transient. The selected observation keeps
|
|
44
|
+
its original spelling, source digest and exact text range, not the surrounding
|
|
45
|
+
snippet. Text ranges use zero-based, end-exclusive UTF-16 code-unit offsets;
|
|
46
|
+
supplementary characters occupy two units. A declared source time is separate
|
|
47
|
+
from the local review time. Identify manual transcription or OCR-derived text
|
|
48
|
+
when that is how the supplied text was obtained.
|
|
49
|
+
|
|
50
|
+
The browser provides a shared declaration for selected occurrences. Open
|
|
51
|
+
**Declaration for candidate** to override a source label, role, observation time
|
|
52
|
+
or text basis for one occurrence. Apply or clear edits before saving or
|
|
53
|
+
downloading; only equivalent numbers with equivalent declarations share a group.
|
|
54
|
+
|
|
55
|
+
An advertised support contact and a sender or caller-ID claim describe different
|
|
56
|
+
roles; neither verifies ownership. International formatting can be compared only
|
|
57
|
+
where its prefix and supported separators permit a defensible normalisation.
|
|
58
|
+
National-format numbers, conflicting country context and unsupported characters
|
|
59
|
+
remain uncertain. Extensions are separate, not appended to the destination.
|
|
60
|
+
Dates, identifiers and unsupported forms must not be treated as verified numbers.
|
|
61
|
+
|
|
62
|
+
Selected numbers can still be private contacts. Review the selected content
|
|
63
|
+
before saving or sharing it. Ordinary public/trusted Case projections do not
|
|
64
|
+
include the private intake attachment; domain-focused interchange does not invent
|
|
65
|
+
a telephone mapping. A missing exported phone observation is not evidence that
|
|
66
|
+
none was supplied.
|
|
67
|
+
|
|
68
|
+
### Compare a displayed claim with a supplied destination
|
|
69
|
+
|
|
70
|
+
Use the intake's manual destination comparison when supplied ad, search or
|
|
71
|
+
screenshot material shows one destination and a separate source supplies another.
|
|
72
|
+
Identify each source, any known observation time, and whether its text is a claim,
|
|
73
|
+
transcription or supplied observation. The comparison uses the same safe hostname
|
|
74
|
+
parsing as an HTML link's displayed-host comparison.
|
|
75
|
+
|
|
76
|
+
Review the exact hostname and registrable boundary. A hostname such as
|
|
77
|
+
`store.example.test.attacker.invalid` is not `store.example.test`. Legitimate
|
|
78
|
+
tracking destinations can also differ. Missing, malformed or credential-bearing
|
|
79
|
+
input leaves insufficient evidence rather than inventing a redirect chain.
|
|
80
|
+
|
|
81
|
+
Exact entered URLs stay in the private transient view. Retained output uses the
|
|
82
|
+
minimised hostname/origin projection and supplied qualifications, not private
|
|
83
|
+
paths, query values or fragments. Comparing the pair never follows either URL
|
|
84
|
+
and does not establish that a redirect occurred.
|
|
85
|
+
|
|
86
|
+
## Optional distribution declarations
|
|
87
|
+
|
|
88
|
+
After reviewing input in Lookup or a Case, open **Declare distribution
|
|
89
|
+
context**. Supply a channel, non-sensitive source label and optional declared
|
|
90
|
+
time, reference, observer and vantage labels. Apply or clear changed declarations
|
|
91
|
+
before saving or downloading. These are analyst claims, not verified delivery,
|
|
92
|
+
capture conditions, network location or independent collection. The local review
|
|
93
|
+
time remains separate from a declared observation time.
|
|
94
|
+
|
|
95
|
+
The CLI accepts the same explicit declaration:
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
whoisleuth intake text selected.txt --intake-context context.json --json
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"schema": "whoisleuth.intake-context",
|
|
104
|
+
"version": 1,
|
|
105
|
+
"context": {
|
|
106
|
+
"channel": "sms",
|
|
107
|
+
"observedAt": "2026-01-02T03:04:05.000Z",
|
|
108
|
+
"sourceLabel": "Reported message",
|
|
109
|
+
"reference": "CASE-17",
|
|
110
|
+
"observerLabel": "Reviewer A",
|
|
111
|
+
"vantageLabel": "Selected device"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The separate context file is limited to 8 KiB; stdin is not accepted for it.
|
|
117
|
+
Use `null` for unknown optional values. Channels are `email`, `sms`, `messaging`,
|
|
118
|
+
`social`, `advertisement`, `website`, `document`, `other` and `unknown`.
|
|
119
|
+
Do not supply credentials, recipients, personal account identifiers or exact
|
|
120
|
+
URLs in labels. All declarations are included in the downloaded or deliberately
|
|
121
|
+
retained review. The Case summary pin contains counts and the report identity,
|
|
122
|
+
not the indicator values or declaration text. Retaining original input remains
|
|
123
|
+
a separate choice.
|
|
124
|
+
|
|
125
|
+
### Select phone observations in the CLI
|
|
126
|
+
|
|
127
|
+
First run `whoisleuth intake text selected.txt --json`. Standard error shows a
|
|
128
|
+
candidate count, the full source digest, UTF-16 ranges and uncertainty, but not
|
|
129
|
+
the numbers themselves. JSON output excludes unselected candidates. `--quiet`
|
|
130
|
+
suppresses this discovery guidance. Inspect the corresponding text in your
|
|
131
|
+
selected file before choosing a range.
|
|
132
|
+
|
|
133
|
+
A version-2 context file binds selections to those exact input bytes. For
|
|
134
|
+
example, a file containing `Support: +1 202 555 0107` followed by one LF newline
|
|
135
|
+
has this selection:
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"schema": "whoisleuth.intake-context",
|
|
140
|
+
"version": 2,
|
|
141
|
+
"context": null,
|
|
142
|
+
"review": {
|
|
143
|
+
"sourceDigestSha256": "sha256:4ff4224ff4ed496fb67fa2ccb25f87b284d1d291f8e79b57646d6c32700eeb52",
|
|
144
|
+
"phones": [{
|
|
145
|
+
"start": 9,
|
|
146
|
+
"end": 24,
|
|
147
|
+
"declaration": {
|
|
148
|
+
"sourceLabel": "Selected support snippet",
|
|
149
|
+
"observedAt": null,
|
|
150
|
+
"basis": "supplied_text",
|
|
151
|
+
"role": "advertised_support_contact",
|
|
152
|
+
"countryCallingCode": null
|
|
153
|
+
}
|
|
154
|
+
}],
|
|
155
|
+
"destinationPair": null
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Run the earlier `--intake-context context.json` command to include that selected
|
|
161
|
+
observation. A changed file or incorrect range is rejected; obtain its current
|
|
162
|
+
digest and review it again. `context` can also carry the existing distribution
|
|
163
|
+
declaration. Version-1 context files remain supported.
|
|
164
|
+
|
|
165
|
+
## Selected internal containment handoff
|
|
166
|
+
|
|
167
|
+
In **Case → Response → Account and device recovery**, open **Prepare an internal
|
|
168
|
+
containment handoff**. Select retained next-step assertions and, independently,
|
|
169
|
+
their linked evidence pins. Choose security operations, identity response,
|
|
170
|
+
endpoint response or network response for tailored review guidance.
|
|
171
|
+
|
|
172
|
+
The preview displays the selected statements, rationale, recorded state,
|
|
173
|
+
times, pin values and source metadata. **Exact JSON disclosure** exposes the
|
|
174
|
+
complete download, including selected pin scope, provenance and recheck fields.
|
|
175
|
+
Supports, contradicts, unresolved and
|
|
176
|
+
unrecorded relationships remain distinct. A linked pin that is not selected is
|
|
177
|
+
not silently included; an unavailable referenced pin remains unavailable.
|
|
178
|
+
The handoff is limited to 20 requests, 40 pins and 128 KiB, with no silent
|
|
179
|
+
truncation. No original file bytes or unrelated Case notes, title, contacts,
|
|
180
|
+
recipient values, assertions or pins are included.
|
|
181
|
+
|
|
182
|
+
Review the audience disclosure before copy, download or explicit retention.
|
|
183
|
+
Internal and trusted audiences use the existing Case disclosure policy. Public
|
|
184
|
+
preview excludes internal requests and supporting pins and cannot produce a
|
|
185
|
+
containment export. Selected analyst text may still be sensitive: minimisation
|
|
186
|
+
does not provide anonymisation. Saving uses an ordinary retained Case file and
|
|
187
|
+
a source-hashed summary pin, not another task or state store.
|
|
188
|
+
|
|
189
|
+
The existing `review-evidence` CLI command accepts an equivalent selected input:
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
{
|
|
193
|
+
"schema": "whoisleuth.internal-containment.input",
|
|
194
|
+
"version": 1,
|
|
195
|
+
"caseExport": { "version": 18, "cases": [] },
|
|
196
|
+
"caseId": "selected-case-id",
|
|
197
|
+
"selection": {
|
|
198
|
+
"audience": "internal",
|
|
199
|
+
"recipientRole": "identity_response",
|
|
200
|
+
"assertionIds": ["selected-next-step-id"],
|
|
201
|
+
"evidencePinIds": ["selected-linked-pin-id"]
|
|
202
|
+
},
|
|
203
|
+
"disclosureReviewed": true
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Replace the empty example with a supported ordinary Case export and exact
|
|
208
|
+
retained IDs after reviewing its selected content. The CLI does not write the
|
|
209
|
+
Case; `--strict-exit` returns 4 when selected supporting context is incomplete.
|
|
210
|
+
Output uses `whoisleuth.internal-containment` version 1 inside the existing
|
|
211
|
+
offline-review envelope. Neither external resolution nor a resolved Case closes
|
|
212
|
+
an open internal request. The handoff performs no assignment, notification,
|
|
213
|
+
blocking, credential change, isolation or other containment action.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { classifyStaticCredentialInput, staticControlDisabled, MAX_STATIC_FORMS, MAX_STATIC_INPUTS, } from "./static-html-analysis.mjs";
|
|
2
|
+
import { CREDENTIAL_CATEGORY_NAMES, MAX_FORM_DESTINATIONS, } from "../packages/evidence/credential-form-attribution.mjs";
|
|
3
|
+
const attribute = (element, name) => element.attributes.find(item => item.name === name)?.value;
|
|
4
|
+
/** HTML Auto buttons submit only without command attributes or a select parent. */
|
|
5
|
+
export function staticControlIsSubmitter(element, parent) {
|
|
6
|
+
const type = (attribute(element, 'type') ?? '').toLowerCase();
|
|
7
|
+
if (element.name === 'input')
|
|
8
|
+
return type === 'submit' || type === 'image';
|
|
9
|
+
if (element.name !== 'button')
|
|
10
|
+
return false;
|
|
11
|
+
if (type === 'submit')
|
|
12
|
+
return true;
|
|
13
|
+
if (type === 'button' || type === 'reset')
|
|
14
|
+
return false;
|
|
15
|
+
return attribute(element, 'command') === undefined && attribute(element, 'commandfor') === undefined
|
|
16
|
+
&& !(parent?.html && parent.name === 'select');
|
|
17
|
+
}
|
|
18
|
+
// Uses the existing bounded, inert-template-free tree. IDs and control values
|
|
19
|
+
// are transient association inputs; only ordinal forms and fixed categories
|
|
20
|
+
// leave this projection.
|
|
21
|
+
export function attributeCredentialForms(analysis, documentUrl, sourceTruncated = false) {
|
|
22
|
+
const elements = analysis.elements;
|
|
23
|
+
const firstId = new Map();
|
|
24
|
+
const firstLegends = new Map();
|
|
25
|
+
let firstUncertainId = Number.POSITIVE_INFINITY;
|
|
26
|
+
const formIndices = [];
|
|
27
|
+
let complete = !sourceTruncated && !analysis.inputLimitReached && !analysis.tagLimitReached && !analysis.forms.truncated;
|
|
28
|
+
for (const [index, element] of elements.entries()) {
|
|
29
|
+
if (element.attributesTruncated && attribute(element, 'id') === undefined)
|
|
30
|
+
firstUncertainId = Math.min(firstUncertainId, index);
|
|
31
|
+
const id = attribute(element, 'id');
|
|
32
|
+
if (id !== undefined && id.length <= 4096 && !firstId.has(id))
|
|
33
|
+
firstId.set(id, index);
|
|
34
|
+
if (element.html && element.name === 'form') {
|
|
35
|
+
if (formIndices.length < MAX_STATIC_FORMS)
|
|
36
|
+
formIndices.push(index);
|
|
37
|
+
else
|
|
38
|
+
complete = false;
|
|
39
|
+
}
|
|
40
|
+
if (element.html && element.name === 'legend' && element.parent !== null && !firstLegends.has(element.parent))
|
|
41
|
+
firstLegends.set(element.parent, index);
|
|
42
|
+
}
|
|
43
|
+
const ancestor = (index, name) => {
|
|
44
|
+
for (let parent = elements[index]?.parent ?? null; parent !== null; parent = elements[parent]?.parent ?? null) {
|
|
45
|
+
const element = elements[parent];
|
|
46
|
+
if (element.html && element.name === name)
|
|
47
|
+
return parent;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
};
|
|
51
|
+
const owner = (index) => {
|
|
52
|
+
const element = elements[index];
|
|
53
|
+
if (element.attributesTruncated)
|
|
54
|
+
return null;
|
|
55
|
+
for (let parent = element.parent; parent !== null; parent = elements[parent]?.parent ?? null) {
|
|
56
|
+
if (elements[parent]?.name === 'fieldset' && elements[parent]?.attributesTruncated)
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const explicit = attribute(element, 'form');
|
|
60
|
+
if (explicit !== undefined) {
|
|
61
|
+
const selected = firstId.get(explicit);
|
|
62
|
+
// A clipped earlier ID could shadow this form in the browser's first-ID
|
|
63
|
+
// lookup. Do not attribute its controls to the later retained match.
|
|
64
|
+
return selected !== undefined && selected < firstUncertainId && elements[selected]?.html && elements[selected]?.name === 'form' ? selected : null;
|
|
65
|
+
}
|
|
66
|
+
return ancestor(index, 'form');
|
|
67
|
+
};
|
|
68
|
+
const destination = (form, submitter) => {
|
|
69
|
+
const method = (submitter && attribute(submitter, 'formmethod')) ?? attribute(form, 'method') ?? 'get';
|
|
70
|
+
if (method.toLowerCase() === 'dialog')
|
|
71
|
+
return { relationship: 'no_submission', origin: null };
|
|
72
|
+
if (form.attributesTruncated || submitter?.attributesTruncated)
|
|
73
|
+
return { relationship: 'unknown', origin: null };
|
|
74
|
+
const action = (submitter && attribute(submitter, 'formaction')) ?? attribute(form, 'action');
|
|
75
|
+
if (action !== undefined && (action.length > 4096 || /[\u0000-\u001f\u007f]/u.test(action)))
|
|
76
|
+
return { relationship: 'unknown', origin: null };
|
|
77
|
+
try {
|
|
78
|
+
const source = new URL(documentUrl);
|
|
79
|
+
// Missing/empty actions use the document URL, not the <base> URL.
|
|
80
|
+
if (action?.trim() && analysis.baseHrefState === 'invalid')
|
|
81
|
+
return { relationship: 'unknown', origin: null };
|
|
82
|
+
const target = new URL(action?.trim() || documentUrl, analysis.effectiveBaseUrl ?? documentUrl);
|
|
83
|
+
if (!['http:', 'https:'].includes(target.protocol) || target.username || target.password || target.origin.length > 320)
|
|
84
|
+
return { relationship: 'unknown', origin: null };
|
|
85
|
+
return { relationship: target.origin === source.origin ? 'same_origin' : 'external', origin: target.origin };
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return { relationship: 'unknown', origin: null };
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const forms = formIndices.map((index, ordinal) => ({
|
|
92
|
+
index: ordinal + 1,
|
|
93
|
+
categories: Object.fromEntries(CREDENTIAL_CATEGORY_NAMES.map(name => [name, 0])),
|
|
94
|
+
destinations: [destination(elements[index])],
|
|
95
|
+
}));
|
|
96
|
+
const formRows = new Map(formIndices.map((index, ordinal) => [index, forms[ordinal]]));
|
|
97
|
+
let unassociatedInputs = 0;
|
|
98
|
+
let controls = 0;
|
|
99
|
+
for (const [index, element] of elements.entries()) {
|
|
100
|
+
if (!element.html || !['input', 'button'].includes(element.name))
|
|
101
|
+
continue;
|
|
102
|
+
if (++controls > MAX_STATIC_INPUTS) {
|
|
103
|
+
complete = false;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
if (staticControlDisabled(elements, index, firstLegends))
|
|
107
|
+
continue;
|
|
108
|
+
const categories = element.name === 'input' ? classifyStaticCredentialInput(element.attributes) : { values: [], truncated: false };
|
|
109
|
+
if (categories.truncated || element.attributesTruncated)
|
|
110
|
+
complete = false;
|
|
111
|
+
const ownerIndex = owner(index);
|
|
112
|
+
const row = ownerIndex === null ? undefined : formRows.get(ownerIndex);
|
|
113
|
+
if (categories.values.length) {
|
|
114
|
+
if (!row) {
|
|
115
|
+
unassociatedInputs++;
|
|
116
|
+
complete = false;
|
|
117
|
+
}
|
|
118
|
+
else
|
|
119
|
+
for (const category of categories.values)
|
|
120
|
+
row.categories[category]++;
|
|
121
|
+
}
|
|
122
|
+
const submitter = staticControlIsSubmitter(element, element.parent === null ? undefined : elements[element.parent]);
|
|
123
|
+
if (row && ownerIndex !== null && submitter && (attribute(element, 'formaction') !== undefined || attribute(element, 'formmethod') !== undefined)) {
|
|
124
|
+
const next = destination(elements[ownerIndex], element);
|
|
125
|
+
if (!row.destinations.some(value => value.relationship === next.relationship && value.origin === next.origin)) {
|
|
126
|
+
if (row.destinations.length >= MAX_FORM_DESTINATIONS)
|
|
127
|
+
complete = false;
|
|
128
|
+
else
|
|
129
|
+
row.destinations.push(next);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (forms.some(form => form.destinations.some(value => value.relationship === 'unknown')))
|
|
134
|
+
complete = false;
|
|
135
|
+
return { forms, unassociatedInputs, complete };
|
|
136
|
+
}
|
|
@@ -5,22 +5,25 @@
|
|
|
5
5
|
// never enter this profile.
|
|
6
6
|
import { createObservation } from "../packages/evidence/observation.mjs";
|
|
7
7
|
import { CREDENTIAL_SURFACE_PROFILE_VERSION } from "./lookup-child-profile-contract.mjs";
|
|
8
|
+
import { attributeCredentialForms } from "./credential-form-attribution.mjs";
|
|
8
9
|
import { MAX_STATIC_FORMS, MAX_STATIC_INPUTS, analyzeStaticHtml, } from "./static-html-analysis.mjs";
|
|
9
10
|
function analyzeCredentialSurfaceProfile(input = {}) {
|
|
10
11
|
const htmlAnalysis = input.htmlAnalysis ?? analyzeStaticHtml(input.html, { baseUrl: input.baseUrl });
|
|
11
12
|
const forms = htmlAnalysis.forms;
|
|
13
|
+
const formAttribution = input.formAttribution ?? attributeCredentialForms(htmlAnalysis, typeof input.baseUrl === 'string' ? input.baseUrl : '', input.sourceTruncated === true);
|
|
12
14
|
const sourceTruncated = input.sourceTruncated === true;
|
|
13
15
|
const truncated = sourceTruncated
|
|
14
16
|
|| htmlAnalysis.inputLimitReached
|
|
15
17
|
|| htmlAnalysis.tagLimitReached
|
|
16
18
|
|| forms.truncated;
|
|
17
19
|
const unclassifiedActions = forms.actions.unclassified;
|
|
18
|
-
const partial = truncated || unclassifiedActions > 0;
|
|
20
|
+
const partial = truncated || unclassifiedActions > 0 || !formAttribution.complete;
|
|
19
21
|
const limitations = [
|
|
20
22
|
'Static input categories use only bounded type and autocomplete declarations; JavaScript-rendered, non-semantic, disabled, and hidden controls are not classified.',
|
|
21
23
|
'Category counts can overlap when one input declares more than one recognised purpose.',
|
|
22
24
|
'External submission can be legitimate and does not establish phishing, unsafe handling, ownership, intent, or maliciousness.',
|
|
23
|
-
'
|
|
25
|
+
'Only form ordinals, input-purpose counts and declared destination origins are retained; no field names, values, labels, placeholders, URL paths, queries or fragments.',
|
|
26
|
+
'Declared destinations, including submitter overrides, are not observed submissions. Script-created or changed forms are not evaluated.',
|
|
24
27
|
];
|
|
25
28
|
if (sourceTruncated || htmlAnalysis.inputLimitReached) {
|
|
26
29
|
limitations.push('The captured homepage body was truncated, so credential-surface evidence may be incomplete.');
|
|
@@ -36,6 +39,7 @@ function analyzeCredentialSurfaceProfile(input = {}) {
|
|
|
36
39
|
}
|
|
37
40
|
return {
|
|
38
41
|
credentialSurfaceVersion: CREDENTIAL_SURFACE_PROFILE_VERSION,
|
|
42
|
+
formAttribution,
|
|
39
43
|
...createObservation({
|
|
40
44
|
status: partial ? 'partial' : 'success',
|
|
41
45
|
observedAt: input.observedAt,
|
package/lib/dmarc-discovery.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isValidAsciiHostname } from "../packages/contracts/domain-name.mjs";
|
|
2
2
|
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
3
3
|
import { parseDmarcRecords } from "./domain-posture-parsers.mjs";
|
|
4
|
+
import { MAX_DMARC_DISCOVERY_OWNERS } from "../packages/evidence/dmarc-policy-context.mjs";
|
|
4
5
|
export function requireDmarcDomain(domain) {
|
|
5
6
|
if (typeof domain !== 'string' || domain.length > 253 || !isValidAsciiHostname(domain) || domain !== domain.toLowerCase()) {
|
|
6
7
|
throw new TypeError('The additional DNS review requires a normalised domain.');
|
|
@@ -29,7 +30,8 @@ export function dmarcPolicyObservation(owner, observation) {
|
|
|
29
30
|
: state;
|
|
30
31
|
const time = typeof observation.observedAt === 'string' && observation.observedAt.length <= 64
|
|
31
32
|
? normalizeExplicitIsoTimestamp(observation.observedAt) : null;
|
|
32
|
-
return { owner, parsed, psd, valid, unavailable, incomplete,
|
|
33
|
+
return { owner, parsed, psd, valid, unavailable, incomplete, observedAt: time,
|
|
34
|
+
state: unavailable ? 'unavailable' : valid ? 'observed' : parsed.records.length ? 'invalid' : 'not_found',
|
|
33
35
|
record: `Recursive DNS TXT _dmarc.${owner} · ${time ?? 'time unavailable'} · ${description}` };
|
|
34
36
|
}
|
|
35
37
|
/** RFC 9989 sections 4.10–4.10.2: at most eight owners, never PSL inference. */
|
|
@@ -38,7 +40,7 @@ export async function walkDmarcTree(domain, exact, resolve, purpose) {
|
|
|
38
40
|
const rows = [dmarcPolicyObservation(domain, exact)];
|
|
39
41
|
const first = rows[0];
|
|
40
42
|
const labels = domain.split('.');
|
|
41
|
-
let next = labels.length >=
|
|
43
|
+
let next = labels.length >= MAX_DMARC_DISCOVERY_OWNERS ? labels.slice(-(MAX_DMARC_DISCOVERY_OWNERS - 1)) : labels.slice(1);
|
|
42
44
|
const policyOnly = purpose === 'policy' && first.valid;
|
|
43
45
|
if (!policyOnly && !first.unavailable && !(first.valid && ['n', 'y'].includes(first.psd))) {
|
|
44
46
|
while (next.length) {
|