securl 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +241 -0
  2. package/LICENSE +21 -0
  3. package/README.md +427 -0
  4. package/RELEASING.md +37 -0
  5. package/SECURITY.md +27 -0
  6. package/dist/certificate.d.ts +5 -0
  7. package/dist/certificate.js +92 -0
  8. package/dist/cli.d.ts +1 -0
  9. package/dist/cli.js +674 -0
  10. package/dist/compromiseSignals.d.ts +10 -0
  11. package/dist/compromiseSignals.js +183 -0
  12. package/dist/cookie-analysis.d.ts +2 -0
  13. package/dist/cookie-analysis.js +41 -0
  14. package/dist/cookieAnalysis.d.ts +2 -0
  15. package/dist/cookieAnalysis.js +82 -0
  16. package/dist/ctDiscovery.d.ts +19 -0
  17. package/dist/ctDiscovery.js +357 -0
  18. package/dist/domain-security.d.ts +10 -0
  19. package/dist/domain-security.js +416 -0
  20. package/dist/header-analysis.d.ts +14 -0
  21. package/dist/header-analysis.js +165 -0
  22. package/dist/historyDiff.d.ts +4 -0
  23. package/dist/historyDiff.js +117 -0
  24. package/dist/html-extraction.d.ts +12 -0
  25. package/dist/html-extraction.js +279 -0
  26. package/dist/html-page-analysis.d.ts +38 -0
  27. package/dist/html-page-analysis.js +459 -0
  28. package/dist/htmlInsights.d.ts +23 -0
  29. package/dist/htmlInsights.js +460 -0
  30. package/dist/identityProvider.d.ts +14 -0
  31. package/dist/identityProvider.js +259 -0
  32. package/dist/index.d.ts +17 -0
  33. package/dist/index.js +1008 -0
  34. package/dist/infrastructure.d.ts +9 -0
  35. package/dist/infrastructure.js +149 -0
  36. package/dist/libraryRisk.d.ts +3 -0
  37. package/dist/libraryRisk.js +164 -0
  38. package/dist/network-validation.d.ts +30 -0
  39. package/dist/network-validation.js +161 -0
  40. package/dist/network.d.ts +34 -0
  41. package/dist/network.js +139 -0
  42. package/dist/passive-intelligence.d.ts +21 -0
  43. package/dist/passive-intelligence.js +247 -0
  44. package/dist/path-discovery.d.ts +4 -0
  45. package/dist/path-discovery.js +50 -0
  46. package/dist/postureDigest.d.ts +142 -0
  47. package/dist/postureDigest.js +159 -0
  48. package/dist/postureDrift.d.ts +4 -0
  49. package/dist/postureDrift.js +118 -0
  50. package/dist/postureRemediation.d.ts +6 -0
  51. package/dist/postureRemediation.js +286 -0
  52. package/dist/redirectChain.d.ts +2 -0
  53. package/dist/redirectChain.js +39 -0
  54. package/dist/riskEvents.d.ts +3 -0
  55. package/dist/riskEvents.js +187 -0
  56. package/dist/scannerConfig.d.ts +49 -0
  57. package/dist/scannerConfig.js +79 -0
  58. package/dist/scoring.d.ts +32 -0
  59. package/dist/scoring.js +367 -0
  60. package/dist/security-txt.d.ts +4 -0
  61. package/dist/security-txt.js +123 -0
  62. package/dist/surfaceEnrichment.d.ts +44 -0
  63. package/dist/surfaceEnrichment.js +377 -0
  64. package/dist/technology-detection.d.ts +4 -0
  65. package/dist/technology-detection.js +93 -0
  66. package/dist/types.d.ts +730 -0
  67. package/dist/types.js +1 -0
  68. package/dist/utils.d.ts +7 -0
  69. package/dist/utils.js +66 -0
  70. package/dist/wafFingerprint.d.ts +5 -0
  71. package/dist/wafFingerprint.js +156 -0
  72. package/examples/risk-events.mjs +27 -0
  73. package/examples/scan-url.mjs +17 -0
  74. package/package.json +102 -0
