@slicedearth/whoisleuth-cli 2.7.0 → 2.8.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/cli/bulk-command-runner.mjs +5 -4
- package/cli/collection-arguments.mjs +6 -0
- package/cli/collection-command-definitions.mjs +13 -3
- package/cli/command-definition.mjs +2 -0
- package/cli/discovery-scan-command-runner.mjs +13 -7
- package/cli/domain-control-monitor.mjs +25 -2
- package/cli/domain-control-observations.mjs +52 -15
- package/cli/fail-policy.mjs +31 -22
- package/cli/lookup-command-runner.mjs +6 -5
- package/cli/lookup-diff.mjs +1 -0
- package/cli/risk-policy.mjs +83 -0
- package/cli/workflow-command-runner.mjs +5 -1
- package/docs/cli-reference.md +7 -0
- package/docs/cli.md +53 -1
- package/lib/generated/cisa-kev-catalog.mjs +5 -3
- package/lib/sslbl-certificates.generated.mjs +139 -139
- package/package.json +1 -1
- package/packages/analysis/brand-profile-signals.mjs +42 -0
- package/packages/investigation/bulk-domain-comparison.mjs +17 -8
- package/third-party-notices.txt +1 -1
package/package.json
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { hammingDistanceHex, isInformativePerceptualHash } from "./perceptual-hash-comparison.mjs";
|
|
2
|
+
function normalizedDomain(value) {
|
|
3
|
+
return value.trim().toLowerCase().replace(/\.$/u, '');
|
|
4
|
+
}
|
|
5
|
+
/** Shared exact comparison and role precedence for browser and CLI consumers. */
|
|
6
|
+
export function profileDomainMatch(domain, profile = null) {
|
|
7
|
+
if (!profile || !domain)
|
|
8
|
+
return null;
|
|
9
|
+
const target = normalizedDomain(domain);
|
|
10
|
+
const lists = [
|
|
11
|
+
['official', profile.officialDomains, 'Exact official-domain declaration'],
|
|
12
|
+
['partner', profile.approvedPartnerDomains, 'Exact approved-partner declaration'],
|
|
13
|
+
['allowlisted', profile.allowlistedDomains, 'Exact domain allowlist entry'],
|
|
14
|
+
];
|
|
15
|
+
for (const [kind, values, reason] of lists) {
|
|
16
|
+
const matched = values.find(value => normalizedDomain(value) === target);
|
|
17
|
+
if (matched !== undefined)
|
|
18
|
+
return { kind, matchedDomain: normalizedDomain(matched), reason };
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
export function profileDomainKind(domain, profile = null) {
|
|
23
|
+
return profileDomainMatch(domain, profile)?.kind ?? null;
|
|
24
|
+
}
|
|
25
|
+
export function profileSignals(domain, evidence, profile = null) {
|
|
26
|
+
const trusted = profileDomainKind(domain, profile);
|
|
27
|
+
if (!profile || trusted)
|
|
28
|
+
return { trusted, faviconMatch: false, faviconNearMatch: false, reusesOfficialAssets: false };
|
|
29
|
+
const exact = Boolean(evidence.faviconHash && profile.officialFaviconHash && evidence.faviconHash === profile.officialFaviconHash);
|
|
30
|
+
const distance = isInformativePerceptualHash(evidence.faviconPHash) && isInformativePerceptualHash(profile.officialFaviconPHash)
|
|
31
|
+
? hammingDistanceHex(evidence.faviconPHash, profile.officialFaviconPHash)
|
|
32
|
+
: null;
|
|
33
|
+
const official = new Set(profile.officialDomains.map(normalizedDomain));
|
|
34
|
+
const reused = Array.isArray(evidence.externalAssetHosts)
|
|
35
|
+
&& evidence.externalAssetHosts.some((host) => typeof host === 'string' && official.has(normalizedDomain(host)));
|
|
36
|
+
return {
|
|
37
|
+
trusted: null,
|
|
38
|
+
faviconMatch: exact,
|
|
39
|
+
faviconNearMatch: !exact && distance !== null && distance <= 8,
|
|
40
|
+
reusesOfficialAssets: reused,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { normalizeBulkSessionResult, } from "../workspace/bulk-session-model.mjs";
|
|
2
2
|
import { SORTED_JSON_V2, sha256ArtifactDigestV2 } from "../evidence/artifact-integrity.mjs";
|
|
3
|
+
import { webCollectionAllowsComparison } from "../evidence/collection-quality.mjs";
|
|
3
4
|
import { BULK_REVIEW_STALE_AFTER_DAYS } from "./bulk-review-policy.mjs";
|
|
4
5
|
import { BULK_DOMAIN_COMPARISON_EXPORT_VERSION, BULK_DOMAIN_COMPARISON_SCHEMA, BULK_DOMAIN_COMPARISON_VERSION, } from "../contracts/investigation-portability.mjs";
|
|
5
6
|
export { BULK_DOMAIN_COMPARISON_EXPORT_VERSION, BULK_DOMAIN_COMPARISON_SCHEMA, BULK_DOMAIN_COMPARISON_VERSION };
|
|
@@ -90,7 +91,7 @@ function row(id, category, label, left, right, method, context, limitations = []
|
|
|
90
91
|
label,
|
|
91
92
|
left: options.retained === false ? 'Not recorded in compact Bulk evidence' : display(left),
|
|
92
93
|
right: options.retained === false ? 'Not recorded in compact Bulk evidence' : display(right),
|
|
93
|
-
state: state(left, right, context.leftSourceState, context.rightSourceState, options),
|
|
94
|
+
state: context.webComparable === false ? 'unavailable' : state(left, right, context.leftSourceState, context.rightSourceState, options),
|
|
94
95
|
method: text(method, 320),
|
|
95
96
|
source: text(context.source, 80),
|
|
96
97
|
leftSourceState: context.leftSourceState,
|
|
@@ -98,7 +99,10 @@ function row(id, category, label, left, right, method, context, limitations = []
|
|
|
98
99
|
observedAt: context.observedAt,
|
|
99
100
|
leftEvidenceHref: context.leftEvidenceHref,
|
|
100
101
|
rightEvidenceHref: context.rightEvidenceHref,
|
|
101
|
-
limitations:
|
|
102
|
+
limitations: [
|
|
103
|
+
...(context.webComparable === false ? ['Incomplete or unknown web collection; retained values are not comparable.'] : []),
|
|
104
|
+
...limitations,
|
|
105
|
+
].map((item) => text(item, 320)).filter(Boolean).slice(0, 6),
|
|
102
106
|
};
|
|
103
107
|
}
|
|
104
108
|
function sourceSummary(value) {
|
|
@@ -157,6 +161,11 @@ export function buildBulkDomainComparison(leftRaw, rightRaw, observedAtRaw = nul
|
|
|
157
161
|
const registry = context(left, right, 'Registry and registrar evidence', ['rdap', 'availability'], observedAt, options);
|
|
158
162
|
const dns = context(left, right, 'DNS evidence', ['dns'], observedAt, options);
|
|
159
163
|
const http = context(left, right, 'HTTP and static page evidence', ['http'], observedAt, options);
|
|
164
|
+
const webContext = (field, base = http) => ({
|
|
165
|
+
...base,
|
|
166
|
+
webComparable: webCollectionAllowsComparison(field, left.webCollectionQuality, left.scanDepth)
|
|
167
|
+
&& webCollectionAllowsComparison(field, right.webCollectionQuality, right.scanDepth),
|
|
168
|
+
});
|
|
160
169
|
const tls = context(left, right, 'TLS evidence', ['tls'], observedAt, options);
|
|
161
170
|
const compactTechnology = comparisonEvidenceContext(left, right, 'technology', {
|
|
162
171
|
...http,
|
|
@@ -203,13 +212,13 @@ export function buildBulkDomainComparison(leftRaw, rightRaw, observedAtRaw = nul
|
|
|
203
212
|
row('null-mx', 'mail', 'Null MX observed', left.hasNullMx, right.hasNullMx, 'Compact Deep DNS observation', dns),
|
|
204
213
|
row('spf', 'mail', 'SPF observed', left.hasSpf, right.hasSpf, 'Compact Deep DNS observation', dns),
|
|
205
214
|
row('dmarc', 'mail', 'DMARC observed', left.hasDmarc, right.hasDmarc, 'Compact Deep DNS observation', dns),
|
|
206
|
-
row('website', 'web', 'Website activity', left.activityStatus, right.activityStatus, 'Bounded homepage observation',
|
|
207
|
-
row('page-title', 'identity', 'Page title', left.pageTitle, right.pageTitle, 'Normalised bounded title equality',
|
|
208
|
-
row('favicon', 'identity', 'Favicon fingerprint', left.faviconHash, right.faviconHash, 'Exact SHA-256 comparison',
|
|
215
|
+
row('website', 'web', 'Website activity', left.activityStatus, right.activityStatus, 'Bounded homepage observation', webContext('activityStatus')),
|
|
216
|
+
row('page-title', 'identity', 'Page title', left.pageTitle, right.pageTitle, 'Normalised bounded title equality', webContext('pageTitle')),
|
|
217
|
+
row('favicon', 'identity', 'Favicon fingerprint', left.faviconHash, right.faviconHash, 'Exact SHA-256 comparison', webContext('faviconHash')),
|
|
209
218
|
row('tracking', 'identity', 'Tracking identifiers', left.relationship.trackingIdentifiers, right.relationship.trackingIdentifiers, 'Normalised exact set comparison', http, ['Shared identifiers are an investigative lead, not proof of ownership.']),
|
|
210
|
-
row('password-field', 'identity', 'Password field', left.hasPasswordField, right.hasPasswordField, 'Bounded static form observation',
|
|
211
|
-
row('phishing-language', 'identity', 'Phishing-language indicator', left.phishingLanguageMatch, right.phishingLanguageMatch, 'Bounded explainable page-language signal',
|
|
212
|
-
row('official-assets', 'identity', 'Official asset reuse', profileContextsComparable ? left.reusesOfficialAssets : null, profileContextsComparable ? right.reusesOfficialAssets : null, 'Configured Brand Profile comparison', profileContext, profileLimitations),
|
|
219
|
+
row('password-field', 'identity', 'Password field', left.hasPasswordField, right.hasPasswordField, 'Bounded static form observation', webContext('hasPasswordField')),
|
|
220
|
+
row('phishing-language', 'identity', 'Phishing-language indicator', left.phishingLanguageMatch, right.phishingLanguageMatch, 'Bounded explainable page-language signal', webContext('phishingLanguageMatch'), ['A wording match is a review lead, not proof of malicious intent.']),
|
|
221
|
+
row('official-assets', 'identity', 'Official asset reuse', profileContextsComparable ? left.reusesOfficialAssets : null, profileContextsComparable ? right.reusesOfficialAssets : null, 'Configured Brand Profile comparison', webContext('reusesOfficialAssets', profileContext), profileLimitations),
|
|
213
222
|
row('technology', 'technology', 'Technology identifiers', left.comparisonEvidence?.technology.ids ?? [], right.comparisonEvidence?.technology.ids ?? [], 'Normalised exact set comparison of at most 12 curated identifiers', compactTechnology, ['An unmatched identifier is not evidence that a technology is absent. Shared technologies do not establish common ownership or control.']),
|
|
214
223
|
row('source-health', 'source', 'Recorded source coverage', sourceSummary(left), sourceSummary(right), 'Source-by-source compact state comparison', source, ['A source-state difference can reflect collection conditions rather than a domain change.']),
|
|
215
224
|
];
|
package/third-party-notices.txt
CHANGED
|
@@ -516,7 +516,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
516
516
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
517
517
|
SOFTWARE.
|
|
518
518
|
================================================================================
|
|
519
|
-
asn1js@3.0
|
|
519
|
+
asn1js@3.1.0
|
|
520
520
|
Relationship: transitive production dependency
|
|
521
521
|
Declared licence: BSD-3-Clause
|
|
522
522
|
Licence source: LICENSE
|