securl 1.11.1 → 1.13.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 +12 -0
- package/README.md +44 -2
- package/RELEASING.md +2 -0
- package/dist/evidenceQuality.d.ts +2 -0
- package/dist/evidenceQuality.js +154 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +43 -15
- package/dist/postureDigest.d.ts +29 -1
- package/dist/postureDigest.js +13 -0
- package/dist/postureInsights.d.ts +2 -0
- package/dist/postureInsights.js +127 -0
- package/dist/types.d.ts +86 -0
- package/package.json +9 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,18 @@ The format is based on Keep a Changelog and this package follows Semantic Versio
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.13.0] - 2026-06-28
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Added `buildEvidenceQualitySummary()` and the `securl/evidence-quality` package export for scan confidence, coverage gaps, and recommended follow-up.
|
|
13
|
+
- Added `evidenceQuality` to completed analysis results and compact posture digests so API, mobile, and CLI clients can explain how trustworthy a scan read is.
|
|
14
|
+
|
|
15
|
+
## [1.12.0] - 2026-06-27
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- Added `buildPostureInsights()` and the `securl/posture-insights` package export for display-ready risk themes, top insights, and next-best actions.
|
|
19
|
+
- Added `postureInsights` to completed analysis results so API, mobile, and CLI clients can render security judgement without reinterpreting raw findings.
|
|
20
|
+
|
|
9
21
|
## [1.11.1] - 2026-06-24
|
|
10
22
|
|
|
11
23
|
### Changed
|
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
|
|
|
@@ -266,6 +287,23 @@ console.log({
|
|
|
266
287
|
});
|
|
267
288
|
```
|
|
268
289
|
|
|
290
|
+
Version `1.13.0+` includes an evidence-quality helper for client surfaces that need to explain how much confidence to place in a scan result.
|
|
291
|
+
|
|
292
|
+
```js
|
|
293
|
+
import { buildEvidenceQualitySummary } from "securl/evidence-quality";
|
|
294
|
+
|
|
295
|
+
const quality = buildEvidenceQualitySummary(resultWithEvidence);
|
|
296
|
+
|
|
297
|
+
console.log({
|
|
298
|
+
level: quality.level,
|
|
299
|
+
score: quality.score,
|
|
300
|
+
gaps: quality.gaps,
|
|
301
|
+
followUp: quality.recommendedFollowUp,
|
|
302
|
+
});
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Evidence quality is also included in `buildPostureDigest()` output, so mobile and API clients can display scan confidence without loading the full result.
|
|
306
|
+
|
|
269
307
|
## Package trust and release signals
|
|
270
308
|
|
|
271
309
|
- public source repository with package code under `packages/core`
|
|
@@ -313,7 +351,7 @@ It is also used by the SecURL app from the local workspace during development.
|
|
|
313
351
|
- local package check: `npm run pack:core`
|
|
314
352
|
- CI verification: `.github/workflows/core-package-checks.yml`
|
|
315
353
|
- publish workflow: `.github/workflows/publish-core-package.yml`
|
|
316
|
-
- publish
|
|
354
|
+
- publish uses npm Trusted Publishing through GitHub Actions OIDC
|
|
317
355
|
- publish uses npm provenance (`npm publish --provenance`)
|
|
318
356
|
|
|
319
357
|
Recommended release flow:
|
|
@@ -341,6 +379,7 @@ Primary exports:
|
|
|
341
379
|
- `buildPostureRiskEventsFromSnapshots(current, previous, diff)` - classify scan changes into alert-friendly risk events.
|
|
342
380
|
- `buildPostureDigest(result)` - reduce a full scan result to a compact API/mobile-friendly digest.
|
|
343
381
|
- `buildActionPlan(result)` - turn remediation, score drivers, exposure, and vendor context into prioritized fix actions.
|
|
382
|
+
- `buildPostureInsights(result)` - summarize risk themes, top insights, and next-best actions for client surfaces.
|
|
344
383
|
- `scanLiveCertificate(url)` - perform a TLS handshake-only certificate read for lightweight cert monitoring.
|
|
345
384
|
- `buildObservationLedger(result)` - produce stable source, confidence, status, and freshness-aware posture observations.
|
|
346
385
|
- `diffObservationLedgers(current, previous)` - compare stable observations and classify their operational impact.
|
|
@@ -349,18 +388,21 @@ Primary exports:
|
|
|
349
388
|
- `buildPostureRemediationPlan(result)` - generate prioritized, owner-aware remediation actions from findings and score drivers.
|
|
350
389
|
- `attachIssueEvidence(result)` - add structured evidence references to findings without changing their existing fields.
|
|
351
390
|
- `buildPostureEvidenceSummary(result)` - produce compact evidence metadata for API, mobile, report, and explainability surfaces.
|
|
391
|
+
- `buildEvidenceQualitySummary(result)` - summarize scan confidence, collection gaps, and recommended follow-up.
|
|
352
392
|
|
|
353
393
|
Package subpath exports:
|
|
354
394
|
|
|
355
395
|
- `securl/history-diff`
|
|
356
396
|
- `securl/posture-digest`
|
|
357
397
|
- `securl/action-plan`
|
|
398
|
+
- `securl/posture-insights`
|
|
358
399
|
- `securl/live-certificate`
|
|
359
400
|
- `securl/observations`
|
|
360
401
|
- `securl/observation-drift`
|
|
361
402
|
- `securl/observation-policy`
|
|
362
403
|
- `securl/posture-drift`
|
|
363
404
|
- `securl/remediation-plan`
|
|
405
|
+
- `securl/evidence-quality`
|
|
364
406
|
- `securl/risk-events`
|
|
365
407
|
- `securl/types`
|
|
366
408
|
|
package/RELEASING.md
CHANGED
|
@@ -35,6 +35,8 @@ git diff --name-status securl-v$(node -p "require('./packages/core/package.json'
|
|
|
35
35
|
3. Push the tag.
|
|
36
36
|
4. Let `.github/workflows/publish-core-package.yml` publish the package through short-lived npm OIDC credentials.
|
|
37
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`.
|
|
39
|
+
|
|
38
40
|
## Post-release
|
|
39
41
|
|
|
40
42
|
1. Confirm the package is available on npm.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
const OBSERVED_KINDS = new Set(["header", "tls", "cookie", "redirect", "dns", "html", "public_record"]);
|
|
2
|
+
const normalizeArray = (value) => (Array.isArray(value) ? value : []);
|
|
3
|
+
function clampScore(score) {
|
|
4
|
+
return Math.max(0, Math.min(100, Math.round(score)));
|
|
5
|
+
}
|
|
6
|
+
function levelForScore(score) {
|
|
7
|
+
if (score >= 80)
|
|
8
|
+
return "high";
|
|
9
|
+
if (score >= 55)
|
|
10
|
+
return "medium";
|
|
11
|
+
return "low";
|
|
12
|
+
}
|
|
13
|
+
function pushSignal(signals, id, label, detail, impact) {
|
|
14
|
+
signals.push({ id, label, detail, impact });
|
|
15
|
+
}
|
|
16
|
+
function buildSummary(level, analysis, gaps) {
|
|
17
|
+
if (analysis.assessmentLimitation?.limited) {
|
|
18
|
+
return "Evidence quality is limited because the target did not return a complete posture read.";
|
|
19
|
+
}
|
|
20
|
+
if (level === "high") {
|
|
21
|
+
return "Evidence quality is high enough to treat the posture result as a solid outside-in read.";
|
|
22
|
+
}
|
|
23
|
+
if (level === "medium") {
|
|
24
|
+
return "Evidence quality is usable, but a few collection gaps should be considered before treating the result as final.";
|
|
25
|
+
}
|
|
26
|
+
if (gaps.length > 0) {
|
|
27
|
+
return "Evidence quality is low, so treat this result as directional until the collection gaps are resolved.";
|
|
28
|
+
}
|
|
29
|
+
return "Evidence quality is low because the scan produced too little structured support for the result.";
|
|
30
|
+
}
|
|
31
|
+
export function buildEvidenceQualitySummary(analysis) {
|
|
32
|
+
const evidenceSummary = analysis.evidenceSummary;
|
|
33
|
+
const issues = normalizeArray(analysis.issues);
|
|
34
|
+
const timing = analysis.scanTiming;
|
|
35
|
+
const totalReferences = evidenceSummary?.totalEvidenceReferences ?? 0;
|
|
36
|
+
const observedReferences = evidenceSummary?.observedCount ?? 0;
|
|
37
|
+
const derivedReferences = evidenceSummary?.derivedCount ?? 0;
|
|
38
|
+
const observedRatio = totalReferences > 0 ? observedReferences / totalReferences : 0;
|
|
39
|
+
const kinds = Object.keys(evidenceSummary?.byKind ?? {});
|
|
40
|
+
const lowConfidence = issues.filter((issue) => issue.confidence === "low").length;
|
|
41
|
+
const mediumConfidence = issues.filter((issue) => issue.confidence === "medium").length;
|
|
42
|
+
const highConfidence = issues.filter((issue) => issue.confidence === "high").length;
|
|
43
|
+
const strengths = [];
|
|
44
|
+
const gaps = [];
|
|
45
|
+
let score = 50;
|
|
46
|
+
if (totalReferences >= 12) {
|
|
47
|
+
score += 18;
|
|
48
|
+
pushSignal(strengths, "evidence_volume", "Evidence volume", "The scan produced a broad set of structured evidence references.", "positive");
|
|
49
|
+
}
|
|
50
|
+
else if (totalReferences >= 5) {
|
|
51
|
+
score += 10;
|
|
52
|
+
pushSignal(strengths, "evidence_volume", "Evidence volume", "The scan produced enough structured evidence for a useful posture read.", "positive");
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
score -= 18;
|
|
56
|
+
pushSignal(gaps, "low_evidence_volume", "Low evidence volume", "The scan produced fewer structured evidence references than expected.", "negative");
|
|
57
|
+
}
|
|
58
|
+
if (observedRatio >= 0.7) {
|
|
59
|
+
score += 16;
|
|
60
|
+
pushSignal(strengths, "observed_evidence", "Observed evidence", "Most evidence came from directly observed target data rather than derived context.", "positive");
|
|
61
|
+
}
|
|
62
|
+
else if (observedRatio >= 0.45) {
|
|
63
|
+
score += 6;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
score -= 14;
|
|
67
|
+
pushSignal(gaps, "derived_heavy", "Derived-heavy evidence", "The result relies heavily on derived evidence, so direct verification is thinner.", "negative");
|
|
68
|
+
}
|
|
69
|
+
if (kinds.length >= 5) {
|
|
70
|
+
score += 12;
|
|
71
|
+
pushSignal(strengths, "evidence_breadth", "Evidence breadth", "The scan collected evidence across several posture areas.", "positive");
|
|
72
|
+
}
|
|
73
|
+
else if (kinds.length <= 2) {
|
|
74
|
+
score -= 12;
|
|
75
|
+
pushSignal(gaps, "narrow_evidence", "Narrow evidence", "Evidence came from a small number of source types.", "negative");
|
|
76
|
+
}
|
|
77
|
+
if (analysis.assessmentLimitation?.limited) {
|
|
78
|
+
score -= 35;
|
|
79
|
+
pushSignal(gaps, "limited_assessment", "Limited assessment", analysis.assessmentLimitation.detail ?? "The target response limited collection.", "negative");
|
|
80
|
+
}
|
|
81
|
+
if (timing?.timedOut) {
|
|
82
|
+
score -= 18;
|
|
83
|
+
pushSignal(gaps, "scan_timeout", "Scan timeout", "The scan timed out before all enrichment completed.", "negative");
|
|
84
|
+
}
|
|
85
|
+
if (analysis.statusCode >= 500 || analysis.statusCode === 0) {
|
|
86
|
+
score -= 14;
|
|
87
|
+
pushSignal(gaps, "availability_status", "Availability status", `The target returned HTTP ${analysis.statusCode}, reducing confidence in the observed posture.`, "negative");
|
|
88
|
+
}
|
|
89
|
+
else if (analysis.statusCode >= 200 && analysis.statusCode < 400) {
|
|
90
|
+
score += 8;
|
|
91
|
+
pushSignal(strengths, "normal_response", "Normal response", `The target returned HTTP ${analysis.statusCode}, supporting a normal posture read.`, "positive");
|
|
92
|
+
}
|
|
93
|
+
if (lowConfidence > 0) {
|
|
94
|
+
score -= Math.min(14, lowConfidence * 4);
|
|
95
|
+
pushSignal(gaps, "low_confidence_findings", "Low-confidence findings", `${lowConfidence} finding${lowConfidence === 1 ? "" : "s"} had low confidence.`, "negative");
|
|
96
|
+
}
|
|
97
|
+
if (highConfidence > 0 && highConfidence >= lowConfidence + mediumConfidence) {
|
|
98
|
+
score += 8;
|
|
99
|
+
pushSignal(strengths, "high_confidence_findings", "High-confidence findings", "Most findings were supported with high-confidence evidence.", "positive");
|
|
100
|
+
}
|
|
101
|
+
if (!normalizeArray(analysis.headers).length) {
|
|
102
|
+
score -= 10;
|
|
103
|
+
pushSignal(gaps, "missing_header_set", "Missing header set", "No response header set was available for the scan.", "negative");
|
|
104
|
+
}
|
|
105
|
+
if (!analysis.certificate?.available) {
|
|
106
|
+
score -= 8;
|
|
107
|
+
pushSignal(gaps, "certificate_unavailable", "Certificate unavailable", "The scan could not read a served TLS certificate.", "negative");
|
|
108
|
+
}
|
|
109
|
+
else if (analysis.certificate.authorized !== false) {
|
|
110
|
+
score += 6;
|
|
111
|
+
pushSignal(strengths, "certificate_observed", "Certificate observed", "A served TLS certificate was observed during the scan.", "positive");
|
|
112
|
+
}
|
|
113
|
+
const finalScore = clampScore(score);
|
|
114
|
+
const level = levelForScore(finalScore);
|
|
115
|
+
const recommendedFollowUp = [
|
|
116
|
+
...(analysis.assessmentLimitation?.limited ? ["Restore complete scan coverage and rescan before treating the grade as final."] : []),
|
|
117
|
+
...(timing?.timedOut ? ["Rerun the scan with a longer timeout or narrower mode to complete enrichment."] : []),
|
|
118
|
+
...(observedRatio < 0.45 ? ["Verify the top findings manually because direct observed evidence is thin."] : []),
|
|
119
|
+
...(kinds.length <= 2 ? ["Collect another scan after the target returns normal headers, TLS, DNS, and HTML evidence."] : []),
|
|
120
|
+
];
|
|
121
|
+
if (recommendedFollowUp.length === 0) {
|
|
122
|
+
recommendedFollowUp.push("Use the score drivers and top insights as the primary follow-up path.");
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
generatedAt: new Date().toISOString(),
|
|
126
|
+
level,
|
|
127
|
+
score: finalScore,
|
|
128
|
+
summary: buildSummary(level, analysis, gaps),
|
|
129
|
+
evidence: {
|
|
130
|
+
totalReferences,
|
|
131
|
+
observedReferences,
|
|
132
|
+
derivedReferences,
|
|
133
|
+
observedRatio: Number(observedRatio.toFixed(2)),
|
|
134
|
+
kinds: kinds.filter((kind) => OBSERVED_KINDS.has(kind) || kind === "probe" || kind === "score_driver"),
|
|
135
|
+
},
|
|
136
|
+
scan: {
|
|
137
|
+
limited: Boolean(analysis.assessmentLimitation?.limited),
|
|
138
|
+
limitedKind: analysis.assessmentLimitation?.kind ?? null,
|
|
139
|
+
timedOut: Boolean(timing?.timedOut),
|
|
140
|
+
statusCode: analysis.statusCode,
|
|
141
|
+
responseTimeMs: analysis.responseTimeMs,
|
|
142
|
+
},
|
|
143
|
+
findings: {
|
|
144
|
+
total: issues.length,
|
|
145
|
+
lowConfidence,
|
|
146
|
+
mediumConfidence,
|
|
147
|
+
highConfidence,
|
|
148
|
+
},
|
|
149
|
+
strengths: strengths.slice(0, 5),
|
|
150
|
+
gaps: gaps.slice(0, 5),
|
|
151
|
+
recommendedFollowUp: recommendedFollowUp.slice(0, 4),
|
|
152
|
+
limitation: analysis.assessmentLimitation?.limited ? analysis.assessmentLimitation : null,
|
|
153
|
+
};
|
|
154
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ 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 { buildEvidenceQualitySummary } from "./evidenceQuality.js";
|
|
19
|
+
export { buildPostureInsights } from "./postureInsights.js";
|
|
18
20
|
export { scanLiveCertificate } from "./certificate.js";
|
|
19
21
|
export { buildExposureBrief } from "./exposureBrief.js";
|
|
20
22
|
export { buildVendorExposureBrief } from "./vendorExposure.js";
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,9 @@ import { URL } from "node:url";
|
|
|
2
2
|
import { scanTls } from "./certificate.js";
|
|
3
3
|
import { buildActionPlan } from "./actionPlan.js";
|
|
4
4
|
import { buildCompromiseSignals, emptyCompromiseSignals } from "./compromiseSignals.js";
|
|
5
|
+
import { buildEvidenceQualitySummary } from "./evidenceQuality.js";
|
|
5
6
|
import { buildExposureBrief } from "./exposureBrief.js";
|
|
7
|
+
import { buildPostureInsights } from "./postureInsights.js";
|
|
6
8
|
import { buildVendorExposureBrief } from "./vendorExposure.js";
|
|
7
9
|
import { parseSetCookie } from "./cookie-analysis.js";
|
|
8
10
|
import { analyzeCookieHeaders } from "./cookieAnalysis.js";
|
|
@@ -510,18 +512,26 @@ async function buildLimitedResult(input, normalizedInput, failure, scanTiming) {
|
|
|
510
512
|
remediationPlan,
|
|
511
513
|
evidenceSummary: buildPostureEvidenceSummary({ ...evidenceResult, remediationPlan }),
|
|
512
514
|
};
|
|
513
|
-
const
|
|
515
|
+
const resultWithQuality = {
|
|
514
516
|
...resultWithRemediation,
|
|
515
|
-
|
|
516
|
-
|
|
517
|
+
evidenceQuality: buildEvidenceQualitySummary(resultWithRemediation),
|
|
518
|
+
};
|
|
519
|
+
const resultWithBriefs = {
|
|
520
|
+
...resultWithQuality,
|
|
521
|
+
exposureBrief: buildExposureBrief(resultWithQuality),
|
|
522
|
+
vendorExposure: buildVendorExposureBrief(resultWithQuality),
|
|
517
523
|
};
|
|
518
524
|
const resultWithActions = {
|
|
519
525
|
...resultWithBriefs,
|
|
520
526
|
actionPlan: buildActionPlan(resultWithBriefs),
|
|
521
527
|
};
|
|
522
|
-
|
|
528
|
+
const resultWithInsights = {
|
|
523
529
|
...resultWithActions,
|
|
524
|
-
|
|
530
|
+
postureInsights: buildPostureInsights(resultWithActions),
|
|
531
|
+
};
|
|
532
|
+
return {
|
|
533
|
+
...resultWithInsights,
|
|
534
|
+
observationLedger: buildObservationLedger(resultWithInsights),
|
|
525
535
|
};
|
|
526
536
|
}
|
|
527
537
|
async function enrichCoreResult(result, profile) {
|
|
@@ -953,18 +963,26 @@ function buildTimedOutEnrichmentResult(result, pageAnalysisEnabled, timeoutMs, c
|
|
|
953
963
|
remediationPlan,
|
|
954
964
|
evidenceSummary: buildPostureEvidenceSummary({ ...evidenceResult, remediationPlan }),
|
|
955
965
|
};
|
|
956
|
-
const
|
|
966
|
+
const resultWithQuality = {
|
|
957
967
|
...resultWithRemediation,
|
|
958
|
-
|
|
959
|
-
|
|
968
|
+
evidenceQuality: buildEvidenceQualitySummary(resultWithRemediation),
|
|
969
|
+
};
|
|
970
|
+
const resultWithBriefs = {
|
|
971
|
+
...resultWithQuality,
|
|
972
|
+
exposureBrief: buildExposureBrief(resultWithQuality),
|
|
973
|
+
vendorExposure: buildVendorExposureBrief(resultWithQuality),
|
|
960
974
|
};
|
|
961
975
|
const resultWithActions = {
|
|
962
976
|
...resultWithBriefs,
|
|
963
977
|
actionPlan: buildActionPlan(resultWithBriefs),
|
|
964
978
|
};
|
|
965
|
-
|
|
979
|
+
const resultWithInsights = {
|
|
966
980
|
...resultWithActions,
|
|
967
|
-
|
|
981
|
+
postureInsights: buildPostureInsights(resultWithActions),
|
|
982
|
+
};
|
|
983
|
+
return {
|
|
984
|
+
...resultWithInsights,
|
|
985
|
+
observationLedger: buildObservationLedger(resultWithInsights),
|
|
968
986
|
};
|
|
969
987
|
}
|
|
970
988
|
export async function analyzeUrl(input, options = {}) {
|
|
@@ -1035,18 +1053,26 @@ export async function analyzeUrl(input, options = {}) {
|
|
|
1035
1053
|
remediationPlan,
|
|
1036
1054
|
evidenceSummary: buildPostureEvidenceSummary({ ...resultWithEvidence, remediationPlan }),
|
|
1037
1055
|
};
|
|
1038
|
-
const
|
|
1056
|
+
const resultWithQuality = {
|
|
1039
1057
|
...resultWithRemediation,
|
|
1040
|
-
|
|
1041
|
-
|
|
1058
|
+
evidenceQuality: buildEvidenceQualitySummary(resultWithRemediation),
|
|
1059
|
+
};
|
|
1060
|
+
const resultWithBriefs = {
|
|
1061
|
+
...resultWithQuality,
|
|
1062
|
+
exposureBrief: buildExposureBrief(resultWithQuality),
|
|
1063
|
+
vendorExposure: buildVendorExposureBrief(resultWithQuality),
|
|
1042
1064
|
};
|
|
1043
1065
|
const resultWithActions = {
|
|
1044
1066
|
...resultWithBriefs,
|
|
1045
1067
|
actionPlan: buildActionPlan(resultWithBriefs),
|
|
1046
1068
|
};
|
|
1047
|
-
|
|
1069
|
+
const resultWithInsights = {
|
|
1048
1070
|
...resultWithActions,
|
|
1049
|
-
|
|
1071
|
+
postureInsights: buildPostureInsights(resultWithActions),
|
|
1072
|
+
};
|
|
1073
|
+
return {
|
|
1074
|
+
...resultWithInsights,
|
|
1075
|
+
observationLedger: buildObservationLedger(resultWithInsights),
|
|
1050
1076
|
};
|
|
1051
1077
|
}
|
|
1052
1078
|
export const analyzeTarget = analyzeUrl;
|
|
@@ -1056,6 +1082,8 @@ export { buildObservationLedger } from "./observations.js";
|
|
|
1056
1082
|
export { diffObservationLedgers } from "./observationDrift.js";
|
|
1057
1083
|
export { DEFAULT_OBSERVATION_POLICY, evaluateObservationPolicy, validateObservationPolicy } from "./observationPolicy.js";
|
|
1058
1084
|
export { buildActionPlan } from "./actionPlan.js";
|
|
1085
|
+
export { buildEvidenceQualitySummary } from "./evidenceQuality.js";
|
|
1086
|
+
export { buildPostureInsights } from "./postureInsights.js";
|
|
1059
1087
|
export { scanLiveCertificate } from "./certificate.js";
|
|
1060
1088
|
export { buildExposureBrief } from "./exposureBrief.js";
|
|
1061
1089
|
export { buildVendorExposureBrief } from "./vendorExposure.js";
|
package/dist/postureDigest.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export declare function buildPostureDigest(analysis: AnalysisResult, { findingLi
|
|
|
20
20
|
posture: "strong" | "weak" | "mixed";
|
|
21
21
|
takeaways: string[];
|
|
22
22
|
limited: boolean;
|
|
23
|
-
limitedKind: "
|
|
23
|
+
limitedKind: "blocked_edge_response" | "auth_required" | "rate_limited" | "service_unavailable" | "other";
|
|
24
24
|
limitation: import("./types.js").AssessmentLimitation;
|
|
25
25
|
scoreDrivers: import("./types.js").ScoreDriver[];
|
|
26
26
|
};
|
|
@@ -59,6 +59,34 @@ export declare function buildPostureDigest(analysis: AnalysisResult, { findingLi
|
|
|
59
59
|
scoreImpact: number;
|
|
60
60
|
}[];
|
|
61
61
|
};
|
|
62
|
+
evidenceQuality: {
|
|
63
|
+
level: import("./types.js").EvidenceQualityLevel;
|
|
64
|
+
score: number;
|
|
65
|
+
summary: string;
|
|
66
|
+
evidence: {
|
|
67
|
+
totalReferences: number;
|
|
68
|
+
observedReferences: number;
|
|
69
|
+
derivedReferences: number;
|
|
70
|
+
observedRatio: number;
|
|
71
|
+
kinds: import("./types.js").ScanEvidenceKind[];
|
|
72
|
+
};
|
|
73
|
+
scan: {
|
|
74
|
+
limited: boolean;
|
|
75
|
+
limitedKind: import("./types.js").AssessmentLimitation["kind"];
|
|
76
|
+
timedOut: boolean;
|
|
77
|
+
statusCode: number;
|
|
78
|
+
responseTimeMs: number;
|
|
79
|
+
};
|
|
80
|
+
findings: {
|
|
81
|
+
total: number;
|
|
82
|
+
lowConfidence: number;
|
|
83
|
+
mediumConfidence: number;
|
|
84
|
+
highConfidence: number;
|
|
85
|
+
};
|
|
86
|
+
strengths: import("./types.js").EvidenceQualitySignal[];
|
|
87
|
+
gaps: import("./types.js").EvidenceQualitySignal[];
|
|
88
|
+
recommendedFollowUp: string[];
|
|
89
|
+
};
|
|
62
90
|
remediationPlan: {
|
|
63
91
|
summary: string;
|
|
64
92
|
totalActions: number;
|
package/dist/postureDigest.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildEvidenceQualitySummary } from "./evidenceQuality.js";
|
|
1
2
|
const SEVERITY_ORDER = {
|
|
2
3
|
critical: 0,
|
|
3
4
|
warning: 1,
|
|
@@ -31,6 +32,7 @@ export function buildPostureDigest(analysis, { findingLimit = 8 } = {}) {
|
|
|
31
32
|
const issues = normalizeArray(analysis.issues);
|
|
32
33
|
const compromiseIndicators = normalizeArray(analysis.compromiseSignals?.indicators);
|
|
33
34
|
const riskIndicators = compromiseIndicators.filter((indicator) => ["warning", "critical"].includes(indicator.severity));
|
|
35
|
+
const evidenceQuality = analysis.evidenceQuality ?? buildEvidenceQualitySummary(analysis);
|
|
34
36
|
return {
|
|
35
37
|
generatedAt: new Date().toISOString(),
|
|
36
38
|
target: {
|
|
@@ -77,6 +79,17 @@ export function buildPostureDigest(analysis, { findingLimit = 8 } = {}) {
|
|
|
77
79
|
scoreImpact: reference.scoreImpact,
|
|
78
80
|
})),
|
|
79
81
|
} : null,
|
|
82
|
+
evidenceQuality: {
|
|
83
|
+
level: evidenceQuality.level,
|
|
84
|
+
score: evidenceQuality.score,
|
|
85
|
+
summary: evidenceQuality.summary,
|
|
86
|
+
evidence: evidenceQuality.evidence,
|
|
87
|
+
scan: evidenceQuality.scan,
|
|
88
|
+
findings: evidenceQuality.findings,
|
|
89
|
+
strengths: normalizeArray(evidenceQuality.strengths).slice(0, 5),
|
|
90
|
+
gaps: normalizeArray(evidenceQuality.gaps).slice(0, 5),
|
|
91
|
+
recommendedFollowUp: normalizeArray(evidenceQuality.recommendedFollowUp).slice(0, 4),
|
|
92
|
+
},
|
|
80
93
|
remediationPlan: analysis.remediationPlan ? {
|
|
81
94
|
summary: analysis.remediationPlan.summary,
|
|
82
95
|
totalActions: analysis.remediationPlan.totalActions,
|
|
@@ -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
|
@@ -172,6 +172,43 @@ export interface PostureEvidenceSummary {
|
|
|
172
172
|
findingEvidence: PostureEvidenceSummaryReference[];
|
|
173
173
|
limitation: AssessmentLimitation | null;
|
|
174
174
|
}
|
|
175
|
+
export type EvidenceQualityLevel = "high" | "medium" | "low";
|
|
176
|
+
export interface EvidenceQualitySignal {
|
|
177
|
+
id: string;
|
|
178
|
+
label: string;
|
|
179
|
+
detail: string;
|
|
180
|
+
impact: "positive" | "negative" | "neutral";
|
|
181
|
+
}
|
|
182
|
+
export interface EvidenceQualitySummary {
|
|
183
|
+
generatedAt: string;
|
|
184
|
+
level: EvidenceQualityLevel;
|
|
185
|
+
score: number;
|
|
186
|
+
summary: string;
|
|
187
|
+
evidence: {
|
|
188
|
+
totalReferences: number;
|
|
189
|
+
observedReferences: number;
|
|
190
|
+
derivedReferences: number;
|
|
191
|
+
observedRatio: number;
|
|
192
|
+
kinds: ScanEvidenceKind[];
|
|
193
|
+
};
|
|
194
|
+
scan: {
|
|
195
|
+
limited: boolean;
|
|
196
|
+
limitedKind: AssessmentLimitation["kind"];
|
|
197
|
+
timedOut: boolean;
|
|
198
|
+
statusCode: number;
|
|
199
|
+
responseTimeMs: number;
|
|
200
|
+
};
|
|
201
|
+
findings: {
|
|
202
|
+
total: number;
|
|
203
|
+
lowConfidence: number;
|
|
204
|
+
mediumConfidence: number;
|
|
205
|
+
highConfidence: number;
|
|
206
|
+
};
|
|
207
|
+
strengths: EvidenceQualitySignal[];
|
|
208
|
+
gaps: EvidenceQualitySignal[];
|
|
209
|
+
recommendedFollowUp: string[];
|
|
210
|
+
limitation: AssessmentLimitation | null;
|
|
211
|
+
}
|
|
175
212
|
export type ExposureBriefLevel = "low" | "medium" | "high" | "critical" | "unknown";
|
|
176
213
|
export type ExposureBriefCategory = "entry_point" | "trust_gap" | "abuse_signal" | "sensitive_exposure" | "third_party" | "identity" | "ai" | "infrastructure";
|
|
177
214
|
export type ExposureBriefSource = "headers" | "tls" | "cookies" | "dns" | "html" | "public_record" | "third_party" | "ai" | "ct" | "api" | "exposure" | "derived";
|
|
@@ -274,6 +311,53 @@ export interface ActionPlan {
|
|
|
274
311
|
nextReview: string;
|
|
275
312
|
limitation: AssessmentLimitation | null;
|
|
276
313
|
}
|
|
314
|
+
export type PostureInsightSeverity = "info" | "warning" | "critical";
|
|
315
|
+
export interface PostureInsightThemeSummary {
|
|
316
|
+
theme: ActionPlanTheme;
|
|
317
|
+
label: string;
|
|
318
|
+
count: number;
|
|
319
|
+
highestSeverity: PostureInsightSeverity;
|
|
320
|
+
highImpactActions: number;
|
|
321
|
+
quickWins: number;
|
|
322
|
+
owners: RemediationOwner[];
|
|
323
|
+
scoreImpact: number;
|
|
324
|
+
}
|
|
325
|
+
export interface PostureInsightItem {
|
|
326
|
+
id: string;
|
|
327
|
+
title: string;
|
|
328
|
+
summary: string;
|
|
329
|
+
severity: PostureInsightSeverity;
|
|
330
|
+
theme: ActionPlanTheme;
|
|
331
|
+
owner: RemediationOwner;
|
|
332
|
+
effort: RemediationEffort;
|
|
333
|
+
impact: RemediationImpact;
|
|
334
|
+
confidence: IssueConfidence;
|
|
335
|
+
scoreImpact: number | null;
|
|
336
|
+
nextAction: string;
|
|
337
|
+
verify: string;
|
|
338
|
+
evidence: ScanEvidenceReference[];
|
|
339
|
+
relatedFindings: string[];
|
|
340
|
+
source: ActionPlanItem["source"];
|
|
341
|
+
}
|
|
342
|
+
export interface PostureInsightAction {
|
|
343
|
+
id: string;
|
|
344
|
+
label: string;
|
|
345
|
+
theme: ActionPlanTheme;
|
|
346
|
+
owner: RemediationOwner;
|
|
347
|
+
effort: RemediationEffort;
|
|
348
|
+
impact: RemediationImpact;
|
|
349
|
+
severity: PostureInsightSeverity;
|
|
350
|
+
verify: string;
|
|
351
|
+
}
|
|
352
|
+
export interface PostureInsights {
|
|
353
|
+
generatedAt: string;
|
|
354
|
+
summary: string;
|
|
355
|
+
posture: ActionPlan["posture"];
|
|
356
|
+
themes: PostureInsightThemeSummary[];
|
|
357
|
+
topInsights: PostureInsightItem[];
|
|
358
|
+
nextBestActions: PostureInsightAction[];
|
|
359
|
+
limitation: AssessmentLimitation | null;
|
|
360
|
+
}
|
|
277
361
|
export interface CrawlPageResult {
|
|
278
362
|
label: string;
|
|
279
363
|
path: string;
|
|
@@ -961,9 +1045,11 @@ export interface AnalysisResult {
|
|
|
961
1045
|
remediation: RemediationSnippet[];
|
|
962
1046
|
remediationPlan?: RemediationPlan;
|
|
963
1047
|
evidenceSummary?: PostureEvidenceSummary;
|
|
1048
|
+
evidenceQuality?: EvidenceQualitySummary;
|
|
964
1049
|
exposureBrief?: ExposureBrief;
|
|
965
1050
|
vendorExposure?: VendorExposureBrief;
|
|
966
1051
|
actionPlan?: ActionPlan;
|
|
1052
|
+
postureInsights?: PostureInsights;
|
|
967
1053
|
crawl: CrawlSummary;
|
|
968
1054
|
securityTxt: SecurityTxtInfo;
|
|
969
1055
|
domainSecurity: DomainSecurityInfo;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "securl",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Passive external security posture scanner for public URLs and web services.",
|
|
6
6
|
"author": {
|
|
@@ -66,6 +66,10 @@
|
|
|
66
66
|
"types": "./dist/postureRemediation.d.ts",
|
|
67
67
|
"default": "./dist/postureRemediation.js"
|
|
68
68
|
},
|
|
69
|
+
"./evidence-quality": {
|
|
70
|
+
"types": "./dist/evidenceQuality.d.ts",
|
|
71
|
+
"default": "./dist/evidenceQuality.js"
|
|
72
|
+
},
|
|
69
73
|
"./exposure-brief": {
|
|
70
74
|
"types": "./dist/exposureBrief.d.ts",
|
|
71
75
|
"default": "./dist/exposureBrief.js"
|
|
@@ -78,6 +82,10 @@
|
|
|
78
82
|
"types": "./dist/actionPlan.d.ts",
|
|
79
83
|
"default": "./dist/actionPlan.js"
|
|
80
84
|
},
|
|
85
|
+
"./posture-insights": {
|
|
86
|
+
"types": "./dist/postureInsights.d.ts",
|
|
87
|
+
"default": "./dist/postureInsights.js"
|
|
88
|
+
},
|
|
81
89
|
"./live-certificate": {
|
|
82
90
|
"types": "./dist/certificate.d.ts",
|
|
83
91
|
"default": "./dist/certificate.js"
|