@@ -0,0 +1,139 @@
1
+ import http from "node:http";
2
+ import https from "node:https";
3
+ import { OBSERVATIONAL_TLS_OPTIONS } from "./certificate.js";
4
+ import { REDIRECT_LIMIT, REQUEST_TIMEOUT_MS, TEXT_BODY_LIMIT } from "./scannerConfig.js";
5
+ import { headerValue } from "./utils.js";
6
+ import { assertPublicRequestTarget, createPinnedLookup } from "./network-validation.js";
7
+ export const SCANNER_USER_AGENT = "ExternalPostureInsight/1.0";
8
+ export function requestOnce(targetUrl, method = "HEAD", options = {}) {
9
+ return requestWithHeaders(targetUrl, method, {}, options);
10
+ }
11
+ export async function requestWithHeaders(targetUrl, method = "HEAD", extraHeaders = {}, options = {}) {
12
+ const validatedAddresses = await assertPublicRequestTarget(targetUrl);
13
+ const isHttps = targetUrl.protocol === "https:";
14
+ const transport = isHttps ? https : http;
15
+ const startedAt = Date.now();
16
+ const timeoutMs = options.timeoutMs ?? REQUEST_TIMEOUT_MS;
17
+ return new Promise((resolve, reject) => {
18
+ const request = transport.request(targetUrl, {
19
+ method,
20
+ ...OBSERVATIONAL_TLS_OPTIONS,
21
+ lookup: createPinnedLookup(validatedAddresses),
22
+ headers: {
23
+ "User-Agent": SCANNER_USER_AGENT,
24
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
25
+ "Accept-Encoding": "identity",
26
+ ...extraHeaders,
27
+ },
28
+ }, (response) => {
29
+ response.resume();
30
+ resolve({
31
+ statusCode: response.statusCode || 0,
32
+ headers: response.headers,
33
+ elapsedMs: Date.now() - startedAt,
34
+ });
35
+ });
36
+ request.on("error", reject);
37
+ request.setTimeout(timeoutMs, () => {
38
+ request.destroy(new Error("Request timed out."));
39
+ });
40
+ request.end();
41
+ });
42
+ }
43
+ export async function requestText(targetUrl, extraHeaders = {}, options = {}) {
44
+ const validatedAddresses = await assertPublicRequestTarget(targetUrl);
45
+ const isHttps = targetUrl.protocol === "https:";
46
+ const transport = isHttps ? https : http;
47
+ const timeoutMs = options.timeoutMs ?? REQUEST_TIMEOUT_MS;
48
+ return new Promise((resolve, reject) => {
49
+ const request = transport.request(targetUrl, {
50
+ method: "GET",
51
+ ...OBSERVATIONAL_TLS_OPTIONS,
52
+ lookup: createPinnedLookup(validatedAddresses),
53
+ headers: {
54
+ "User-Agent": SCANNER_USER_AGENT,
55
+ Accept: "text/plain,text/*;q=0.9,*/*;q=0.1",
56
+ "Accept-Encoding": "identity",
57
+ ...extraHeaders,
58
+ },
59
+ }, (response) => {
60
+ let body = "";
61
+ response.setEncoding("utf8");
62
+ response.on("data", (chunk) => {
63
+ body += chunk;
64
+ if (body.length > TEXT_BODY_LIMIT) {
65
+ body = body.slice(0, TEXT_BODY_LIMIT);
66
+ }
67
+ });
68
+ response.on("end", () => {
69
+ resolve({
70
+ statusCode: response.statusCode || 0,
71
+ headers: response.headers,
72
+ body,
73
+ });
74
+ });
75
+ });
76
+ request.on("error", reject);
77
+ request.setTimeout(timeoutMs, () => {
78
+ request.destroy(new Error("Request timed out."));
79
+ });
80
+ request.end();
81
+ });
82
+ }
83
+ function tryParseJson(body) {
84
+ if (!body)
85
+ return null;
86
+ try {
87
+ return JSON.parse(body);
88
+ }
89
+ catch {
90
+ return null;
91
+ }
92
+ }
93
+ export async function requestJson(targetUrl, extraHeaders = {}, options = {}) {
94
+ const response = await requestText(targetUrl, {
95
+ Accept: "application/json,text/plain;q=0.9,*/*;q=0.1",
96
+ ...extraHeaders,
97
+ }, options);
98
+ return {
99
+ ...response,
100
+ json: tryParseJson(response.body),
101
+ };
102
+ }
103
+ export async function fetchWithRedirects(initialUrl, redirectLimit = REDIRECT_LIMIT, options = {}) {
104
+ const redirects = [];
105
+ let currentUrl = initialUrl;
106
+ let response = await requestOnce(currentUrl, "HEAD", options);
107
+ if (response.statusCode === 405 || response.statusCode === 403) {
108
+ response = await requestOnce(currentUrl, "GET", options);
109
+ }
110
+ while ([301, 302, 303, 307, 308].includes(response.statusCode) &&
111
+ headerValue(response.headers, "location") &&
112
+ redirects.length < redirectLimit) {
113
+ const location = headerValue(response.headers, "location");
114
+ redirects.push({
115
+ url: currentUrl.toString(),
116
+ status: response.statusCode,
117
+ statusCode: response.statusCode,
118
+ location,
119
+ isHttps: currentUrl.protocol === "https:",
120
+ secure: currentUrl.protocol === "https:",
121
+ });
122
+ currentUrl = new URL(location, currentUrl);
123
+ // Each hop is validated and IP-pinned inside requestOnce -> assertPublicRequestTarget,
124
+ // so a redirect cannot be re-pointed at a private address between check and connect.
125
+ response = await requestOnce(currentUrl, "HEAD", options);
126
+ if (response.statusCode === 405 || response.statusCode === 403) {
127
+ response = await requestOnce(currentUrl, "GET", options);
128
+ }
129
+ }
130
+ redirects.push({
131
+ url: currentUrl.toString(),
132
+ status: response.statusCode,
133
+ statusCode: response.statusCode,
134
+ location: null,
135
+ isHttps: currentUrl.protocol === "https:",
136
+ secure: currentUrl.protocol === "https:",
137
+ });
138
+ return { finalUrl: currentUrl, redirects, response };
139
+ }
@@ -0,0 +1,21 @@
1
+ import type { AiSurfaceInfo, ApiSurfaceInfo, DomainSecurityInfo, HtmlSecurityInfo, IdentityProviderInfo, InfrastructureInfo, PassiveIntelligenceInfo, PublicSignalsInfo, SecurityTxtInfo, TechnologyResult, ThirdPartyTrustInfo, WafFingerprintInfo } from "./types.js";
2
+ interface PassiveIntelligenceInput {
3
+ technologies: TechnologyResult[];
4
+ infrastructure: InfrastructureInfo;
5
+ thirdPartyTrust: ThirdPartyTrustInfo;
6
+ htmlSecurity: HtmlSecurityInfo;
7
+ aiSurface: AiSurfaceInfo;
8
+ domainSecurity: DomainSecurityInfo;
9
+ securityTxt: SecurityTxtInfo;
10
+ publicSignals: PublicSignalsInfo;
11
+ identityProvider: IdentityProviderInfo;
12
+ wafFingerprint: WafFingerprintInfo;
13
+ apiSurface: ApiSurfaceInfo;
14
+ assessmentLimitation?: {
15
+ limited: boolean;
16
+ title: string | null;
17
+ } | null;
18
+ }
19
+ export declare function buildPassiveIntelligence(input: PassiveIntelligenceInput): PassiveIntelligenceInfo;
20
+ export declare function emptyPassiveIntelligence(reason?: string): PassiveIntelligenceInfo;
21
+ export {};
@@ -0,0 +1,247 @@
1
+ import { unique } from "./utils.js";
2
+ const SOURCE_BOUNDARY = "Passive read only: normal HTTP/TLS responses, public DNS records, public trust records, and visible page assets. No port scanning, brute forcing, login probing, exploit payloads, or bypass attempts are used.";
3
+ const titleCase = (value) => value
4
+ .split(/[_\s-]+/)
5
+ .filter(Boolean)
6
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
7
+ .join(" ");
8
+ const describeList = (items, fallback, limit = 3) => {
9
+ const values = unique(items.filter(Boolean));
10
+ if (!values.length)
11
+ return fallback;
12
+ if (values.length <= limit)
13
+ return values.join(", ");
14
+ return `${values.slice(0, limit).join(", ")} +${values.length - limit} more`;
15
+ };
16
+ const categoryRank = {
17
+ attention: 0,
18
+ watch: 1,
19
+ neutral: 2,
20
+ positive: 3,
21
+ };
22
+ const addSignal = (signals, signal) => {
23
+ const key = `${signal.category}:${signal.title}:${signal.evidence.join("|")}`;
24
+ if (signals.some((existing) => `${existing.category}:${existing.title}:${existing.evidence.join("|")}` === key)) {
25
+ return;
26
+ }
27
+ signals.push(signal);
28
+ };
29
+ const confidenceFromTechnologies = (technologies) => {
30
+ if (technologies.some((technology) => technology.confidence === "high"))
31
+ return "high";
32
+ if (technologies.some((technology) => technology.confidence === "medium"))
33
+ return "medium";
34
+ return "low";
35
+ };
36
+ const visibleProviderNames = (providers, categories) => unique(providers.filter((provider) => categories.includes(provider.category)).map((provider) => provider.name));
37
+ export function buildPassiveIntelligence(input) {
38
+ const signals = [];
39
+ const technologiesByCategory = (category) => input.technologies.filter((technology) => technology.category === category);
40
+ const networkStack = technologiesByCategory("network");
41
+ const hostingStack = technologiesByCategory("hosting");
42
+ const frontendStack = technologiesByCategory("frontend");
43
+ const serverStack = technologiesByCategory("server");
44
+ const securityStack = technologiesByCategory("security");
45
+ if (input.assessmentLimitation?.limited) {
46
+ addSignal(signals, {
47
+ category: "exposure",
48
+ title: "Limited external read",
49
+ summary: input.assessmentLimitation.title || "The target did not return a normal page response.",
50
+ confidence: "high",
51
+ source: "derived",
52
+ risk: "watch",
53
+ evidence: [input.assessmentLimitation.title || "Limited assessment"],
54
+ action: "Treat technology and missing-control observations as partial until a normal trusted response can be read.",
55
+ });
56
+ }
57
+ if (networkStack.length || hostingStack.length || serverStack.length || frontendStack.length) {
58
+ const stackNames = [
59
+ ...networkStack,
60
+ ...hostingStack,
61
+ ...serverStack,
62
+ ...frontendStack,
63
+ ].map((technology) => technology.version ? `${technology.name} ${technology.version}` : technology.name);
64
+ addSignal(signals, {
65
+ category: "technology",
66
+ title: "Visible technology stack",
67
+ summary: `Public response and asset evidence suggests ${describeList(stackNames, "no obvious stack markers")}.`,
68
+ confidence: confidenceFromTechnologies([...networkStack, ...hostingStack, ...serverStack, ...frontendStack]),
69
+ source: "html",
70
+ risk: "neutral",
71
+ evidence: stackNames.slice(0, 6),
72
+ action: "Use this as attribution context only; review exposed framework or server versions where exact versions are visible.",
73
+ });
74
+ }
75
+ if (securityStack.length || input.wafFingerprint.detected || input.infrastructure.providers.length) {
76
+ const providers = unique([
77
+ ...input.infrastructure.providers.map((provider) => provider.provider),
78
+ ...input.wafFingerprint.providers.map((provider) => provider.name),
79
+ ...securityStack.map((technology) => technology.name),
80
+ ]);
81
+ addSignal(signals, {
82
+ category: "infrastructure",
83
+ title: "Edge and protection signals",
84
+ summary: providers.length
85
+ ? `Passive evidence points to ${describeList(providers, "no obvious edge provider")}.`
86
+ : "No obvious edge provider was inferred.",
87
+ confidence: input.wafFingerprint.providers.some((provider) => provider.confidence === "high") ? "high" : "medium",
88
+ source: "headers",
89
+ risk: providers.length ? "positive" : "neutral",
90
+ evidence: providers.slice(0, 6),
91
+ action: providers.length
92
+ ? "Confirm security headers are applied consistently behind the edge layer, not only on the landing page."
93
+ : null,
94
+ });
95
+ }
96
+ const analyticsProviders = visibleProviderNames(input.thirdPartyTrust.providers, ["analytics", "ads"]);
97
+ const replayProviders = visibleProviderNames(input.thirdPartyTrust.providers, ["session_replay"]);
98
+ const consentProviders = visibleProviderNames(input.thirdPartyTrust.providers, ["consent"]);
99
+ const supportProviders = visibleProviderNames(input.thirdPartyTrust.providers, ["support"]);
100
+ const telemetryProviders = unique([...analyticsProviders, ...replayProviders, ...consentProviders, ...supportProviders]);
101
+ if (telemetryProviders.length) {
102
+ addSignal(signals, {
103
+ category: "telemetry",
104
+ title: "Visible telemetry and customer-experience tooling",
105
+ summary: `The page publicly loads ${describeList(telemetryProviders, "visible telemetry tooling")}.`,
106
+ confidence: "high",
107
+ source: "asset",
108
+ risk: replayProviders.length || analyticsProviders.length > 2 ? "watch" : "neutral",
109
+ evidence: telemetryProviders.slice(0, 8),
110
+ action: replayProviders.length
111
+ ? "Check consent, masking, and retention settings for any session replay or behavioural analytics tooling."
112
+ : "Confirm analytics and consent tooling are intentionally exposed and covered by privacy notices.",
113
+ });
114
+ }
115
+ if (input.thirdPartyTrust.providers.length) {
116
+ const providerDomains = input.thirdPartyTrust.providers.map((provider) => provider.domain);
117
+ addSignal(signals, {
118
+ category: "third_party",
119
+ title: "Third-party dependency surface",
120
+ summary: `${input.thirdPartyTrust.totalProviders} third-party provider${input.thirdPartyTrust.totalProviders === 1 ? "" : "s"} were visible from the fetched page.`,
121
+ confidence: "high",
122
+ source: "asset",
123
+ risk: input.thirdPartyTrust.highRiskProviders > 0 ? "watch" : "neutral",
124
+ evidence: providerDomains.slice(0, 8),
125
+ action: "Review whether critical security, analytics, payments, and support dependencies are documented and monitored.",
126
+ });
127
+ }
128
+ if (input.aiSurface.detected || input.aiSurface.vendors.length) {
129
+ addSignal(signals, {
130
+ category: "ai",
131
+ title: "AI or support automation surface",
132
+ summary: input.aiSurface.vendors.length
133
+ ? `Visible page signals suggest ${describeList(input.aiSurface.vendors.map((vendor) => vendor.name), "AI or automation tooling")}.`
134
+ : "AI or automation language was visible on the page.",
135
+ confidence: input.aiSurface.vendors.some((vendor) => vendor.confidence === "high") ? "high" : "medium",
136
+ source: "html",
137
+ risk: input.aiSurface.issues.length ? "watch" : "neutral",
138
+ evidence: [
139
+ ...input.aiSurface.vendors.map((vendor) => `${vendor.name}: ${vendor.evidence}`),
140
+ ...input.aiSurface.disclosures,
141
+ ].slice(0, 6),
142
+ action: input.aiSurface.issues.length
143
+ ? "Check disclosure, privacy, and data-handling language for public AI or automation features."
144
+ : "Keep AI and support automation disclosures aligned with privacy and customer-support practices.",
145
+ });
146
+ }
147
+ const emailEvidence = unique([
148
+ input.domainSecurity.emailPolicy.spf.status !== "missing"
149
+ ? `SPF ${input.domainSecurity.emailPolicy.spf.status}`
150
+ : "SPF missing",
151
+ input.domainSecurity.emailPolicy.dmarc.status !== "missing"
152
+ ? `DMARC ${input.domainSecurity.emailPolicy.dmarc.status}`
153
+ : "DMARC missing",
154
+ input.domainSecurity.mtaSts.dns ? "MTA-STS present" : "MTA-STS missing",
155
+ input.domainSecurity.caaRecords.length ? "CAA present" : "CAA missing",
156
+ input.domainSecurity.dnssec.enabled ? "DNSSEC signed" : "DNSSEC not signed",
157
+ ]);
158
+ addSignal(signals, {
159
+ category: "email",
160
+ title: "Domain and email trust posture",
161
+ summary: input.domainSecurity.issues.length
162
+ ? `${input.domainSecurity.issues.length} domain or email trust signal${input.domainSecurity.issues.length === 1 ? "" : "s"} need attention.`
163
+ : "Core domain and email trust signals look broadly healthy from public records.",
164
+ confidence: "high",
165
+ source: "public_record",
166
+ risk: input.domainSecurity.issues.length ? "watch" : "positive",
167
+ evidence: emailEvidence,
168
+ action: input.domainSecurity.issues.length
169
+ ? "Prioritise DMARC/SPF/MTA-STS/DNSSEC gaps that align with how important email is for this domain."
170
+ : null,
171
+ });
172
+ const trustEvidence = unique([
173
+ input.securityTxt.status === "present_valid" ? "security.txt valid" : "security.txt missing or incomplete",
174
+ `HSTS preload: ${titleCase(input.publicSignals.hstsPreload.status)}`,
175
+ ...input.htmlSecurity.firstPartyPaths.filter((path) => /privacy|contact|security|support|legal/i.test(path)).slice(0, 5),
176
+ ]);
177
+ addSignal(signals, {
178
+ category: "trust",
179
+ title: "Public trust and disclosure signals",
180
+ summary: input.securityTxt.status === "present_valid"
181
+ ? "A vulnerability disclosure route was visible through security.txt."
182
+ : "No valid security.txt disclosure route was visible from the standard location.",
183
+ confidence: "high",
184
+ source: "public_record",
185
+ risk: input.securityTxt.status === "present_valid" ? "positive" : "watch",
186
+ evidence: trustEvidence,
187
+ action: input.securityTxt.status === "present_valid"
188
+ ? "Keep disclosure contacts, expiry, and policy links current."
189
+ : "Publish /.well-known/security.txt with a monitored contact and policy URL.",
190
+ });
191
+ if (input.htmlSecurity.passiveLeakSignals.length || input.htmlSecurity.clientExposureSignals.length || input.apiSurface.issues.length) {
192
+ const exposureSignals = [
193
+ ...input.htmlSecurity.passiveLeakSignals.map((signal) => signal.title),
194
+ ...input.htmlSecurity.clientExposureSignals.map((signal) => signal.title),
195
+ ...input.apiSurface.issues,
196
+ ];
197
+ addSignal(signals, {
198
+ category: "exposure",
199
+ title: "Client-visible exposure clues",
200
+ summary: `${exposureSignals.length} client-visible exposure signal${exposureSignals.length === 1 ? "" : "s"} were observed in the fetched page or public API hints.`,
201
+ confidence: "medium",
202
+ source: "html",
203
+ risk: input.htmlSecurity.passiveLeakSignals.some((signal) => signal.severity === "warning") ? "attention" : "watch",
204
+ evidence: exposureSignals.slice(0, 8),
205
+ action: "Review exposed client configuration, public endpoint references, source maps, and token-like values for intended visibility.",
206
+ });
207
+ }
208
+ const orderedSignals = signals.sort((a, b) => categoryRank[a.risk] - categoryRank[b.risk]);
209
+ const stackSummary = describeList(unique([
210
+ ...networkStack.map((technology) => technology.name),
211
+ ...hostingStack.map((technology) => technology.name),
212
+ ...serverStack.map((technology) => technology.name),
213
+ ...frontendStack.map((technology) => technology.name),
214
+ ]), "No confident technology stack was inferred from passive evidence.");
215
+ const telemetrySummary = telemetryProviders.length
216
+ ? `Visible telemetry/customer tooling: ${describeList(telemetryProviders, "none")}.`
217
+ : "No prominent analytics, consent, support, or session-replay tooling was identified from visible assets.";
218
+ const trustSummary = orderedSignals.some((signal) => signal.risk === "attention" || signal.risk === "watch")
219
+ ? "Several public trust, exposure, or dependency signals deserve review."
220
+ : "Passive intelligence did not surface major public trust or exposure concerns.";
221
+ return {
222
+ postureRead: orderedSignals.some((signal) => signal.risk === "attention")
223
+ ? "Passive intelligence found public signals that should be reviewed."
224
+ : orderedSignals.some((signal) => signal.risk === "watch")
225
+ ? "Passive intelligence found useful context and a few watch items."
226
+ : "Passive intelligence found useful context without obvious concerns.",
227
+ stackSummary,
228
+ telemetrySummary,
229
+ trustSummary,
230
+ collectionBoundary: SOURCE_BOUNDARY,
231
+ signals: orderedSignals,
232
+ issues: orderedSignals.filter((signal) => signal.risk === "attention" || signal.risk === "watch").map((signal) => signal.summary),
233
+ strengths: orderedSignals.filter((signal) => signal.risk === "positive").map((signal) => signal.summary),
234
+ };
235
+ }
236
+ export function emptyPassiveIntelligence(reason = "Passive intelligence was not available for this scan.") {
237
+ return {
238
+ postureRead: reason,
239
+ stackSummary: "No passive stack summary is available.",
240
+ telemetrySummary: "No passive telemetry summary is available.",
241
+ trustSummary: "No passive trust summary is available.",
242
+ collectionBoundary: SOURCE_BOUNDARY,
243
+ signals: [],
244
+ issues: [],
245
+ strengths: [],
246
+ };
247
+ }
@@ -0,0 +1,4 @@
1
+ export declare function isLikelyPagePath(pathname: string): boolean;
2
+ export declare function scorePagePath(pathname: string): number;
3
+ export declare function normalizeDiscoveredPath(value: string | undefined | null, finalUrl: URL): string | null;
4
+ export declare function rankDiscoveredPaths(paths: Array<string | null | undefined | false>): string[];
@@ -0,0 +1,50 @@
1
+ import { DISCOVERY_PATH_LIMIT } from "./scannerConfig.js";
2
+ import { unique } from "./utils.js";
3
+ const PAGE_PATH_PRIORITY_PATTERNS = [
4
+ /\/login/i,
5
+ /\/account/i,
6
+ /\/dashboard/i,
7
+ /\/admin/i,
8
+ /\/app/i,
9
+ /\/portal/i,
10
+ /\/signin/i,
11
+ /\/auth/i,
12
+ /\/support/i,
13
+ /\/contact/i,
14
+ /\/security/i,
15
+ ];
16
+ export function isLikelyPagePath(pathname) {
17
+ if (!pathname || pathname === "/") {
18
+ return false;
19
+ }
20
+ return !/\.(?:css|js|mjs|json|xml|txt|ico|png|jpe?g|gif|svg|webp|avif|woff2?|ttf|eot|map|pdf|zip|gz|mp4|webm)$/i.test(pathname);
21
+ }
22
+ export function scorePagePath(pathname) {
23
+ return PAGE_PATH_PRIORITY_PATTERNS.reduce((score, pattern, index) => {
24
+ if (pattern.test(pathname)) {
25
+ return score + (PAGE_PATH_PRIORITY_PATTERNS.length - index) * 10;
26
+ }
27
+ return score;
28
+ }, pathname.split("/").filter(Boolean).length <= 2 ? 5 : 0);
29
+ }
30
+ export function normalizeDiscoveredPath(value, finalUrl) {
31
+ if (!value || /^(mailto|tel|javascript):/i.test(value)) {
32
+ return null;
33
+ }
34
+ try {
35
+ const resolved = new URL(value, finalUrl);
36
+ if (resolved.origin !== finalUrl.origin || !isLikelyPagePath(resolved.pathname)) {
37
+ return null;
38
+ }
39
+ const normalizedPath = `${resolved.pathname}${resolved.search}`;
40
+ return normalizedPath.length <= 120 ? normalizedPath : resolved.pathname;
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
46
+ export function rankDiscoveredPaths(paths) {
47
+ return unique(paths)
48
+ .sort((left, right) => scorePagePath(right) - scorePagePath(left))
49
+ .slice(0, DISCOVERY_PATH_LIMIT);
50
+ }
@@ -0,0 +1,142 @@
1
+ import type { AnalysisResult } from "./types.js";
2
+ export declare function buildPostureDigest(analysis: AnalysisResult, { findingLimit }?: {
3
+ findingLimit?: number;
4
+ }): {
5
+ generatedAt: string;
6
+ target: {
7
+ inputUrl: string;
8
+ finalUrl: string;
9
+ host: string;
10
+ statusCode: number;
11
+ responseTimeMs: number;
12
+ scannedAt: string;
13
+ };
14
+ posture: {
15
+ score: number;
16
+ grade: string;
17
+ summary: string;
18
+ overview: string;
19
+ mainRisk: string;
20
+ posture: "strong" | "weak" | "mixed";
21
+ takeaways: string[];
22
+ limited: boolean;
23
+ limitedKind: "other" | "blocked_edge_response" | "auth_required" | "rate_limited" | "service_unavailable";
24
+ limitation: import("./types.js").AssessmentLimitation;
25
+ scoreDrivers: import("./types.js").ScoreDriver[];
26
+ };
27
+ findings: {
28
+ total: number;
29
+ bySeverity: {
30
+ critical: number;
31
+ warning: number;
32
+ info: number;
33
+ };
34
+ top: {
35
+ severity: "info" | "warning" | "critical";
36
+ title: string;
37
+ detail: string;
38
+ confidence: import("./types.js").IssueConfidence;
39
+ source: import("./types.js").IssueSource;
40
+ owasp: import("./types.js").OwaspCategory[];
41
+ mitre: import("./types.js").MitreRelevance[];
42
+ }[];
43
+ };
44
+ remediationPlan: {
45
+ summary: string;
46
+ totalActions: number;
47
+ highImpactActions: number;
48
+ quickWins: number;
49
+ topActions: {
50
+ id: string;
51
+ priority: number;
52
+ title: string;
53
+ owner: import("./types.js").RemediationOwner;
54
+ effort: import("./types.js").RemediationEffort;
55
+ impact: import("./types.js").RemediationImpact;
56
+ action: string;
57
+ verify: string;
58
+ relatedFindings: string[];
59
+ }[];
60
+ };
61
+ controls: {
62
+ headers: {
63
+ total: number;
64
+ missing: number;
65
+ warning: number;
66
+ present: number;
67
+ };
68
+ cookies: {
69
+ total: number;
70
+ issues: string[];
71
+ };
72
+ tls: {
73
+ available: boolean;
74
+ valid: boolean;
75
+ authorized: boolean;
76
+ issuer: string;
77
+ daysRemaining: number;
78
+ issues: string[];
79
+ };
80
+ };
81
+ surface: {
82
+ redirects: {
83
+ totalHops: number;
84
+ hasMixedRedirect: boolean;
85
+ crossesDomain: boolean;
86
+ issues: string[];
87
+ };
88
+ exposure: {
89
+ issues: string[];
90
+ interesting: number;
91
+ exposed: number;
92
+ };
93
+ api: {
94
+ issues: string[];
95
+ public: number;
96
+ interesting: number;
97
+ };
98
+ cors: {
99
+ issues: string[];
100
+ allowCredentials: string;
101
+ allowedOrigin: string;
102
+ };
103
+ };
104
+ trust: {
105
+ domainSecurity: {
106
+ emailDeliverabilityScore: {
107
+ score: number;
108
+ grade: "A" | "B" | "C" | "D" | "F";
109
+ breakdown: Record<string, number>;
110
+ };
111
+ issues: string[];
112
+ strengths: string[];
113
+ };
114
+ securityTxt: {
115
+ status: import("./types.js").SecurityTxtStatus;
116
+ contact: string[];
117
+ };
118
+ thirdParty: {
119
+ providers: string[];
120
+ highRiskProviders: number;
121
+ issues: string[];
122
+ };
123
+ identityProvider: string;
124
+ wafProviders: string[];
125
+ infrastructureProviders: string[];
126
+ };
127
+ intelligence: {
128
+ passiveRead: string;
129
+ compromisePosture: "no_public_ioc" | "review_recommended" | "suspicious" | "reputation_flagged" | "not_assessed";
130
+ compromiseSummary: string;
131
+ riskIndicators: {
132
+ severity: "info" | "warning" | "critical" | "watch";
133
+ category: "infrastructure" | "exposure" | "credential_collection" | "script_anomaly" | "supply_chain" | "reputation";
134
+ title: string;
135
+ detail: string;
136
+ confidence: import("./types.js").IssueConfidence;
137
+ }[];
138
+ ctPriorityHosts: string[];
139
+ aiVendors: string[];
140
+ };
141
+ timing: import("./types.js").ScanTimingInfo;
142
+ };