securl 1.4.1 → 1.5.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.
- package/CHANGELOG.md +13 -0
- package/README.md +19 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +11 -5
- package/dist/postureDigest.d.ts +18 -0
- package/dist/postureDigest.js +18 -0
- package/dist/postureRemediation.d.ts +4 -0
- package/dist/postureRemediation.js +62 -0
- package/dist/types.d.ts +20 -0
- package/package.json +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,19 @@ The format is based on Keep a Changelog and this package follows Semantic Versio
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.5.1] - 2026-06-15
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- Tightened npm package positioning around passive URL security posture scanning and clarified that SecURL is not a URL shortener.
|
|
13
|
+
- Filtered public GitHub package signals to dependency-key matches so same-name projects are not counted as adoption.
|
|
14
|
+
|
|
15
|
+
## [1.5.0] - 2026-06-14
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- Added `buildPostureEvidenceSummary()` for compact, API/mobile-friendly evidence metadata that explains score drivers and findings without requiring clients to inspect the full scan payload.
|
|
19
|
+
- Added the `securl/evidence-summary` package export for consumers that want the evidence summary helper directly.
|
|
20
|
+
- Added `evidenceSummary` to completed analysis results and posture digests.
|
|
21
|
+
|
|
9
22
|
## [1.4.1] - 2026-06-14
|
|
10
23
|
|
|
11
24
|
### Changed
|
package/README.md
CHANGED
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
[](./LICENSE)
|
|
6
6
|
[](https://github.com/this-is-securl/securl/actions/workflows/core-package-checks.yml)
|
|
7
7
|
|
|
8
|
-
**The passive security posture engine behind SecURL.**
|
|
8
|
+
**The passive URL security posture engine behind SecURL.**
|
|
9
9
|
|
|
10
|
-
`securl` is the reusable scanner engine behind [SecURL](https://securl.online), a
|
|
10
|
+
`securl` is the reusable scanner engine behind [SecURL](https://securl.online), a passive external security posture scanner for public URLs and web services.
|
|
11
11
|
|
|
12
12
|
It is designed for passive, low-noise assessment rather than active exploitation or broad reconnaissance. The engine turns public web signals into structured JSON, Markdown, SARIF, and CI-friendly output.
|
|
13
13
|
|
|
14
|
+
It is not a URL shortener. It assesses the external security posture of URLs you choose to scan.
|
|
15
|
+
|
|
14
16
|
Use it when you need a fast outside-in read on a public web service:
|
|
15
17
|
|
|
16
18
|
- run a security posture smoke check in CI before release
|
|
@@ -193,6 +195,20 @@ console.log(remediationPlan.items.map((item) => ({
|
|
|
193
195
|
})));
|
|
194
196
|
```
|
|
195
197
|
|
|
198
|
+
Version `1.5.0+` includes a compact evidence summary for API, mobile, and report clients that need to explain why a scan scored the way it did without walking the full result object.
|
|
199
|
+
|
|
200
|
+
```js
|
|
201
|
+
import { buildPostureEvidenceSummary } from "securl/evidence-summary";
|
|
202
|
+
|
|
203
|
+
const evidenceSummary = buildPostureEvidenceSummary(resultWithEvidence);
|
|
204
|
+
|
|
205
|
+
console.log({
|
|
206
|
+
total: evidenceSummary.totalEvidenceReferences,
|
|
207
|
+
observed: evidenceSummary.observedCount,
|
|
208
|
+
topEvidence: evidenceSummary.topEvidence,
|
|
209
|
+
});
|
|
210
|
+
```
|
|
211
|
+
|
|
196
212
|
## Package trust and release signals
|
|
197
213
|
|
|
198
214
|
- public source repository with package code under `packages/core`
|
|
@@ -270,6 +286,7 @@ Primary exports:
|
|
|
270
286
|
- `buildPostureDriftReportFromSnapshots(current, previous)` - produce a complete scan-to-scan drift report for monitoring, alerting, and history views.
|
|
271
287
|
- `buildPostureRemediationPlan(result)` - generate prioritized, owner-aware remediation actions from findings and score drivers.
|
|
272
288
|
- `attachIssueEvidence(result)` - add structured evidence references to findings without changing their existing fields.
|
|
289
|
+
- `buildPostureEvidenceSummary(result)` - produce compact evidence metadata for API, mobile, report, and explainability surfaces.
|
|
273
290
|
|
|
274
291
|
Package subpath exports:
|
|
275
292
|
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { AnalysisResult, AnalyzeTargetOptions, HtmlSecurityInfo } from "./t
|
|
|
3
3
|
export { buildPostureRiskEventsFromDiff, buildPostureRiskEventsFromSnapshots } from "./riskEvents.js";
|
|
4
4
|
export { buildPostureDigest } from "./postureDigest.js";
|
|
5
5
|
export { buildPostureDriftReport, buildPostureDriftReportFromDiff, buildPostureDriftReportFromSnapshots, } from "./postureDrift.js";
|
|
6
|
-
export { attachIssueEvidence, buildIssueEvidence, buildPostureRemediationPlan, } from "./postureRemediation.js";
|
|
6
|
+
export { attachIssueEvidence, buildIssueEvidence, buildPostureEvidenceSummary, buildPostureRemediationPlan, } from "./postureRemediation.js";
|
|
7
7
|
export type { PostureRiskEvent, PostureRiskEventSeverity } from "./types.js";
|
|
8
8
|
declare function formatErrorMessage(error: any): string;
|
|
9
9
|
export declare function analyzeHtmlDocument(input: string | URL, html: string): HtmlSecurityInfo;
|
package/dist/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import { fetchWithRedirects, requestJson, requestOnce, requestText, requestWithH
|
|
|
18
18
|
import { normalizeDiscoveredPath, rankDiscoveredPaths } from "./path-discovery.js";
|
|
19
19
|
import { buildPassiveIntelligence, emptyPassiveIntelligence } from "./passive-intelligence.js";
|
|
20
20
|
import { analyzeRedirectChain } from "./redirectChain.js";
|
|
21
|
-
import { attachIssueEvidence, buildPostureRemediationPlan } from "./postureRemediation.js";
|
|
21
|
+
import { attachIssueEvidence, buildPostureEvidenceSummary, buildPostureRemediationPlan } from "./postureRemediation.js";
|
|
22
22
|
import { scoreAnalysis, scorePostureAnalysis, summarizePostureGrade } from "./scoring.js";
|
|
23
23
|
import { fetchSecurityTxt } from "./security-txt.js";
|
|
24
24
|
import { detectTechnologies } from "./technology-detection.js";
|
|
@@ -27,7 +27,7 @@ import { analyzeWafFingerprint } from "./wafFingerprint.js";
|
|
|
27
27
|
export { buildPostureRiskEventsFromDiff, buildPostureRiskEventsFromSnapshots } from "./riskEvents.js";
|
|
28
28
|
export { buildPostureDigest } from "./postureDigest.js";
|
|
29
29
|
export { buildPostureDriftReport, buildPostureDriftReportFromDiff, buildPostureDriftReportFromSnapshots, } from "./postureDrift.js";
|
|
30
|
-
export { attachIssueEvidence, buildIssueEvidence, buildPostureRemediationPlan, } from "./postureRemediation.js";
|
|
30
|
+
export { attachIssueEvidence, buildIssueEvidence, buildPostureEvidenceSummary, buildPostureRemediationPlan, } from "./postureRemediation.js";
|
|
31
31
|
function buildScanProfile(mode, requestedTimeoutMs) {
|
|
32
32
|
const deepPassive = mode === "deep-passive";
|
|
33
33
|
const scanTimeoutMs = requestedTimeoutMs ?? (deepPassive ? DEEP_PASSIVE_SCAN_TIMEOUT_MS : MAX_SCAN_DURATION_MS);
|
|
@@ -500,9 +500,11 @@ async function buildLimitedResult(input, normalizedInput, failure, scanTiming) {
|
|
|
500
500
|
},
|
|
501
501
|
};
|
|
502
502
|
const evidenceResult = attachIssueEvidence(limitedResult);
|
|
503
|
+
const remediationPlan = buildPostureRemediationPlan(evidenceResult);
|
|
503
504
|
return {
|
|
504
505
|
...evidenceResult,
|
|
505
|
-
remediationPlan
|
|
506
|
+
remediationPlan,
|
|
507
|
+
evidenceSummary: buildPostureEvidenceSummary({ ...evidenceResult, remediationPlan }),
|
|
506
508
|
};
|
|
507
509
|
}
|
|
508
510
|
async function enrichCoreResult(result, profile) {
|
|
@@ -928,9 +930,11 @@ function buildTimedOutEnrichmentResult(result, pageAnalysisEnabled, timeoutMs, c
|
|
|
928
930
|
executiveSummary: buildExecutiveSummary(timedOutResult),
|
|
929
931
|
};
|
|
930
932
|
const evidenceResult = attachIssueEvidence(timedOutResultWithSummary);
|
|
933
|
+
const remediationPlan = buildPostureRemediationPlan(evidenceResult);
|
|
931
934
|
return {
|
|
932
935
|
...evidenceResult,
|
|
933
|
-
remediationPlan
|
|
936
|
+
remediationPlan,
|
|
937
|
+
evidenceSummary: buildPostureEvidenceSummary({ ...evidenceResult, remediationPlan }),
|
|
934
938
|
};
|
|
935
939
|
}
|
|
936
940
|
export async function analyzeUrl(input, options = {}) {
|
|
@@ -995,9 +999,11 @@ export async function analyzeUrl(input, options = {}) {
|
|
|
995
999
|
executiveSummary: buildExecutiveSummary(scoredResult),
|
|
996
1000
|
};
|
|
997
1001
|
const resultWithEvidence = attachIssueEvidence(resultWithSummary);
|
|
1002
|
+
const remediationPlan = buildPostureRemediationPlan(resultWithEvidence);
|
|
998
1003
|
return {
|
|
999
1004
|
...resultWithEvidence,
|
|
1000
|
-
remediationPlan
|
|
1005
|
+
remediationPlan,
|
|
1006
|
+
evidenceSummary: buildPostureEvidenceSummary({ ...resultWithEvidence, remediationPlan }),
|
|
1001
1007
|
};
|
|
1002
1008
|
}
|
|
1003
1009
|
export const analyzeTarget = analyzeUrl;
|
package/dist/postureDigest.d.ts
CHANGED
|
@@ -41,6 +41,24 @@ export declare function buildPostureDigest(analysis: AnalysisResult, { findingLi
|
|
|
41
41
|
mitre: import("./types.js").MitreRelevance[];
|
|
42
42
|
}[];
|
|
43
43
|
};
|
|
44
|
+
evidence: {
|
|
45
|
+
summary: string;
|
|
46
|
+
totalEvidenceReferences: number;
|
|
47
|
+
byKind: Partial<Record<import("./types.js").ScanEvidenceKind, number>>;
|
|
48
|
+
observedCount: number;
|
|
49
|
+
derivedCount: number;
|
|
50
|
+
topEvidence: {
|
|
51
|
+
kind: import("./types.js").ScanEvidenceKind;
|
|
52
|
+
label: string;
|
|
53
|
+
observed: string;
|
|
54
|
+
expected: string;
|
|
55
|
+
source: import("./types.js").IssueSource | "headers" | "cookies" | "tls" | "dns" | "html" | "public_record" | "third_party" | "ai" | "availability" | "breadth" | "assessment_limit" | "derived";
|
|
56
|
+
areaLabel: string;
|
|
57
|
+
relatedFinding: string;
|
|
58
|
+
severity: "info" | "warning" | "critical";
|
|
59
|
+
scoreImpact: number;
|
|
60
|
+
}[];
|
|
61
|
+
};
|
|
44
62
|
remediationPlan: {
|
|
45
63
|
summary: string;
|
|
46
64
|
totalActions: number;
|
package/dist/postureDigest.js
CHANGED
|
@@ -59,6 +59,24 @@ export function buildPostureDigest(analysis, { findingLimit = 8 } = {}) {
|
|
|
59
59
|
bySeverity: countIssuesBySeverity(issues),
|
|
60
60
|
top: topIssues(issues, findingLimit),
|
|
61
61
|
},
|
|
62
|
+
evidence: analysis.evidenceSummary ? {
|
|
63
|
+
summary: analysis.evidenceSummary.summary,
|
|
64
|
+
totalEvidenceReferences: analysis.evidenceSummary.totalEvidenceReferences,
|
|
65
|
+
byKind: analysis.evidenceSummary.byKind,
|
|
66
|
+
observedCount: analysis.evidenceSummary.observedCount,
|
|
67
|
+
derivedCount: analysis.evidenceSummary.derivedCount,
|
|
68
|
+
topEvidence: normalizeArray(analysis.evidenceSummary.topEvidence).slice(0, 5).map((reference) => ({
|
|
69
|
+
kind: reference.kind,
|
|
70
|
+
label: reference.label,
|
|
71
|
+
observed: reference.observed,
|
|
72
|
+
expected: reference.expected,
|
|
73
|
+
source: reference.source,
|
|
74
|
+
areaLabel: reference.areaLabel,
|
|
75
|
+
relatedFinding: reference.relatedFinding,
|
|
76
|
+
severity: reference.severity,
|
|
77
|
+
scoreImpact: reference.scoreImpact,
|
|
78
|
+
})),
|
|
79
|
+
} : null,
|
|
62
80
|
remediationPlan: analysis.remediationPlan ? {
|
|
63
81
|
summary: analysis.remediationPlan.summary,
|
|
64
82
|
totalActions: analysis.remediationPlan.totalActions,
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { AnalysisResult, RemediationPlan, ScanEvidenceReference, ScanIssue } from "./types.js";
|
|
2
|
+
import type { PostureEvidenceSummary } from "./types.js";
|
|
2
3
|
export declare function buildIssueEvidence(issue: ScanIssue, analysis: AnalysisResult): ScanEvidenceReference[];
|
|
3
4
|
export declare function attachIssueEvidence(analysis: AnalysisResult): AnalysisResult;
|
|
5
|
+
export declare function buildPostureEvidenceSummary(analysis: AnalysisResult, { limit }?: {
|
|
6
|
+
limit?: number;
|
|
7
|
+
}): PostureEvidenceSummary;
|
|
4
8
|
export declare function buildPostureRemediationPlan(analysis: AnalysisResult, { limit }?: {
|
|
5
9
|
limit?: number;
|
|
6
10
|
}): RemediationPlan;
|
|
@@ -73,6 +73,9 @@ function evidenceKindForScoreSource(source) {
|
|
|
73
73
|
return "public_record";
|
|
74
74
|
return "score_driver";
|
|
75
75
|
}
|
|
76
|
+
function incrementCount(counts, key) {
|
|
77
|
+
counts[key] = (counts[key] ?? 0) + 1;
|
|
78
|
+
}
|
|
76
79
|
function headerEvidenceForIssue(issue, headers) {
|
|
77
80
|
const issueText = `${issue.title} ${issue.detail}`.toLowerCase();
|
|
78
81
|
const matched = headers.find((header) => issueText.includes(header.label.toLowerCase()) || issueText.includes(header.key.toLowerCase()));
|
|
@@ -165,6 +168,65 @@ export function attachIssueEvidence(analysis) {
|
|
|
165
168
|
})),
|
|
166
169
|
};
|
|
167
170
|
}
|
|
171
|
+
function rankEvidence(left, right) {
|
|
172
|
+
const impactDelta = (right.scoreImpact ?? -1) - (left.scoreImpact ?? -1);
|
|
173
|
+
if (impactDelta !== 0)
|
|
174
|
+
return impactDelta;
|
|
175
|
+
const severityRank = { critical: 0, warning: 1, info: 2 };
|
|
176
|
+
const severityDelta = (severityRank[left.severity ?? "info"] ?? 2) - (severityRank[right.severity ?? "info"] ?? 2);
|
|
177
|
+
if (severityDelta !== 0)
|
|
178
|
+
return severityDelta;
|
|
179
|
+
return left.label.localeCompare(right.label);
|
|
180
|
+
}
|
|
181
|
+
export function buildPostureEvidenceSummary(analysis, { limit = 12 } = {}) {
|
|
182
|
+
const scoreDriverEvidence = normalizeArray(analysis.scoreDrivers)
|
|
183
|
+
.filter((driver) => driver.impact > 0)
|
|
184
|
+
.map((driver) => ({
|
|
185
|
+
kind: evidenceKindForScoreSource(driver.source),
|
|
186
|
+
label: driver.label,
|
|
187
|
+
observed: driver.detail,
|
|
188
|
+
source: driver.source,
|
|
189
|
+
areaLabel: driver.areaLabel,
|
|
190
|
+
scoreImpact: driver.impact,
|
|
191
|
+
}));
|
|
192
|
+
const findingEvidence = normalizeArray(analysis.issues).flatMap((issue) => {
|
|
193
|
+
const evidence = normalizeArray(issue.evidence).length ? normalizeArray(issue.evidence) : buildIssueEvidence(issue, analysis);
|
|
194
|
+
return evidence.map((reference) => ({
|
|
195
|
+
...reference,
|
|
196
|
+
relatedFinding: issue.title,
|
|
197
|
+
severity: issue.severity,
|
|
198
|
+
scoreImpact: null,
|
|
199
|
+
}));
|
|
200
|
+
});
|
|
201
|
+
const allEvidence = [
|
|
202
|
+
...scoreDriverEvidence,
|
|
203
|
+
...findingEvidence,
|
|
204
|
+
];
|
|
205
|
+
const byKind = {};
|
|
206
|
+
const bySource = {};
|
|
207
|
+
for (const reference of allEvidence) {
|
|
208
|
+
incrementCount(byKind, reference.kind);
|
|
209
|
+
incrementCount(bySource, String(reference.source ?? "unknown"));
|
|
210
|
+
}
|
|
211
|
+
const observedKinds = new Set(["header", "tls", "cookie", "redirect", "dns", "html", "public_record"]);
|
|
212
|
+
const observedCount = allEvidence.filter((reference) => observedKinds.has(reference.kind)).length;
|
|
213
|
+
const derivedCount = allEvidence.length - observedCount;
|
|
214
|
+
return {
|
|
215
|
+
generatedAt: new Date().toISOString(),
|
|
216
|
+
summary: allEvidence.length
|
|
217
|
+
? `${allEvidence.length} evidence reference${allEvidence.length === 1 ? "" : "s"} explain the main score drivers and findings.`
|
|
218
|
+
: "No structured evidence references were generated for this scan.",
|
|
219
|
+
totalEvidenceReferences: allEvidence.length,
|
|
220
|
+
byKind,
|
|
221
|
+
bySource,
|
|
222
|
+
observedCount,
|
|
223
|
+
derivedCount,
|
|
224
|
+
topEvidence: [...allEvidence].sort(rankEvidence).slice(0, limit),
|
|
225
|
+
scoreDriverEvidence: scoreDriverEvidence.slice(0, limit),
|
|
226
|
+
findingEvidence: findingEvidence.sort(rankEvidence).slice(0, limit),
|
|
227
|
+
limitation: analysis.assessmentLimitation ?? null,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
168
230
|
function relatedFindingsForDriver(driver, issues) {
|
|
169
231
|
const driverText = `${driver.areaKey} ${driver.label} ${driver.detail}`.toLowerCase();
|
|
170
232
|
return issues
|
package/dist/types.d.ts
CHANGED
|
@@ -137,6 +137,25 @@ export interface RemediationPlan {
|
|
|
137
137
|
quickWins: number;
|
|
138
138
|
items: RemediationPlanItem[];
|
|
139
139
|
}
|
|
140
|
+
export interface PostureEvidenceSummaryReference extends ScanEvidenceReference {
|
|
141
|
+
areaLabel?: string;
|
|
142
|
+
relatedFinding?: string;
|
|
143
|
+
severity?: Exclude<Severity, "good">;
|
|
144
|
+
scoreImpact?: number | null;
|
|
145
|
+
}
|
|
146
|
+
export interface PostureEvidenceSummary {
|
|
147
|
+
generatedAt: string;
|
|
148
|
+
summary: string;
|
|
149
|
+
totalEvidenceReferences: number;
|
|
150
|
+
byKind: Partial<Record<ScanEvidenceKind, number>>;
|
|
151
|
+
bySource: Record<string, number>;
|
|
152
|
+
observedCount: number;
|
|
153
|
+
derivedCount: number;
|
|
154
|
+
topEvidence: PostureEvidenceSummaryReference[];
|
|
155
|
+
scoreDriverEvidence: PostureEvidenceSummaryReference[];
|
|
156
|
+
findingEvidence: PostureEvidenceSummaryReference[];
|
|
157
|
+
limitation: AssessmentLimitation | null;
|
|
158
|
+
}
|
|
140
159
|
export interface CrawlPageResult {
|
|
141
160
|
label: string;
|
|
142
161
|
path: string;
|
|
@@ -701,6 +720,7 @@ export interface AnalysisResult {
|
|
|
701
720
|
strengths: string[];
|
|
702
721
|
remediation: RemediationSnippet[];
|
|
703
722
|
remediationPlan?: RemediationPlan;
|
|
723
|
+
evidenceSummary?: PostureEvidenceSummary;
|
|
704
724
|
crawl: CrawlSummary;
|
|
705
725
|
securityTxt: SecurityTxtInfo;
|
|
706
726
|
domainSecurity: DomainSecurityInfo;
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "securl",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Passive external security posture
|
|
5
|
+
"description": "Passive external security posture scanner for public URLs and web services.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Keith Batterham",
|
|
8
8
|
"url": "https://github.com/ktbatterham"
|
|
@@ -61,6 +61,10 @@
|
|
|
61
61
|
"./remediation-plan": {
|
|
62
62
|
"types": "./dist/postureRemediation.d.ts",
|
|
63
63
|
"default": "./dist/postureRemediation.js"
|
|
64
|
+
},
|
|
65
|
+
"./evidence-summary": {
|
|
66
|
+
"types": "./dist/postureRemediation.d.ts",
|
|
67
|
+
"default": "./dist/postureRemediation.js"
|
|
64
68
|
}
|
|
65
69
|
},
|
|
66
70
|
"files": [
|