securl 1.4.1 → 1.5.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 +7 -0
- package/README.md +15 -0
- 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 +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,13 @@ The format is based on Keep a Changelog and this package follows Semantic Versio
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.5.0] - 2026-06-14
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Added `buildPostureEvidenceSummary()` for compact, API/mobile-friendly evidence metadata that explains score drivers and findings without requiring clients to inspect the full scan payload.
|
|
13
|
+
- Added the `securl/evidence-summary` package export for consumers that want the evidence summary helper directly.
|
|
14
|
+
- Added `evidenceSummary` to completed analysis results and posture digests.
|
|
15
|
+
|
|
9
16
|
## [1.4.1] - 2026-06-14
|
|
10
17
|
|
|
11
18
|
### Changed
|
package/README.md
CHANGED
|
@@ -193,6 +193,20 @@ console.log(remediationPlan.items.map((item) => ({
|
|
|
193
193
|
})));
|
|
194
194
|
```
|
|
195
195
|
|
|
196
|
+
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.
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
import { buildPostureEvidenceSummary } from "securl/evidence-summary";
|
|
200
|
+
|
|
201
|
+
const evidenceSummary = buildPostureEvidenceSummary(resultWithEvidence);
|
|
202
|
+
|
|
203
|
+
console.log({
|
|
204
|
+
total: evidenceSummary.totalEvidenceReferences,
|
|
205
|
+
observed: evidenceSummary.observedCount,
|
|
206
|
+
topEvidence: evidenceSummary.topEvidence,
|
|
207
|
+
});
|
|
208
|
+
```
|
|
209
|
+
|
|
196
210
|
## Package trust and release signals
|
|
197
211
|
|
|
198
212
|
- public source repository with package code under `packages/core`
|
|
@@ -270,6 +284,7 @@ Primary exports:
|
|
|
270
284
|
- `buildPostureDriftReportFromSnapshots(current, previous)` - produce a complete scan-to-scan drift report for monitoring, alerting, and history views.
|
|
271
285
|
- `buildPostureRemediationPlan(result)` - generate prioritized, owner-aware remediation actions from findings and score drivers.
|
|
272
286
|
- `attachIssueEvidence(result)` - add structured evidence references to findings without changing their existing fields.
|
|
287
|
+
- `buildPostureEvidenceSummary(result)` - produce compact evidence metadata for API, mobile, report, and explainability surfaces.
|
|
273
288
|
|
|
274
289
|
Package subpath exports:
|
|
275
290
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "securl",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Passive external security posture analysis engine for SecURL.",
|
|
6
6
|
"author": {
|
|
@@ -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": [
|