@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
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { normalizeBulkSessionResult, } from "
|
|
2
|
-
import {
|
|
3
|
-
import { BULK_REVIEW_STALE_AFTER_DAYS } from "./bulk-
|
|
4
|
-
|
|
5
|
-
export
|
|
1
|
+
import { normalizeBulkSessionResult, } from "../workspace/bulk-session-model.mjs";
|
|
2
|
+
import { SORTED_JSON_V2, sha256ArtifactDigestV2 } from "../evidence/artifact-integrity.mjs";
|
|
3
|
+
import { BULK_REVIEW_STALE_AFTER_DAYS } from "./bulk-review-policy.mjs";
|
|
4
|
+
import { BULK_DOMAIN_COMPARISON_EXPORT_VERSION, BULK_DOMAIN_COMPARISON_SCHEMA, BULK_DOMAIN_COMPARISON_VERSION, } from "../contracts/investigation-portability.mjs";
|
|
5
|
+
export { BULK_DOMAIN_COMPARISON_EXPORT_VERSION, BULK_DOMAIN_COMPARISON_SCHEMA, BULK_DOMAIN_COMPARISON_VERSION };
|
|
6
6
|
function text(value, maximum = 500) {
|
|
7
7
|
return typeof value === 'string'
|
|
8
8
|
? value.replace(/[\u0000-\u001f\u007f]/gu, ' ').replace(/\s+/gu, ' ').trim().slice(0, maximum)
|
|
@@ -164,6 +164,23 @@ export function buildBulkDomainComparison(leftRaw, rightRaw, observedAtRaw = nul
|
|
|
164
164
|
});
|
|
165
165
|
const compactTls = comparisonEvidenceContext(left, right, 'tls', tls);
|
|
166
166
|
const source = context(left, right, 'Recorded source coverage', [], observedAt, options);
|
|
167
|
+
const profileContextsComparable = left.profileContext.sourceState === 'ready'
|
|
168
|
+
&& right.profileContext.sourceState === 'ready'
|
|
169
|
+
&& left.profileContext.activeProfileId === right.profileContext.activeProfileId
|
|
170
|
+
&& left.profileContext.profileUpdatedAt === right.profileContext.profileUpdatedAt;
|
|
171
|
+
const profileContext = {
|
|
172
|
+
...http,
|
|
173
|
+
source: 'Brand Profile comparison context',
|
|
174
|
+
leftSourceState: profileContextsComparable ? 'complete' : 'unavailable',
|
|
175
|
+
rightSourceState: profileContextsComparable ? 'complete' : 'unavailable',
|
|
176
|
+
};
|
|
177
|
+
const profileLimitations = profileContextsComparable
|
|
178
|
+
? []
|
|
179
|
+
: [
|
|
180
|
+
left.profileContext.limitation,
|
|
181
|
+
right.profileContext.limitation,
|
|
182
|
+
'Official-asset comparison is withheld unless both rows retain the same ready Brand Profile provenance.',
|
|
183
|
+
].filter(Boolean);
|
|
167
184
|
const registrationConflicting = [left.availability, right.availability]
|
|
168
185
|
.some((value) => ['conflict', 'conflicting'].includes(value.toLowerCase()));
|
|
169
186
|
const rows = [
|
|
@@ -192,7 +209,7 @@ export function buildBulkDomainComparison(leftRaw, rightRaw, observedAtRaw = nul
|
|
|
192
209
|
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.']),
|
|
193
210
|
row('password-field', 'identity', 'Password field', left.hasPasswordField, right.hasPasswordField, 'Bounded static form observation', http),
|
|
194
211
|
row('phishing-language', 'identity', 'Phishing-language indicator', left.phishingLanguageMatch, right.phishingLanguageMatch, 'Bounded explainable page-language signal', http, ['A wording match is a review lead, not proof of malicious intent.']),
|
|
195
|
-
row('official-assets', 'identity', 'Official asset reuse', left.reusesOfficialAssets, right.reusesOfficialAssets, 'Configured Brand Profile comparison',
|
|
212
|
+
row('official-assets', 'identity', 'Official asset reuse', profileContextsComparable ? left.reusesOfficialAssets : null, profileContextsComparable ? right.reusesOfficialAssets : null, 'Configured Brand Profile comparison', profileContext, profileLimitations),
|
|
196
213
|
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.']),
|
|
197
214
|
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.']),
|
|
198
215
|
];
|
|
@@ -219,11 +236,13 @@ export function buildBulkDomainComparison(leftRaw, rightRaw, observedAtRaw = nul
|
|
|
219
236
|
'This compares normalised evidence already present in two saved observations for the same domain and makes no new request.',
|
|
220
237
|
'Missing, unavailable, and differently collected evidence remain distinct from an observed difference.',
|
|
221
238
|
'An observed difference can reflect changed collection conditions and does not by itself establish current state, ownership, intent, safety, or maliciousness.',
|
|
239
|
+
...(!profileContextsComparable ? ['Profile-derived identity comparison is unavailable because the saved Brand Profile contexts are not ready and identical.'] : []),
|
|
222
240
|
]
|
|
223
241
|
: [
|
|
224
242
|
'This compares compact settled evidence already present in Bulk and makes no new request.',
|
|
225
243
|
'Missing, unavailable, and differently collected evidence remain distinct from an observed difference.',
|
|
226
244
|
'Equality does not establish common ownership, infrastructure control, intent, safety, or maliciousness.',
|
|
245
|
+
...(!profileContextsComparable ? ['Profile-derived identity comparison is unavailable because the saved Brand Profile contexts are not ready and identical.'] : []),
|
|
227
246
|
],
|
|
228
247
|
};
|
|
229
248
|
}
|
|
@@ -235,14 +254,14 @@ export async function buildBulkDomainComparisonExport(comparisonRaw, generatedAt
|
|
|
235
254
|
const generatedAt = timestamp(generatedAtRaw) || new Date().toISOString();
|
|
236
255
|
const unsigned = {
|
|
237
256
|
schema: BULK_DOMAIN_COMPARISON_SCHEMA,
|
|
238
|
-
version:
|
|
257
|
+
version: BULK_DOMAIN_COMPARISON_EXPORT_VERSION,
|
|
239
258
|
generatedAt,
|
|
240
259
|
comparison,
|
|
241
260
|
};
|
|
242
|
-
const digestSha256 = await
|
|
261
|
+
const digestSha256 = await sha256ArtifactDigestV2(unsigned);
|
|
243
262
|
const document = {
|
|
244
263
|
...unsigned,
|
|
245
|
-
integrity: { algorithm: 'SHA-256', digestSha256 },
|
|
264
|
+
integrity: { algorithm: 'SHA-256', canonicalization: SORTED_JSON_V2, digestSha256 },
|
|
246
265
|
};
|
|
247
266
|
return {
|
|
248
267
|
document,
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { normalizeBulkSessionResult, } from "
|
|
3
|
-
|
|
4
|
-
export
|
|
5
|
-
export const MAX_BULK_MAIL_EXPOSURE_ROWS = 2_000;
|
|
1
|
+
import { SORTED_JSON_V2, sha256ArtifactDigestV2 } from "../evidence/artifact-integrity.mjs";
|
|
2
|
+
import { BULK_PROFILE_CONTEXT_MISMATCH_LIMITATION, normalizeBulkProfileContext, normalizeBulkSessionResult, } from "../workspace/bulk-session-model.mjs";
|
|
3
|
+
import { BULK_MAIL_EXPOSURE_EXPORT_VERSION, BULK_MAIL_EXPOSURE_SCHEMA, BULK_MAIL_EXPOSURE_VERSION, MAX_BULK_MAIL_EXPOSURE_ROWS, } from "../contracts/investigation-portability.mjs";
|
|
4
|
+
export { BULK_MAIL_EXPOSURE_EXPORT_VERSION, BULK_MAIL_EXPOSURE_SCHEMA, BULK_MAIL_EXPOSURE_VERSION, MAX_BULK_MAIL_EXPOSURE_ROWS };
|
|
6
5
|
const STATE_LABELS = {
|
|
7
6
|
authenticated_mail: 'Receiving mail with SPF and DMARC',
|
|
8
7
|
evidence_incomplete: 'Mail evidence incomplete',
|
|
@@ -16,7 +15,11 @@ function timestamp(value) {
|
|
|
16
15
|
return null;
|
|
17
16
|
return new Date(value).toISOString();
|
|
18
17
|
}
|
|
19
|
-
function baselineLabel(profile) {
|
|
18
|
+
function baselineLabel(profile, profileSourceState) {
|
|
19
|
+
if (profileSourceState === 'loading')
|
|
20
|
+
return 'Brand Profile context loading';
|
|
21
|
+
if (profileSourceState === 'unavailable')
|
|
22
|
+
return 'Brand Profile context unavailable';
|
|
20
23
|
if (profile === 'defensive_no_mail')
|
|
21
24
|
return 'Configured no-mail baseline';
|
|
22
25
|
if (profile === 'parked')
|
|
@@ -64,7 +67,21 @@ function stateDetail(state) {
|
|
|
64
67
|
}
|
|
65
68
|
return 'The compact scan did not retain enough settled DNS evidence to classify mail exposure.';
|
|
66
69
|
}
|
|
67
|
-
function relation(state, profile) {
|
|
70
|
+
function relation(state, profile, profileSourceState, profileContextComparable, profileContextLimitation) {
|
|
71
|
+
if (profileSourceState !== 'ready') {
|
|
72
|
+
return {
|
|
73
|
+
baselineRelation: 'inconclusive',
|
|
74
|
+
baselineDetail: profileSourceState === 'loading'
|
|
75
|
+
? 'Brand Profile context is still loading, so no profile-derived mail comparison is available.'
|
|
76
|
+
: 'Brand Profile context could not be read, so no profile-derived mail comparison is available.',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (!profileContextComparable) {
|
|
80
|
+
return {
|
|
81
|
+
baselineRelation: 'inconclusive',
|
|
82
|
+
baselineDetail: profileContextLimitation || BULK_PROFILE_CONTEXT_MISMATCH_LIMITATION,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
68
85
|
if (!profile || state === 'evidence_incomplete' || state === 'mail_auth_incomplete') {
|
|
69
86
|
return {
|
|
70
87
|
baselineRelation: 'inconclusive',
|
|
@@ -102,8 +119,16 @@ function relation(state, profile) {
|
|
|
102
119
|
baselineDetail: 'The parked-domain profile is context rather than a claim about the expected candidate mail posture.',
|
|
103
120
|
};
|
|
104
121
|
}
|
|
105
|
-
function rowLimitations(row, state) {
|
|
122
|
+
function rowLimitations(row, state, profileSourceState, profileContextComparable, profileContextLimitation) {
|
|
106
123
|
const output = [];
|
|
124
|
+
if (profileSourceState !== 'ready') {
|
|
125
|
+
output.push(profileSourceState === 'loading'
|
|
126
|
+
? 'Brand Profile context was still loading; official-domain mail posture was not evaluated.'
|
|
127
|
+
: 'Brand Profile context was unavailable; official-domain mail posture was not evaluated.');
|
|
128
|
+
}
|
|
129
|
+
if (profileSourceState === 'ready' && !profileContextComparable) {
|
|
130
|
+
output.push(profileContextLimitation || BULK_PROFILE_CONTEXT_MISMATCH_LIMITATION);
|
|
131
|
+
}
|
|
107
132
|
const coverage = dnsCoverage(row);
|
|
108
133
|
if (!coverage)
|
|
109
134
|
output.push('DNS source coverage was not recorded for this compact result.');
|
|
@@ -115,30 +140,48 @@ function rowLimitations(row, state) {
|
|
|
115
140
|
output.push('No explicit MX is not equivalent to a null MX or proof that the domain cannot receive mail.');
|
|
116
141
|
}
|
|
117
142
|
output.push('Mail configuration alone does not establish use, intent, control, safety, or maliciousness.');
|
|
118
|
-
return output.slice(0,
|
|
143
|
+
return output.slice(0, 6);
|
|
119
144
|
}
|
|
120
145
|
export function buildBulkMailExposureReport(rowsRaw, options = {}) {
|
|
121
146
|
const generatedAt = timestamp(options.generatedAt) ?? new Date().toISOString();
|
|
122
|
-
const
|
|
147
|
+
const profileSourceState = options.profileSourceState ?? 'ready';
|
|
148
|
+
const currentProfileContext = normalizeBulkProfileContext(options.currentProfileContext ?? (profileSourceState === 'ready'
|
|
149
|
+
? { sourceState: 'ready', activeProfileId: null, profileUpdatedAt: null, limitation: '' }
|
|
150
|
+
: { sourceState: 'unavailable' }));
|
|
151
|
+
const profile = profileSourceState === 'ready' ? options.profile ?? null : null;
|
|
152
|
+
const officialDomains = [...new Set((profileSourceState === 'ready' ? options.officialDomains ?? [] : [])
|
|
123
153
|
.map((domain) => domain.trim().toLowerCase().replace(/\.$/u, ''))
|
|
124
154
|
.filter(Boolean))].slice(0, 20);
|
|
155
|
+
let profileContextUnevaluatedCount = 0;
|
|
125
156
|
const rows = rowsRaw
|
|
126
|
-
.map(normalizeBulkSessionResult)
|
|
157
|
+
.map((row) => normalizeBulkSessionResult(row))
|
|
127
158
|
.filter((row) => Boolean(row))
|
|
128
159
|
.filter((row) => row.trusted === null)
|
|
129
160
|
.slice(0, MAX_BULK_MAIL_EXPOSURE_ROWS)
|
|
130
161
|
.map((item) => {
|
|
131
162
|
const state = exposureState(item);
|
|
163
|
+
const profileContextComparable = profileSourceState === 'ready'
|
|
164
|
+
&& currentProfileContext.sourceState === 'ready'
|
|
165
|
+
&& item.profileContext.sourceState === 'ready'
|
|
166
|
+
&& item.profileContext.activeProfileId === currentProfileContext.activeProfileId
|
|
167
|
+
&& item.profileContext.profileUpdatedAt === currentProfileContext.profileUpdatedAt;
|
|
168
|
+
const profileContextLimitation = profileContextComparable
|
|
169
|
+
? item.profileContext.limitation
|
|
170
|
+
: item.profileContext.limitation || BULK_PROFILE_CONTEXT_MISMATCH_LIMITATION;
|
|
171
|
+
if (!profileContextComparable)
|
|
172
|
+
profileContextUnevaluatedCount += 1;
|
|
132
173
|
return {
|
|
133
174
|
domain: item.domain,
|
|
134
175
|
state,
|
|
135
176
|
label: STATE_LABELS[state],
|
|
136
177
|
detail: stateDetail(state),
|
|
137
|
-
...relation(state,
|
|
178
|
+
...relation(state, profile, profileSourceState, profileContextComparable, profileContextLimitation),
|
|
138
179
|
mutationTypes: item.mutationTypes.slice(0, 40),
|
|
139
180
|
registration: item.availability,
|
|
140
181
|
sourceCoverage: item.sourceCoverage.slice(0, 12),
|
|
141
|
-
|
|
182
|
+
profileContextState: item.profileContext.sourceState,
|
|
183
|
+
profileContextLimitation,
|
|
184
|
+
limitations: rowLimitations(item, state, profileSourceState, profileContextComparable, profileContextLimitation),
|
|
142
185
|
};
|
|
143
186
|
});
|
|
144
187
|
const counts = {
|
|
@@ -156,16 +199,22 @@ export function buildBulkMailExposureReport(rowsRaw, options = {}) {
|
|
|
156
199
|
generatedAt,
|
|
157
200
|
observedAt: timestamp(options.observedAt),
|
|
158
201
|
baseline: {
|
|
159
|
-
profile
|
|
202
|
+
profile,
|
|
160
203
|
officialDomains,
|
|
161
|
-
label: baselineLabel(
|
|
204
|
+
label: baselineLabel(profile, profileSourceState),
|
|
162
205
|
limitations: [
|
|
206
|
+
...(profileSourceState === 'loading'
|
|
207
|
+
? ['Brand Profile context was loading, so profile-derived mail posture is inconclusive.']
|
|
208
|
+
: profileSourceState === 'unavailable'
|
|
209
|
+
? ['Brand Profile context was unavailable, so profile-derived mail posture is inconclusive.']
|
|
210
|
+
: []),
|
|
163
211
|
'The Brand Profile describes an analyst-configured official-domain posture; it is not a live observation.',
|
|
164
212
|
'Candidate similarity and mail configuration do not establish use, control, intent, safety, or maliciousness.',
|
|
165
213
|
],
|
|
166
214
|
},
|
|
167
215
|
rows,
|
|
168
216
|
counts,
|
|
217
|
+
profileContextUnevaluatedCount,
|
|
169
218
|
limitations: [
|
|
170
219
|
'This review uses compact Bulk evidence already collected and makes no additional request.',
|
|
171
220
|
'Null MX, no explicit MX, receiving mail, authentication gaps, and incomplete evidence remain separate states.',
|
|
@@ -176,13 +225,13 @@ export function buildBulkMailExposureReport(rowsRaw, options = {}) {
|
|
|
176
225
|
export async function buildBulkMailExposureExport(report) {
|
|
177
226
|
const unsigned = {
|
|
178
227
|
schema: BULK_MAIL_EXPOSURE_SCHEMA,
|
|
179
|
-
version:
|
|
228
|
+
version: BULK_MAIL_EXPOSURE_EXPORT_VERSION,
|
|
180
229
|
report,
|
|
181
230
|
};
|
|
182
|
-
const digestSha256 = await
|
|
231
|
+
const digestSha256 = await sha256ArtifactDigestV2(unsigned);
|
|
183
232
|
const document = {
|
|
184
233
|
...unsigned,
|
|
185
|
-
integrity: { algorithm: 'SHA-256', digestSha256 },
|
|
234
|
+
integrity: { algorithm: 'SHA-256', canonicalization: SORTED_JSON_V2, digestSha256 },
|
|
186
235
|
};
|
|
187
236
|
return {
|
|
188
237
|
document,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { normalizeBulkSessionResult, } from "
|
|
2
|
-
import { BULK_REVIEW_STATES, } from "
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
export
|
|
1
|
+
import { normalizeBulkSessionResult, } from "../workspace/bulk-session-model.mjs";
|
|
2
|
+
import { BULK_REVIEW_STATES, } from "../workspace/bulk-review-model.mjs";
|
|
3
|
+
import { SORTED_JSON_V2, sha256ArtifactDigestV2 } from "../evidence/artifact-integrity.mjs";
|
|
4
|
+
import { BULK_REVIEW_MANIFEST_SCHEMA, BULK_REVIEW_MANIFEST_VERSION, } from "../contracts/investigation-portability.mjs";
|
|
5
|
+
export { BULK_REVIEW_MANIFEST_SCHEMA, BULK_REVIEW_MANIFEST_VERSION };
|
|
6
6
|
const MAX_MANIFEST_ROWS = 2_000;
|
|
7
7
|
const REVIEW_STATE_SET = new Set(BULK_REVIEW_STATES);
|
|
8
8
|
function timestamp(value, fallback) {
|
|
@@ -21,7 +21,7 @@ export async function buildBulkReviewManifest(input) {
|
|
|
21
21
|
const observedAt = timestamp(input.observedAt, generatedAt);
|
|
22
22
|
const states = reviewStateMap(input.reviewStates);
|
|
23
23
|
const rows = input.rows
|
|
24
|
-
.map(normalizeBulkSessionResult)
|
|
24
|
+
.map((row) => normalizeBulkSessionResult(row))
|
|
25
25
|
.filter((item) => Boolean(item))
|
|
26
26
|
.slice(0, MAX_MANIFEST_ROWS)
|
|
27
27
|
.map((item) => ({
|
|
@@ -30,6 +30,7 @@ export async function buildBulkReviewManifest(input) {
|
|
|
30
30
|
resultState: item.status,
|
|
31
31
|
scanDepth: item.scanDepth,
|
|
32
32
|
sourceCoverage: item.sourceCoverage,
|
|
33
|
+
profileContext: { ...item.profileContext },
|
|
33
34
|
}));
|
|
34
35
|
const unsigned = {
|
|
35
36
|
schema: BULK_REVIEW_MANIFEST_SCHEMA,
|
|
@@ -45,14 +46,14 @@ export async function buildBulkReviewManifest(input) {
|
|
|
45
46
|
rows,
|
|
46
47
|
limitations: [
|
|
47
48
|
'This manifest records the explicit review selection and view context for a separate CSV export.',
|
|
48
|
-
'It contains compact source states only and excludes raw payloads, contact records, notes, and transient request state.',
|
|
49
|
+
'It contains compact source states and bounded row-level Brand Profile provenance only, and excludes raw payloads, Profile contents, contact records, notes, and transient request state.',
|
|
49
50
|
'Reproducing the filters does not reproduce upstream responses or imply that evidence remains current.',
|
|
50
51
|
],
|
|
51
52
|
};
|
|
52
|
-
const digestSha256 = await
|
|
53
|
+
const digestSha256 = await sha256ArtifactDigestV2(unsigned);
|
|
53
54
|
const document = {
|
|
54
55
|
...unsigned,
|
|
55
|
-
integrity: { algorithm: 'SHA-256', digestSha256 },
|
|
56
|
+
integrity: { algorithm: 'SHA-256', canonicalization: SORTED_JSON_V2, digestSha256 },
|
|
56
57
|
};
|
|
57
58
|
return {
|
|
58
59
|
document,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const BULK_REVIEW_STALE_AFTER_DAYS = 7;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { canonicalArtifactJsonV2, sha256ArtifactDigestV2, SORTED_JSON_V2, } from "../evidence/artifact-integrity.mjs";
|
|
2
|
+
import { LOOKUP_ASSET_GRAPH_SCHEMA, } from "./lookup-asset-graph.mjs";
|
|
3
|
+
import { LOOKUP_INVESTIGATION_BRIEF_SCHEMA, LOOKUP_INVESTIGATION_BRIEF_VERSION, } from "./lookup-investigation-brief.mjs";
|
|
4
|
+
import { INVESTIGATION_CAPSULE_ANALYST_RECORDS_SCHEMA, INVESTIGATION_CAPSULE_ANALYST_RECORDS_VERSION, INVESTIGATION_CAPSULE_SCHEMA, INVESTIGATION_CAPSULE_VERSION, PUBLIC_INVESTIGATION_CAPSULE_VERSION, SUPPORTED_INVESTIGATION_CAPSULE_VERSIONS, } from "../contracts/investigation-portability.mjs";
|
|
5
|
+
export { INVESTIGATION_CAPSULE_ANALYST_RECORDS_SCHEMA, INVESTIGATION_CAPSULE_ANALYST_RECORDS_VERSION, INVESTIGATION_CAPSULE_SCHEMA, INVESTIGATION_CAPSULE_VERSION, PUBLIC_INVESTIGATION_CAPSULE_VERSION, SUPPORTED_INVESTIGATION_CAPSULE_VERSIONS, };
|
|
4
6
|
function safeVersion(value) {
|
|
5
7
|
return typeof value === 'string' && /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.test(value)
|
|
6
8
|
? value
|
|
@@ -46,14 +48,14 @@ function analystProjection(record) {
|
|
|
46
48
|
export async function buildInvestigationCapsule(input) {
|
|
47
49
|
const analystRecords = input.includeAnalystRecords ? analystProjection(input.caseRecord) : null;
|
|
48
50
|
const [evidenceDigest, briefDigest, graphDigest, analystRecordsDigest] = await Promise.all([
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
analystRecords ?
|
|
51
|
+
sha256ArtifactDigestV2(input.lookupEvidence),
|
|
52
|
+
sha256ArtifactDigestV2(input.brief),
|
|
53
|
+
sha256ArtifactDigestV2(input.graph),
|
|
54
|
+
analystRecords ? sha256ArtifactDigestV2(analystRecords) : Promise.resolve(null),
|
|
53
55
|
]);
|
|
54
56
|
const evidenceSchema = safeSchema(input.lookupEvidence.schema);
|
|
55
57
|
const evidenceVersion = safeSchemaVersion(input.lookupEvidence.schemaVersion);
|
|
56
|
-
|
|
58
|
+
const unsigned = {
|
|
57
59
|
schema: INVESTIGATION_CAPSULE_SCHEMA,
|
|
58
60
|
schemaVersion: INVESTIGATION_CAPSULE_VERSION,
|
|
59
61
|
generatedAt: iso(input.generatedAt),
|
|
@@ -61,31 +63,56 @@ export async function buildInvestigationCapsule(input) {
|
|
|
61
63
|
target: { value: input.brief.target, type: input.brief.targetType },
|
|
62
64
|
sourceContracts: [
|
|
63
65
|
{ id: 'lookup-evidence', schema: evidenceSchema, version: evidenceVersion, digest: evidenceDigest, embedded: false },
|
|
64
|
-
{ id: 'investigation-brief', schema:
|
|
65
|
-
{ id: 'asset-graph', schema:
|
|
66
|
+
{ id: 'investigation-brief', schema: LOOKUP_INVESTIGATION_BRIEF_SCHEMA, version: LOOKUP_INVESTIGATION_BRIEF_VERSION, digest: briefDigest, embedded: true },
|
|
67
|
+
{ id: 'asset-graph', schema: LOOKUP_ASSET_GRAPH_SCHEMA, version: input.graph.version, digest: graphDigest, embedded: true },
|
|
66
68
|
...(analystRecords && analystRecordsDigest
|
|
67
|
-
? [{
|
|
69
|
+
? [{
|
|
70
|
+
id: 'analyst-records',
|
|
71
|
+
schema: INVESTIGATION_CAPSULE_ANALYST_RECORDS_SCHEMA,
|
|
72
|
+
version: INVESTIGATION_CAPSULE_ANALYST_RECORDS_VERSION,
|
|
73
|
+
digest: analystRecordsDigest,
|
|
74
|
+
embedded: true,
|
|
75
|
+
}]
|
|
68
76
|
: []),
|
|
69
77
|
],
|
|
70
78
|
investigationBrief: input.brief,
|
|
71
79
|
graphSnapshot: input.graph,
|
|
72
80
|
analystRecords,
|
|
73
|
-
integrity: { algorithm: 'SHA-256', briefDigest, graphDigest, analystRecordsDigest },
|
|
74
81
|
limitations: [
|
|
75
82
|
'The Lookup evidence file is linked by digest but is not embedded; keep or share that exact file separately when verification is required.',
|
|
76
83
|
'Digest verification can detect changed content but does not establish who created the capsule. No digital signature is applied.',
|
|
77
|
-
'The graph and brief are bounded projections of collected and derived evidence, not attribution, ownership, safety, availability, or maliciousness conclusions.',
|
|
84
|
+
'The graph and canonical Decision Fact brief are bounded projections of collected and derived evidence, not attribution, ownership, safety, availability, or maliciousness conclusions.',
|
|
78
85
|
analystRecords
|
|
79
86
|
? 'Analyst decisions and assertions were deliberately included; review them for sensitive or personal information before sharing.'
|
|
80
87
|
: 'Analyst decisions, assertions, notes, contacts, actions, and raw source payloads are excluded.',
|
|
81
88
|
],
|
|
82
89
|
};
|
|
90
|
+
return {
|
|
91
|
+
...unsigned,
|
|
92
|
+
integrity: {
|
|
93
|
+
algorithm: 'SHA-256',
|
|
94
|
+
canonicalization: SORTED_JSON_V2,
|
|
95
|
+
scope: 'capsule excluding integrity',
|
|
96
|
+
briefDigest,
|
|
97
|
+
graphDigest,
|
|
98
|
+
analystRecordsDigest,
|
|
99
|
+
digestSha256: await sha256ArtifactDigestV2(unsigned),
|
|
100
|
+
},
|
|
101
|
+
};
|
|
83
102
|
}
|
|
84
103
|
export async function verifyInvestigationCapsule(capsule) {
|
|
104
|
+
const current = capsule.schemaVersion === INVESTIGATION_CAPSULE_VERSION;
|
|
105
|
+
const publicVersion = capsule.schemaVersion === PUBLIC_INVESTIGATION_CAPSULE_VERSION;
|
|
106
|
+
const integrityRecord = capsule.integrity;
|
|
107
|
+
if ((!current && !publicVersion)
|
|
108
|
+
|| integrityRecord.canonicalization !== SORTED_JSON_V2
|
|
109
|
+
|| integrityRecord.scope !== 'capsule excluding integrity') {
|
|
110
|
+
return { valid: false, brief: false, graph: false, analystRecords: null, whole: false };
|
|
111
|
+
}
|
|
85
112
|
const [briefDigest, graphDigest, analystRecordsDigest] = await Promise.all([
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
capsule.analystRecords ?
|
|
113
|
+
sha256ArtifactDigestV2(capsule.investigationBrief),
|
|
114
|
+
sha256ArtifactDigestV2(capsule.graphSnapshot),
|
|
115
|
+
capsule.analystRecords ? sha256ArtifactDigestV2(capsule.analystRecords) : Promise.resolve(null),
|
|
89
116
|
]);
|
|
90
117
|
const brief = briefDigest === capsule.integrity.briefDigest;
|
|
91
118
|
const graph = graphDigest === capsule.integrity.graphDigest;
|
|
@@ -94,12 +121,17 @@ export async function verifyInvestigationCapsule(capsule) {
|
|
|
94
121
|
: capsule.integrity.analystRecordsDigest === null
|
|
95
122
|
? null
|
|
96
123
|
: false;
|
|
97
|
-
|
|
124
|
+
const { integrity: _integrity, ...unsigned } = capsule;
|
|
125
|
+
const whole = integrityRecord.algorithm === 'SHA-256'
|
|
126
|
+
&& integrityRecord.canonicalization === SORTED_JSON_V2
|
|
127
|
+
&& integrityRecord.scope === 'capsule excluding integrity'
|
|
128
|
+
&& integrityRecord.digestSha256 === await sha256ArtifactDigestV2(unsigned);
|
|
129
|
+
return { valid: brief && graph && analystRecords !== false && whole, brief, graph, analystRecords, whole };
|
|
98
130
|
}
|
|
99
131
|
export function investigationCapsuleFilename(capsule) {
|
|
100
132
|
const target = capsule.target.value.toLowerCase().replace(/[^a-z0-9.-]+/gu, '-').replace(/^-|-$/gu, '').slice(0, 80) || 'lookup';
|
|
101
133
|
return `whoisleuth-investigation-capsule-${target}-${capsule.generatedAt.slice(0, 10)}.json`;
|
|
102
134
|
}
|
|
103
135
|
export function serializeInvestigationCapsule(capsule) {
|
|
104
|
-
return `${
|
|
136
|
+
return `${canonicalArtifactJsonV2(capsule)}\n`;
|
|
105
137
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { LOOKUP_ASSET_GRAPH_SCHEMA, LOOKUP_ASSET_GRAPH_VERSION, } from "../contracts/investigation-portability.mjs";
|
|
2
|
+
export { LOOKUP_ASSET_GRAPH_SCHEMA, LOOKUP_ASSET_GRAPH_VERSION };
|
|
1
3
|
const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/gu;
|
|
2
4
|
const MAX_NODES = 72;
|
|
3
5
|
const MAX_EDGES = 120;
|
|
@@ -158,7 +160,7 @@ export function buildLookupAssetGraph(input) {
|
|
|
158
160
|
const target = hostname(input.target);
|
|
159
161
|
if (!target) {
|
|
160
162
|
return {
|
|
161
|
-
version:
|
|
163
|
+
version: LOOKUP_ASSET_GRAPH_VERSION,
|
|
162
164
|
targetId: '',
|
|
163
165
|
nodes: [],
|
|
164
166
|
edges: [],
|
|
@@ -705,7 +707,7 @@ export function buildLookupAssetGraph(input) {
|
|
|
705
707
|
}
|
|
706
708
|
}
|
|
707
709
|
return {
|
|
708
|
-
version:
|
|
710
|
+
version: LOOKUP_ASSET_GRAPH_VERSION,
|
|
709
711
|
targetId,
|
|
710
712
|
nodes: [...nodes.values()],
|
|
711
713
|
edges: [...edges.values()],
|