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 +2 -0
- package/dist/cli.js +0 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -0
- package/dist/types.d.ts +35 -0
- package/dist/vendorExposure.d.ts +2 -0
- package/dist/vendorExposure.js +151 -0
- package/package.json +5 -1
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,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.
|
|
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": [
|