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 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: buildPostureRemediationPlan(evidenceResult),
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: buildPostureRemediationPlan(evidenceResult),
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: buildPostureRemediationPlan(resultWithEvidence),
1005
+ remediationPlan,
1006
+ evidenceSummary: buildPostureEvidenceSummary({ ...resultWithEvidence, remediationPlan }),
1001
1007
  };
1002
1008
  }
1003
1009
  export const analyzeTarget = analyzeUrl;
@@ -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;
@@ -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.4.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": [