securl 1.11.0 → 1.12.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 +11 -0
- package/README.md +25 -2
- package/RELEASING.md +9 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +20 -6
- package/dist/postureInsights.d.ts +2 -0
- package/dist/postureInsights.js +127 -0
- package/dist/types.d.ts +48 -0
- package/package.json +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,17 @@ The format is based on Keep a Changelog and this package follows Semantic Versio
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.12.0] - 2026-06-27
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Added `buildPostureInsights()` and the `securl/posture-insights` package export for display-ready risk themes, top insights, and next-best actions.
|
|
13
|
+
- Added `postureInsights` to completed analysis results so API, mobile, and CLI clients can render security judgement without reinterpreting raw findings.
|
|
14
|
+
|
|
15
|
+
## [1.11.1] - 2026-06-24
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- Updated `node-html-parser` to 8.0.3, replacing the retired entity decoder while preserving the existing parsing API and Node 22 runtime floor.
|
|
19
|
+
|
|
9
20
|
## [1.11.0] - 2026-06-20
|
|
10
21
|
|
|
11
22
|
### Added
|
package/README.md
CHANGED
|
@@ -194,7 +194,28 @@ console.log({
|
|
|
194
194
|
|
|
195
195
|
Action-plan items include owner, effort, impact, confidence, score impact where available, evidence references, and verification guidance.
|
|
196
196
|
|
|
197
|
-
### 7.
|
|
197
|
+
### 7. Posture insights
|
|
198
|
+
|
|
199
|
+
Version `1.12.0+` includes a posture-insights helper for client surfaces that need display-ready risk themes and next-best actions.
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
import { analyzeUrl } from "securl";
|
|
203
|
+
import { buildPostureInsights } from "securl/posture-insights";
|
|
204
|
+
|
|
205
|
+
const result = await analyzeUrl("https://example.com");
|
|
206
|
+
const insights = buildPostureInsights(result);
|
|
207
|
+
|
|
208
|
+
console.log({
|
|
209
|
+
summary: insights.summary,
|
|
210
|
+
themes: insights.themes,
|
|
211
|
+
topInsights: insights.topInsights,
|
|
212
|
+
nextBestActions: insights.nextBestActions,
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Posture insights are derived from the action plan, so clients can render security judgement without reinterpreting raw findings, score drivers, exposure details, or vendor context.
|
|
217
|
+
|
|
218
|
+
### 8. Live certificate checks
|
|
198
219
|
|
|
199
220
|
Version `1.9.0+` includes a lightweight certificate helper for Cert Watch-style clients that only need the currently served TLS certificate.
|
|
200
221
|
|
|
@@ -313,7 +334,7 @@ It is also used by the SecURL app from the local workspace during development.
|
|
|
313
334
|
- local package check: `npm run pack:core`
|
|
314
335
|
- CI verification: `.github/workflows/core-package-checks.yml`
|
|
315
336
|
- publish workflow: `.github/workflows/publish-core-package.yml`
|
|
316
|
-
- publish
|
|
337
|
+
- publish uses npm Trusted Publishing through GitHub Actions OIDC
|
|
317
338
|
- publish uses npm provenance (`npm publish --provenance`)
|
|
318
339
|
|
|
319
340
|
Recommended release flow:
|
|
@@ -341,6 +362,7 @@ Primary exports:
|
|
|
341
362
|
- `buildPostureRiskEventsFromSnapshots(current, previous, diff)` - classify scan changes into alert-friendly risk events.
|
|
342
363
|
- `buildPostureDigest(result)` - reduce a full scan result to a compact API/mobile-friendly digest.
|
|
343
364
|
- `buildActionPlan(result)` - turn remediation, score drivers, exposure, and vendor context into prioritized fix actions.
|
|
365
|
+
- `buildPostureInsights(result)` - summarize risk themes, top insights, and next-best actions for client surfaces.
|
|
344
366
|
- `scanLiveCertificate(url)` - perform a TLS handshake-only certificate read for lightweight cert monitoring.
|
|
345
367
|
- `buildObservationLedger(result)` - produce stable source, confidence, status, and freshness-aware posture observations.
|
|
346
368
|
- `diffObservationLedgers(current, previous)` - compare stable observations and classify their operational impact.
|
|
@@ -355,6 +377,7 @@ Package subpath exports:
|
|
|
355
377
|
- `securl/history-diff`
|
|
356
378
|
- `securl/posture-digest`
|
|
357
379
|
- `securl/action-plan`
|
|
380
|
+
- `securl/posture-insights`
|
|
358
381
|
- `securl/live-certificate`
|
|
359
382
|
- `securl/observations`
|
|
360
383
|
- `securl/observation-drift`
|
package/RELEASING.md
CHANGED
|
@@ -21,14 +21,21 @@ git diff --name-status securl-v$(node -p "require('./packages/core/package.json'
|
|
|
21
21
|
3. Run:
|
|
22
22
|
- `npm run release:core:check`
|
|
23
23
|
4. Review the dry-run tarball contents.
|
|
24
|
-
5. Confirm
|
|
24
|
+
5. Confirm npm Trusted Publishing is configured for this package:
|
|
25
|
+
- provider: GitHub Actions
|
|
26
|
+
- organization: `this-is-securl`
|
|
27
|
+
- repository: `securl`
|
|
28
|
+
- workflow filename: `publish-core-package.yml`
|
|
29
|
+
- allowed action: `npm publish`
|
|
25
30
|
|
|
26
31
|
## Release steps
|
|
27
32
|
|
|
28
33
|
1. Commit the version/changelog update.
|
|
29
34
|
2. Tag the release using `securl-v<version>`, for example `securl-v1.4.1`.
|
|
30
35
|
3. Push the tag.
|
|
31
|
-
4. Let `.github/workflows/publish-core-package.yml` publish the package.
|
|
36
|
+
4. Let `.github/workflows/publish-core-package.yml` publish the package through short-lived npm OIDC credentials.
|
|
37
|
+
|
|
38
|
+
The publish workflow verifies that the release commit is already contained in `origin/main` with `git merge-base --is-ancestor`. If a manual workflow dispatch is needed after a tag publish failure, run it from `main`; the package version comes from `packages/core/package.json`.
|
|
32
39
|
|
|
33
40
|
## Post-release
|
|
34
41
|
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export { buildObservationLedger } from "./observations.js";
|
|
|
15
15
|
export { diffObservationLedgers } from "./observationDrift.js";
|
|
16
16
|
export { DEFAULT_OBSERVATION_POLICY, evaluateObservationPolicy, validateObservationPolicy } from "./observationPolicy.js";
|
|
17
17
|
export { buildActionPlan } from "./actionPlan.js";
|
|
18
|
+
export { buildPostureInsights } from "./postureInsights.js";
|
|
18
19
|
export { scanLiveCertificate } from "./certificate.js";
|
|
19
20
|
export { buildExposureBrief } from "./exposureBrief.js";
|
|
20
21
|
export { buildVendorExposureBrief } from "./vendorExposure.js";
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { scanTls } from "./certificate.js";
|
|
|
3
3
|
import { buildActionPlan } from "./actionPlan.js";
|
|
4
4
|
import { buildCompromiseSignals, emptyCompromiseSignals } from "./compromiseSignals.js";
|
|
5
5
|
import { buildExposureBrief } from "./exposureBrief.js";
|
|
6
|
+
import { buildPostureInsights } from "./postureInsights.js";
|
|
6
7
|
import { buildVendorExposureBrief } from "./vendorExposure.js";
|
|
7
8
|
import { parseSetCookie } from "./cookie-analysis.js";
|
|
8
9
|
import { analyzeCookieHeaders } from "./cookieAnalysis.js";
|
|
@@ -519,9 +520,13 @@ async function buildLimitedResult(input, normalizedInput, failure, scanTiming) {
|
|
|
519
520
|
...resultWithBriefs,
|
|
520
521
|
actionPlan: buildActionPlan(resultWithBriefs),
|
|
521
522
|
};
|
|
522
|
-
|
|
523
|
+
const resultWithInsights = {
|
|
523
524
|
...resultWithActions,
|
|
524
|
-
|
|
525
|
+
postureInsights: buildPostureInsights(resultWithActions),
|
|
526
|
+
};
|
|
527
|
+
return {
|
|
528
|
+
...resultWithInsights,
|
|
529
|
+
observationLedger: buildObservationLedger(resultWithInsights),
|
|
525
530
|
};
|
|
526
531
|
}
|
|
527
532
|
async function enrichCoreResult(result, profile) {
|
|
@@ -962,9 +967,13 @@ function buildTimedOutEnrichmentResult(result, pageAnalysisEnabled, timeoutMs, c
|
|
|
962
967
|
...resultWithBriefs,
|
|
963
968
|
actionPlan: buildActionPlan(resultWithBriefs),
|
|
964
969
|
};
|
|
965
|
-
|
|
970
|
+
const resultWithInsights = {
|
|
966
971
|
...resultWithActions,
|
|
967
|
-
|
|
972
|
+
postureInsights: buildPostureInsights(resultWithActions),
|
|
973
|
+
};
|
|
974
|
+
return {
|
|
975
|
+
...resultWithInsights,
|
|
976
|
+
observationLedger: buildObservationLedger(resultWithInsights),
|
|
968
977
|
};
|
|
969
978
|
}
|
|
970
979
|
export async function analyzeUrl(input, options = {}) {
|
|
@@ -1044,9 +1053,13 @@ export async function analyzeUrl(input, options = {}) {
|
|
|
1044
1053
|
...resultWithBriefs,
|
|
1045
1054
|
actionPlan: buildActionPlan(resultWithBriefs),
|
|
1046
1055
|
};
|
|
1047
|
-
|
|
1056
|
+
const resultWithInsights = {
|
|
1048
1057
|
...resultWithActions,
|
|
1049
|
-
|
|
1058
|
+
postureInsights: buildPostureInsights(resultWithActions),
|
|
1059
|
+
};
|
|
1060
|
+
return {
|
|
1061
|
+
...resultWithInsights,
|
|
1062
|
+
observationLedger: buildObservationLedger(resultWithInsights),
|
|
1050
1063
|
};
|
|
1051
1064
|
}
|
|
1052
1065
|
export const analyzeTarget = analyzeUrl;
|
|
@@ -1056,6 +1069,7 @@ export { buildObservationLedger } from "./observations.js";
|
|
|
1056
1069
|
export { diffObservationLedgers } from "./observationDrift.js";
|
|
1057
1070
|
export { DEFAULT_OBSERVATION_POLICY, evaluateObservationPolicy, validateObservationPolicy } from "./observationPolicy.js";
|
|
1058
1071
|
export { buildActionPlan } from "./actionPlan.js";
|
|
1072
|
+
export { buildPostureInsights } from "./postureInsights.js";
|
|
1059
1073
|
export { scanLiveCertificate } from "./certificate.js";
|
|
1060
1074
|
export { buildExposureBrief } from "./exposureBrief.js";
|
|
1061
1075
|
export { buildVendorExposureBrief } from "./vendorExposure.js";
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { buildActionPlan } from "./actionPlan.js";
|
|
2
|
+
const SEVERITY_RANK = {
|
|
3
|
+
critical: 3,
|
|
4
|
+
warning: 2,
|
|
5
|
+
info: 1,
|
|
6
|
+
};
|
|
7
|
+
const THEME_LABELS = {
|
|
8
|
+
browser_hardening: "Browser hardening",
|
|
9
|
+
transport: "Transport security",
|
|
10
|
+
domain_trust: "Domain trust",
|
|
11
|
+
public_exposure: "Public exposure",
|
|
12
|
+
vendor_risk: "Vendor risk",
|
|
13
|
+
identity: "Identity surface",
|
|
14
|
+
availability: "Availability",
|
|
15
|
+
monitoring: "Monitoring",
|
|
16
|
+
};
|
|
17
|
+
function severityForAction(item) {
|
|
18
|
+
if (item.impact === "high" || (item.scoreImpact ?? 0) >= 10) {
|
|
19
|
+
return "critical";
|
|
20
|
+
}
|
|
21
|
+
if (item.impact === "medium" || (item.scoreImpact ?? 0) >= 4) {
|
|
22
|
+
return "warning";
|
|
23
|
+
}
|
|
24
|
+
return "info";
|
|
25
|
+
}
|
|
26
|
+
function highestSeverity(left, right) {
|
|
27
|
+
return SEVERITY_RANK[right] > SEVERITY_RANK[left] ? right : left;
|
|
28
|
+
}
|
|
29
|
+
function buildSummary(analysis, insights) {
|
|
30
|
+
if (analysis.assessmentLimitation?.limited) {
|
|
31
|
+
return "The scan was limited, so restore complete scan coverage before treating the posture read as final.";
|
|
32
|
+
}
|
|
33
|
+
if (!insights.length) {
|
|
34
|
+
return "No immediate posture action stands out from the passive evidence. Keep monitoring and rescan after meaningful changes.";
|
|
35
|
+
}
|
|
36
|
+
const critical = insights.filter((insight) => insight.severity === "critical").length;
|
|
37
|
+
if (critical > 0) {
|
|
38
|
+
return `${critical} critical insight${critical === 1 ? "" : "s"} should be reviewed first because ${critical === 1 ? "it carries" : "they carry"} the highest posture impact.`;
|
|
39
|
+
}
|
|
40
|
+
return `${insights.length} actionable insight${insights.length === 1 ? "" : "s"} ${insights.length === 1 ? "is" : "are"} available for follow-up.`;
|
|
41
|
+
}
|
|
42
|
+
function buildThemeSummaries(items) {
|
|
43
|
+
const themes = new Map();
|
|
44
|
+
for (const item of items) {
|
|
45
|
+
const severity = severityForAction(item);
|
|
46
|
+
const existing = themes.get(item.theme);
|
|
47
|
+
if (!existing) {
|
|
48
|
+
themes.set(item.theme, {
|
|
49
|
+
theme: item.theme,
|
|
50
|
+
label: THEME_LABELS[item.theme],
|
|
51
|
+
count: 1,
|
|
52
|
+
highestSeverity: severity,
|
|
53
|
+
highImpactActions: item.impact === "high" ? 1 : 0,
|
|
54
|
+
quickWins: item.effort === "low" ? 1 : 0,
|
|
55
|
+
owners: [item.owner],
|
|
56
|
+
scoreImpact: item.scoreImpact ?? 0,
|
|
57
|
+
});
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
existing.count += 1;
|
|
61
|
+
existing.highestSeverity = highestSeverity(existing.highestSeverity, severity);
|
|
62
|
+
existing.highImpactActions += item.impact === "high" ? 1 : 0;
|
|
63
|
+
existing.quickWins += item.effort === "low" ? 1 : 0;
|
|
64
|
+
existing.scoreImpact += item.scoreImpact ?? 0;
|
|
65
|
+
if (!existing.owners.includes(item.owner)) {
|
|
66
|
+
existing.owners.push(item.owner);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return [...themes.values()].sort((left, right) => {
|
|
70
|
+
const severityDelta = SEVERITY_RANK[right.highestSeverity] - SEVERITY_RANK[left.highestSeverity];
|
|
71
|
+
if (severityDelta !== 0)
|
|
72
|
+
return severityDelta;
|
|
73
|
+
const impactDelta = right.highImpactActions - left.highImpactActions;
|
|
74
|
+
if (impactDelta !== 0)
|
|
75
|
+
return impactDelta;
|
|
76
|
+
return right.scoreImpact - left.scoreImpact;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function toInsight(item) {
|
|
80
|
+
return {
|
|
81
|
+
id: `insight:${item.id}`,
|
|
82
|
+
title: item.title,
|
|
83
|
+
summary: item.whyNow,
|
|
84
|
+
severity: severityForAction(item),
|
|
85
|
+
theme: item.theme,
|
|
86
|
+
owner: item.owner,
|
|
87
|
+
effort: item.effort,
|
|
88
|
+
impact: item.impact,
|
|
89
|
+
confidence: item.confidence,
|
|
90
|
+
scoreImpact: item.scoreImpact,
|
|
91
|
+
nextAction: item.action,
|
|
92
|
+
verify: item.verify,
|
|
93
|
+
evidence: item.evidence,
|
|
94
|
+
relatedFindings: item.relatedFindings,
|
|
95
|
+
source: item.source,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function toNextBestAction(item) {
|
|
99
|
+
return {
|
|
100
|
+
id: item.id,
|
|
101
|
+
label: item.action,
|
|
102
|
+
theme: item.theme,
|
|
103
|
+
owner: item.owner,
|
|
104
|
+
effort: item.effort,
|
|
105
|
+
impact: item.impact,
|
|
106
|
+
severity: severityForAction(item),
|
|
107
|
+
verify: item.verify,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
export function buildPostureInsights(analysis) {
|
|
111
|
+
const actionPlan = analysis.actionPlan ?? buildActionPlan(analysis);
|
|
112
|
+
const topItems = actionPlan.items.slice(0, 6);
|
|
113
|
+
const topInsights = topItems.map(toInsight);
|
|
114
|
+
const ownerOrder = ["edge", "app", "dns", "identity", "third_party"];
|
|
115
|
+
return {
|
|
116
|
+
generatedAt: new Date().toISOString(),
|
|
117
|
+
summary: buildSummary(analysis, topInsights),
|
|
118
|
+
posture: actionPlan.posture,
|
|
119
|
+
themes: buildThemeSummaries(actionPlan.items).map((theme) => ({
|
|
120
|
+
...theme,
|
|
121
|
+
owners: [...theme.owners].sort((left, right) => ownerOrder.indexOf(left) - ownerOrder.indexOf(right)),
|
|
122
|
+
})),
|
|
123
|
+
topInsights,
|
|
124
|
+
nextBestActions: topItems.slice(0, 3).map(toNextBestAction),
|
|
125
|
+
limitation: actionPlan.limitation,
|
|
126
|
+
};
|
|
127
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -274,6 +274,53 @@ export interface ActionPlan {
|
|
|
274
274
|
nextReview: string;
|
|
275
275
|
limitation: AssessmentLimitation | null;
|
|
276
276
|
}
|
|
277
|
+
export type PostureInsightSeverity = "info" | "warning" | "critical";
|
|
278
|
+
export interface PostureInsightThemeSummary {
|
|
279
|
+
theme: ActionPlanTheme;
|
|
280
|
+
label: string;
|
|
281
|
+
count: number;
|
|
282
|
+
highestSeverity: PostureInsightSeverity;
|
|
283
|
+
highImpactActions: number;
|
|
284
|
+
quickWins: number;
|
|
285
|
+
owners: RemediationOwner[];
|
|
286
|
+
scoreImpact: number;
|
|
287
|
+
}
|
|
288
|
+
export interface PostureInsightItem {
|
|
289
|
+
id: string;
|
|
290
|
+
title: string;
|
|
291
|
+
summary: string;
|
|
292
|
+
severity: PostureInsightSeverity;
|
|
293
|
+
theme: ActionPlanTheme;
|
|
294
|
+
owner: RemediationOwner;
|
|
295
|
+
effort: RemediationEffort;
|
|
296
|
+
impact: RemediationImpact;
|
|
297
|
+
confidence: IssueConfidence;
|
|
298
|
+
scoreImpact: number | null;
|
|
299
|
+
nextAction: string;
|
|
300
|
+
verify: string;
|
|
301
|
+
evidence: ScanEvidenceReference[];
|
|
302
|
+
relatedFindings: string[];
|
|
303
|
+
source: ActionPlanItem["source"];
|
|
304
|
+
}
|
|
305
|
+
export interface PostureInsightAction {
|
|
306
|
+
id: string;
|
|
307
|
+
label: string;
|
|
308
|
+
theme: ActionPlanTheme;
|
|
309
|
+
owner: RemediationOwner;
|
|
310
|
+
effort: RemediationEffort;
|
|
311
|
+
impact: RemediationImpact;
|
|
312
|
+
severity: PostureInsightSeverity;
|
|
313
|
+
verify: string;
|
|
314
|
+
}
|
|
315
|
+
export interface PostureInsights {
|
|
316
|
+
generatedAt: string;
|
|
317
|
+
summary: string;
|
|
318
|
+
posture: ActionPlan["posture"];
|
|
319
|
+
themes: PostureInsightThemeSummary[];
|
|
320
|
+
topInsights: PostureInsightItem[];
|
|
321
|
+
nextBestActions: PostureInsightAction[];
|
|
322
|
+
limitation: AssessmentLimitation | null;
|
|
323
|
+
}
|
|
277
324
|
export interface CrawlPageResult {
|
|
278
325
|
label: string;
|
|
279
326
|
path: string;
|
|
@@ -964,6 +1011,7 @@ export interface AnalysisResult {
|
|
|
964
1011
|
exposureBrief?: ExposureBrief;
|
|
965
1012
|
vendorExposure?: VendorExposureBrief;
|
|
966
1013
|
actionPlan?: ActionPlan;
|
|
1014
|
+
postureInsights?: PostureInsights;
|
|
967
1015
|
crawl: CrawlSummary;
|
|
968
1016
|
securityTxt: SecurityTxtInfo;
|
|
969
1017
|
domainSecurity: DomainSecurityInfo;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "securl",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Passive external security posture scanner for public URLs and web services.",
|
|
6
6
|
"author": {
|
|
@@ -78,6 +78,10 @@
|
|
|
78
78
|
"types": "./dist/actionPlan.d.ts",
|
|
79
79
|
"default": "./dist/actionPlan.js"
|
|
80
80
|
},
|
|
81
|
+
"./posture-insights": {
|
|
82
|
+
"types": "./dist/postureInsights.d.ts",
|
|
83
|
+
"default": "./dist/postureInsights.js"
|
|
84
|
+
},
|
|
81
85
|
"./live-certificate": {
|
|
82
86
|
"types": "./dist/certificate.d.ts",
|
|
83
87
|
"default": "./dist/certificate.js"
|
|
@@ -129,6 +133,6 @@
|
|
|
129
133
|
],
|
|
130
134
|
"license": "MIT",
|
|
131
135
|
"dependencies": {
|
|
132
|
-
"node-html-parser": "^
|
|
136
|
+
"node-html-parser": "^8.0.3"
|
|
133
137
|
}
|
|
134
138
|
}
|