securl 1.6.0 → 1.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/CHANGELOG.md CHANGED
@@ -7,6 +7,8 @@ The format is based on Keep a Changelog and this package follows Semantic Versio
7
7
  ## [Unreleased]
8
8
 
9
9
  ### Added
10
+ - Added `buildVendorExposureBrief()` for compact vendor and supply-chain exposure summaries covering visible third-party providers, data-flow categories, SRI gaps, priority vendors, and next actions.
11
+ - Added `vendorExposure` to analysis results and the `securl/vendor-exposure` package export for SDK consumers.
10
12
  - Added `buildExposureBrief()` for compact outside-observer action briefs covering public entry points, sensitive exposures, trust gaps, abuse indicators, third-party risk, AI surface signals, and next actions.
11
13
  - Added `exposureBrief` to analysis results and the `securl/exposure-brief` package export for SDK consumers.
12
14
 
package/dist/cli.js CHANGED
File without changes
package/dist/index.d.ts CHANGED
@@ -12,6 +12,7 @@ export declare const analyzeTarget: typeof analyzeUrl;
12
12
  export { formatErrorMessage };
13
13
  export { buildCompromiseSignals, emptyCompromiseSignals } from "./compromiseSignals.js";
14
14
  export { buildExposureBrief } from "./exposureBrief.js";
15
+ export { buildVendorExposureBrief } from "./vendorExposure.js";
15
16
  export { analyzeInfrastructure } from "./infrastructure.js";
16
17
  export { buildHistoryDiff, buildHistoryDiffFromSnapshots, snapshotFromAnalysis } from "./historyDiff.js";
17
18
  export { assertPublicRequestTarget, isLocalHostname, isPrivateAddress, } from "./network-validation.js";
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ import { URL } from "node:url";
2
2
  import { scanTls } from "./certificate.js";
3
3
  import { buildCompromiseSignals, emptyCompromiseSignals } from "./compromiseSignals.js";
4
4
  import { buildExposureBrief } from "./exposureBrief.js";
5
+ import { buildVendorExposureBrief } from "./vendorExposure.js";
5
6
  import { parseSetCookie } from "./cookie-analysis.js";
6
7
  import { analyzeCookieHeaders } from "./cookieAnalysis.js";
7
8
  import { fetchCtDiscovery } from "./ctDiscovery.js";
@@ -510,6 +511,7 @@ async function buildLimitedResult(input, normalizedInput, failure, scanTiming) {
510
511
  return {
511
512
  ...resultWithRemediation,
512
513
  exposureBrief: buildExposureBrief(resultWithRemediation),
514
+ vendorExposure: buildVendorExposureBrief(resultWithRemediation),
513
515
  };
514
516
  }
515
517
  async function enrichCoreResult(result, profile) {
@@ -944,6 +946,7 @@ function buildTimedOutEnrichmentResult(result, pageAnalysisEnabled, timeoutMs, c
944
946
  return {
945
947
  ...resultWithRemediation,
946
948
  exposureBrief: buildExposureBrief(resultWithRemediation),
949
+ vendorExposure: buildVendorExposureBrief(resultWithRemediation),
947
950
  };
948
951
  }
949
952
  export async function analyzeUrl(input, options = {}) {
@@ -1017,12 +1020,14 @@ export async function analyzeUrl(input, options = {}) {
1017
1020
  return {
1018
1021
  ...resultWithRemediation,
1019
1022
  exposureBrief: buildExposureBrief(resultWithRemediation),
1023
+ vendorExposure: buildVendorExposureBrief(resultWithRemediation),
1020
1024
  };
1021
1025
  }
1022
1026
  export const analyzeTarget = analyzeUrl;
1023
1027
  export { formatErrorMessage };
1024
1028
  export { buildCompromiseSignals, emptyCompromiseSignals } from "./compromiseSignals.js";
1025
1029
  export { buildExposureBrief } from "./exposureBrief.js";
1030
+ export { buildVendorExposureBrief } from "./vendorExposure.js";
1026
1031
  export { analyzeInfrastructure } from "./infrastructure.js";
1027
1032
  export { buildHistoryDiff, buildHistoryDiffFromSnapshots, snapshotFromAnalysis } from "./historyDiff.js";
1028
1033
  export { assertPublicRequestTarget, isLocalHostname, isPrivateAddress, } from "./network-validation.js";
package/dist/types.d.ts CHANGED
@@ -190,6 +190,40 @@ export interface ExposureBrief {
190
190
  collectionBoundary: string;
191
191
  limitation: AssessmentLimitation | null;
192
192
  }
193
+ export type VendorExposureRisk = "low" | "medium" | "high";
194
+ export interface VendorExposureProvider {
195
+ name: string;
196
+ domain: string;
197
+ category: ThirdPartyProvider["category"];
198
+ risk: ThirdPartyProvider["risk"];
199
+ evidence: string;
200
+ reviewPriority: "routine" | "review" | "urgent";
201
+ dataFlow: "content_delivery" | "telemetry" | "user_interaction" | "payment" | "security" | "ai" | "unknown";
202
+ action: string;
203
+ }
204
+ export interface VendorExposureBrief {
205
+ generatedAt: string;
206
+ risk: VendorExposureRisk;
207
+ summary: string;
208
+ counts: {
209
+ totalProviders: number;
210
+ highRiskProviders: number;
211
+ mediumRiskProviders: number;
212
+ sessionReplayProviders: number;
213
+ analyticsProviders: number;
214
+ aiProviders: number;
215
+ paymentProviders: number;
216
+ supportProviders: number;
217
+ missingSriScripts: number;
218
+ };
219
+ providers: VendorExposureProvider[];
220
+ highPriorityProviders: VendorExposureProvider[];
221
+ issues: string[];
222
+ strengths: string[];
223
+ nextActions: string[];
224
+ collectionBoundary: string;
225
+ limitation: AssessmentLimitation | null;
226
+ }
193
227
  export interface CrawlPageResult {
194
228
  label: string;
195
229
  path: string;
@@ -756,6 +790,7 @@ export interface AnalysisResult {
756
790
  remediationPlan?: RemediationPlan;
757
791
  evidenceSummary?: PostureEvidenceSummary;
758
792
  exposureBrief?: ExposureBrief;
793
+ vendorExposure?: VendorExposureBrief;
759
794
  crawl: CrawlSummary;
760
795
  securityTxt: SecurityTxtInfo;
761
796
  domainSecurity: DomainSecurityInfo;
@@ -0,0 +1,2 @@
1
+ import type { AnalysisResult, VendorExposureBrief } from "./types.js";
2
+ export declare function buildVendorExposureBrief(analysis: AnalysisResult): VendorExposureBrief;
@@ -0,0 +1,151 @@
1
+ const normalizeArray = (value) => (Array.isArray(value) ? value : []);
2
+ function dataFlowForCategory(category) {
3
+ if (category === "analytics" || category === "ads" || category === "session_replay") {
4
+ return "telemetry";
5
+ }
6
+ if (category === "support" || category === "social" || category === "consent") {
7
+ return "user_interaction";
8
+ }
9
+ if (category === "payments") {
10
+ return "payment";
11
+ }
12
+ if (category === "security") {
13
+ return "security";
14
+ }
15
+ if (category === "ai") {
16
+ return "ai";
17
+ }
18
+ if (category === "cdn") {
19
+ return "content_delivery";
20
+ }
21
+ return "unknown";
22
+ }
23
+ function reviewPriority(provider) {
24
+ if (provider.risk === "high" || provider.category === "session_replay" || provider.category === "payments") {
25
+ return "urgent";
26
+ }
27
+ if (provider.risk === "medium" || provider.category === "ai" || provider.category === "ads") {
28
+ return "review";
29
+ }
30
+ return "routine";
31
+ }
32
+ function actionForProvider(provider) {
33
+ if (provider.category === "session_replay") {
34
+ return "Confirm session replay masking, consent coverage, retention, and vendor ownership.";
35
+ }
36
+ if (provider.category === "payments") {
37
+ return "Confirm payment provider ownership, PCI scope, and expected public loading paths.";
38
+ }
39
+ if (provider.category === "ai") {
40
+ return "Confirm AI vendor disclosure, data-handling boundaries, and escalation ownership.";
41
+ }
42
+ if (provider.risk === "high") {
43
+ return "Confirm the provider is intentional, documented, and covered by security and privacy review.";
44
+ }
45
+ if (provider.risk === "medium") {
46
+ return "Review whether the provider is still needed and document the data-flow owner.";
47
+ }
48
+ return "Keep the provider in the vendor inventory and monitor for drift.";
49
+ }
50
+ function rankProvider(provider) {
51
+ const priorityWeight = { urgent: 0, review: 1, routine: 2 }[provider.reviewPriority];
52
+ const riskWeight = { high: 0, medium: 1, low: 2 }[provider.risk];
53
+ return priorityWeight * 10 + riskWeight;
54
+ }
55
+ function summarizeRisk(risk, counts) {
56
+ if (counts.totalProviders === 0) {
57
+ return "No obvious third-party script or stylesheet providers were observed on the fetched page.";
58
+ }
59
+ if (risk === "high") {
60
+ return "The fetched page exposes high-priority third-party dependencies that deserve explicit ownership and review.";
61
+ }
62
+ if (risk === "medium") {
63
+ return "The fetched page has a visible vendor footprint with review-worthy data-flow or integrity considerations.";
64
+ }
65
+ return "The fetched page uses third-party providers, but the visible footprint is mostly lower-risk delivery or operational tooling.";
66
+ }
67
+ function deriveRisk(counts, issues) {
68
+ if (counts.highRiskProviders > 0 ||
69
+ counts.sessionReplayProviders > 0 ||
70
+ counts.missingSriScripts >= 3 ||
71
+ issues.some((issue) => /session replay|high-trust|high-observability/i.test(issue))) {
72
+ return "high";
73
+ }
74
+ if (counts.mediumRiskProviders > 0 || counts.aiProviders > 0 || counts.paymentProviders > 0 || counts.missingSriScripts > 0 || issues.length > 0) {
75
+ return "medium";
76
+ }
77
+ return "low";
78
+ }
79
+ function pushUnique(values, value) {
80
+ if (!value) {
81
+ return;
82
+ }
83
+ const trimmed = value.trim();
84
+ if (!trimmed || values.includes(trimmed)) {
85
+ return;
86
+ }
87
+ values.push(trimmed);
88
+ }
89
+ export function buildVendorExposureBrief(analysis) {
90
+ const sourceProviders = normalizeArray(analysis.thirdPartyTrust?.providers);
91
+ const providers = sourceProviders
92
+ .map((provider) => ({
93
+ name: provider.name,
94
+ domain: provider.domain,
95
+ category: provider.category,
96
+ risk: provider.risk,
97
+ evidence: provider.evidence,
98
+ reviewPriority: reviewPriority(provider),
99
+ dataFlow: dataFlowForCategory(provider.category),
100
+ action: actionForProvider(provider),
101
+ }))
102
+ .sort((left, right) => {
103
+ const rankDelta = rankProvider(left) - rankProvider(right);
104
+ if (rankDelta !== 0) {
105
+ return rankDelta;
106
+ }
107
+ return left.name.localeCompare(right.name);
108
+ });
109
+ const missingSriScripts = normalizeArray(analysis.htmlSecurity?.missingSriScriptUrls).length;
110
+ const issues = normalizeArray(analysis.thirdPartyTrust?.issues);
111
+ const strengths = normalizeArray(analysis.thirdPartyTrust?.strengths);
112
+ const counts = {
113
+ totalProviders: analysis.thirdPartyTrust?.totalProviders ?? providers.length,
114
+ highRiskProviders: analysis.thirdPartyTrust?.highRiskProviders ?? providers.filter((provider) => provider.risk === "high").length,
115
+ mediumRiskProviders: providers.filter((provider) => provider.risk === "medium").length,
116
+ sessionReplayProviders: providers.filter((provider) => provider.category === "session_replay").length,
117
+ analyticsProviders: providers.filter((provider) => provider.category === "analytics" || provider.category === "ads").length,
118
+ aiProviders: providers.filter((provider) => provider.category === "ai").length + normalizeArray(analysis.aiSurface?.vendors).length,
119
+ paymentProviders: providers.filter((provider) => provider.category === "payments").length,
120
+ supportProviders: providers.filter((provider) => provider.category === "support").length,
121
+ missingSriScripts,
122
+ };
123
+ const risk = deriveRisk(counts, issues);
124
+ const highPriorityProviders = providers.filter((provider) => provider.reviewPriority !== "routine").slice(0, 10);
125
+ const nextActions = [];
126
+ for (const provider of highPriorityProviders) {
127
+ pushUnique(nextActions, provider.action);
128
+ }
129
+ if (missingSriScripts > 0) {
130
+ pushUnique(nextActions, "Add Subresource Integrity for third-party scripts that can be pinned safely, or document why they cannot be pinned.");
131
+ }
132
+ if (counts.totalProviders > 0) {
133
+ pushUnique(nextActions, "Keep a lightweight vendor inventory covering owner, purpose, data handled, and removal criteria.");
134
+ }
135
+ if (nextActions.length === 0) {
136
+ pushUnique(nextActions, "Keep monitoring vendor drift after frontend, analytics, support, payment, or AI changes.");
137
+ }
138
+ return {
139
+ generatedAt: new Date().toISOString(),
140
+ risk,
141
+ summary: summarizeRisk(risk, counts),
142
+ counts,
143
+ providers,
144
+ highPriorityProviders,
145
+ issues,
146
+ strengths,
147
+ nextActions: nextActions.slice(0, 6),
148
+ collectionBoundary: "Passive public page evidence only. Vendor signals are inferred from fetched HTML, scripts, stylesheets, and visible AI/provider markers.",
149
+ limitation: analysis.assessmentLimitation?.limited ? analysis.assessmentLimitation : null,
150
+ };
151
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securl",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "type": "module",
5
5
  "description": "Passive external security posture scanner for public URLs and web services.",
6
6
  "author": {
@@ -69,6 +69,10 @@
69
69
  "./exposure-brief": {
70
70
  "types": "./dist/exposureBrief.d.ts",
71
71
  "default": "./dist/exposureBrief.js"
72
+ },
73
+ "./vendor-exposure": {
74
+ "types": "./dist/vendorExposure.d.ts",
75
+ "default": "./dist/vendorExposure.js"
72
76
  }
73
77
  },
74
78
  "files": [