@secureport/core 1.0.0 → 2.2.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/README.md +2 -1
- package/dist/finding.d.ts +9 -8
- package/dist/finding.d.ts.map +1 -1
- package/dist/fingerprint.d.ts +12 -5
- package/dist/fingerprint.d.ts.map +1 -1
- package/dist/fingerprint.js +74 -8
- package/dist/fingerprint.js.map +1 -1
- package/dist/import/nessus.d.ts +9 -6
- package/dist/import/nessus.d.ts.map +1 -1
- package/dist/import/nessus.js +9 -6
- package/dist/import/nessus.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/issue.d.ts +1 -1
- package/dist/issue.d.ts.map +1 -1
- package/dist/reconcile.d.ts +71 -1
- package/dist/reconcile.d.ts.map +1 -1
- package/dist/reconcile.js +64 -4
- package/dist/reconcile.js.map +1 -1
- package/dist/refingerprint.d.ts +116 -0
- package/dist/refingerprint.d.ts.map +1 -0
- package/dist/refingerprint.js +190 -0
- package/dist/refingerprint.js.map +1 -0
- package/dist/report/anchors.d.ts +39 -0
- package/dist/report/anchors.d.ts.map +1 -0
- package/dist/report/anchors.js +73 -0
- package/dist/report/anchors.js.map +1 -0
- package/dist/report/html.d.ts.map +1 -1
- package/dist/report/html.js +126 -22
- package/dist/report/html.js.map +1 -1
- package/dist/report/markdown.d.ts.map +1 -1
- package/dist/report/markdown.js +112 -26
- package/dist/report/markdown.js.map +1 -1
- package/dist/report/model.d.ts +227 -1
- package/dist/report/model.d.ts.map +1 -1
- package/dist/report/model.js +87 -8
- package/dist/report/model.js.map +1 -1
- package/package.json +1 -1
- package/src/finding.ts +9 -8
- package/src/fingerprint.ts +76 -10
- package/src/import/nessus.ts +9 -6
- package/src/index.ts +14 -2
- package/src/issue.ts +1 -0
- package/src/reconcile.ts +121 -5
- package/src/refingerprint.ts +282 -0
- package/src/report/anchors.ts +96 -0
- package/src/report/html.ts +146 -26
- package/src/report/markdown.ts +125 -25
- package/src/report/model.ts +344 -7
package/dist/report/model.d.ts
CHANGED
|
@@ -36,7 +36,171 @@ export interface ReportOptions {
|
|
|
36
36
|
* with the copy somebody was sent.
|
|
37
37
|
*/
|
|
38
38
|
readonly now: Date;
|
|
39
|
+
/**
|
|
40
|
+
* Keep issues below this severity out of the detail sections.
|
|
41
|
+
*
|
|
42
|
+
* **The counts do not move, and that asymmetry is the whole design.**
|
|
43
|
+
* {@link ReportModel.outstanding}, the exposure score and the breach count
|
|
44
|
+
* describe the target, not the document, so a floor that changed them would
|
|
45
|
+
* let somebody produce a report saying "3 outstanding" about a target with
|
|
46
|
+
* forty. What the floor removes is pages, not facts: the header still sums
|
|
47
|
+
* everything, and {@link ReportModel.limitations} gains a sentence naming
|
|
48
|
+
* how many issues were left out.
|
|
49
|
+
*
|
|
50
|
+
* That is the same shape the suppressed appendix already uses — excluded
|
|
51
|
+
* from the sections, stated in the limitations — because it answers the
|
|
52
|
+
* same question, which is what a reader is not being shown.
|
|
53
|
+
*
|
|
54
|
+
* Omitted entirely by default: every issue is listed.
|
|
55
|
+
*/
|
|
56
|
+
readonly severityFloor?: Severity;
|
|
57
|
+
/**
|
|
58
|
+
* Whether to list the issues this run resolved. Defaults to `true`.
|
|
59
|
+
*
|
|
60
|
+
* Off is a legitimate choice for a document that only needs to state what is
|
|
61
|
+
* outstanding, and unlike {@link ReportOptions.severityFloor} it needs no disclosure —
|
|
62
|
+
* omitting evidence of remediation understates the good news rather than the
|
|
63
|
+
* bad, so a reader cannot be misled about risk by its absence.
|
|
64
|
+
*
|
|
65
|
+
* It does not touch {@link ReportModel.retest}: a retest's `fixed` verdict is
|
|
66
|
+
* the document's entire point, and a caller asking for less detail about
|
|
67
|
+
* resolved issues is not asking for a retest that cannot do its job.
|
|
68
|
+
*/
|
|
69
|
+
readonly includeResolved?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* How much of each issue's evidence to print. Defaults to `'summary'`.
|
|
72
|
+
*
|
|
73
|
+
* - `'none'` — no evidence at all. The issue, its severity, its location and
|
|
74
|
+
* what to do about it. External references are still listed: an advisory is
|
|
75
|
+
* reading, not evidence.
|
|
76
|
+
* - `'summary'` — how many findings support the issue, and nothing about any
|
|
77
|
+
* one of them. What every report printed before this option existed.
|
|
78
|
+
* - `'full'` — each supporting finding in its own right: where it was seen,
|
|
79
|
+
* when, at what severity, with its CVSS and CVE where the engine supplied
|
|
80
|
+
* them, and pointers to any stored capture.
|
|
81
|
+
*
|
|
82
|
+
* **No verbosity names an engine, and that is not negotiable.** Which scanner
|
|
83
|
+
* produced a finding is an implementation detail of the assessment, and
|
|
84
|
+
* naming the stack in a customer-facing document gives away more than it
|
|
85
|
+
* explains — `00-DOMAIN.md` §7 and {@link TestingBasis.engines}. The engine
|
|
86
|
+
* name has leaked into rendered output twice already, so `'full'` prints
|
|
87
|
+
* what was found and never who found it.
|
|
88
|
+
*/
|
|
89
|
+
readonly evidenceVerbosity?: EvidenceVerbosity;
|
|
90
|
+
/** Whose document this is. See {@link Branding}. */
|
|
91
|
+
readonly branding?: Branding;
|
|
92
|
+
/**
|
|
93
|
+
* Open the report with a cover page. Defaults to `false`.
|
|
94
|
+
*
|
|
95
|
+
* **On the options rather than on {@link Branding}, and the split is
|
|
96
|
+
* deliberate.** Branding is org-level configuration, stored once and the
|
|
97
|
+
* same for every document; whether a particular render wants a cover is a
|
|
98
|
+
* per-report layout decision — a PDF for a client does, a Markdown export
|
|
99
|
+
* piped into a terminal does not.
|
|
100
|
+
*
|
|
101
|
+
* Markdown has no pages, so there it is a block at the top rather than a
|
|
102
|
+
* page of its own. The statement is the same; the format has one form for
|
|
103
|
+
* it.
|
|
104
|
+
*/
|
|
105
|
+
readonly coverPage?: boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Open the body with a contents list, and give every section an anchor.
|
|
108
|
+
* Defaults to `false`.
|
|
109
|
+
*
|
|
110
|
+
* **The anchors are what a PDF needs, not the list.** Chromium derives a
|
|
111
|
+
* document outline from the heading structure, which is how a reader
|
|
112
|
+
* navigates a sixty-page report in a viewer's sidebar; the printed contents
|
|
113
|
+
* list is for whoever has it on paper. Both come from the same scan of the
|
|
114
|
+
* rendered headings, so a list entry cannot point at an anchor that is not
|
|
115
|
+
* there.
|
|
116
|
+
*
|
|
117
|
+
* Individual issues (`<h4>`) are left out on purpose: a report with sixty
|
|
118
|
+
* findings would have a contents list longer than its summary.
|
|
119
|
+
*/
|
|
120
|
+
readonly tableOfContents?: boolean;
|
|
39
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Whose document this is — the marks on it, and the name on it.
|
|
124
|
+
*
|
|
125
|
+
* Per `00-TIER-MATRIX.md`: free-tier reports are watermarked, branding is
|
|
126
|
+
* `[branding]`, and white-label is Enterprise only. **None of that is enforced
|
|
127
|
+
* here.** This package has no idea what anyone is paying, and a pure function
|
|
128
|
+
* that consulted an entitlement would be the wrong place to find out; the
|
|
129
|
+
* caller passes what the caller is entitled to.
|
|
130
|
+
*/
|
|
131
|
+
export interface Branding {
|
|
132
|
+
/**
|
|
133
|
+
* The name that appears where Secureport's otherwise would — on an
|
|
134
|
+
* Attestation Letter, as the party who carried out the testing.
|
|
135
|
+
*
|
|
136
|
+
* {@link ReportOptions.preparedBy} wins over it when both are given, because
|
|
137
|
+
* it is the more specific statement: branding is who owns the document,
|
|
138
|
+
* `preparedBy` is who did the work, and they are not always the same party.
|
|
139
|
+
*/
|
|
140
|
+
readonly companyName?: string;
|
|
141
|
+
/**
|
|
142
|
+
* Accent colour for headings and rules, as a hex triplet — `#0a7`,
|
|
143
|
+
* `#00aa77`, or `#00aa77ff`.
|
|
144
|
+
*
|
|
145
|
+
* **Hex only, and the restriction is a security boundary rather than
|
|
146
|
+
* fussiness.** This value is interpolated into the document's `<style>`
|
|
147
|
+
* block, which is a context the HTML renderer's escaping does not protect:
|
|
148
|
+
* it escapes text nodes, and a colour of `red</style><script>…` would close
|
|
149
|
+
* the element and run. Anything that does not match the pattern is refused by
|
|
150
|
+
* {@link buildReportModel} rather than sanitised, because silently altering
|
|
151
|
+
* somebody's brand colour is its own kind of wrong.
|
|
152
|
+
*/
|
|
153
|
+
readonly primaryColour?: string;
|
|
154
|
+
/**
|
|
155
|
+
* Remove Secureport's own marks from the document entirely.
|
|
156
|
+
*
|
|
157
|
+
* An Attestation Letter with this set and no name to put in Secureport's
|
|
158
|
+
* place is refused: a formal statement that testing was carried out has to
|
|
159
|
+
* say who carried it out, and an unattributed one is worth nothing to the
|
|
160
|
+
* auditor it exists for.
|
|
161
|
+
*/
|
|
162
|
+
readonly whiteLabel?: boolean;
|
|
163
|
+
/**
|
|
164
|
+
* Text printed across every page — what the free tier stamps on a report.
|
|
165
|
+
*
|
|
166
|
+
* Rendered as an element rather than through CSS `content`, so it goes
|
|
167
|
+
* through the same escaping as every other value from outside.
|
|
168
|
+
*/
|
|
169
|
+
readonly watermark?: string;
|
|
170
|
+
/**
|
|
171
|
+
* The logo, as a `data:` URI — never a URL.
|
|
172
|
+
*
|
|
173
|
+
* **The restriction is what keeps a report a single file.** `renderHtml`
|
|
174
|
+
* emits a self-contained document with its stylesheet inlined and nothing
|
|
175
|
+
* linked, which is what lets P7 render it through Chromium with no network
|
|
176
|
+
* and get the same bytes in CI as on a laptop. One `<img src="https://…">`
|
|
177
|
+
* would trade that for a logo, and trade it silently: the report would look
|
|
178
|
+
* right on the machine that rendered it and lose its mark for a reader
|
|
179
|
+
* offline, or three months later when the URL stops resolving.
|
|
180
|
+
*
|
|
181
|
+
* Refused by {@link buildReportModel} rather than fetched, because a pure
|
|
182
|
+
* function that reached the network would stop being one.
|
|
183
|
+
*
|
|
184
|
+
* Rendered into an `<img>`, which is also why an SVG data URI is allowed: an
|
|
185
|
+
* image context does not execute script, where inlining the same markup into
|
|
186
|
+
* the document would.
|
|
187
|
+
*/
|
|
188
|
+
readonly logo?: string;
|
|
189
|
+
/**
|
|
190
|
+
* Address, registration number, contact — whatever belongs under the name on
|
|
191
|
+
* a cover page, one line each.
|
|
192
|
+
*
|
|
193
|
+
* Only shown when {@link ReportOptions.coverPage} is set, because there is
|
|
194
|
+
* nowhere else in the document these belong.
|
|
195
|
+
*/
|
|
196
|
+
readonly companyDetails?: readonly string[];
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* How much of an issue's supporting evidence a report prints.
|
|
200
|
+
*
|
|
201
|
+
* See {@link ReportOptions.evidenceVerbosity} for what each level shows.
|
|
202
|
+
*/
|
|
203
|
+
export type EvidenceVerbosity = 'none' | 'summary' | 'full';
|
|
40
204
|
/**
|
|
41
205
|
* How the findings in a report were actually produced.
|
|
42
206
|
*
|
|
@@ -81,6 +245,21 @@ export interface TestingBasis {
|
|
|
81
245
|
/** One sentence, ready to print. */
|
|
82
246
|
readonly statement: string;
|
|
83
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* What a severity floor kept out of a report's detail sections.
|
|
250
|
+
*
|
|
251
|
+
* The issues are still open, still counted and still the target's problem;
|
|
252
|
+
* only the pages describing them are gone. That distinction is the reason this
|
|
253
|
+
* is reported at all rather than silently applied.
|
|
254
|
+
*/
|
|
255
|
+
export interface OmittedIssues {
|
|
256
|
+
/** How many outstanding issues were left out. Never `0` — the field is absent instead. */
|
|
257
|
+
readonly count: number;
|
|
258
|
+
/** The floor that excluded them. */
|
|
259
|
+
readonly floor: Severity;
|
|
260
|
+
/** One sentence, ready to print, so no two renderers word it differently. */
|
|
261
|
+
readonly statement: string;
|
|
262
|
+
}
|
|
84
263
|
/** A group of issues sharing a severity, most urgent first. */
|
|
85
264
|
export interface ReportSection {
|
|
86
265
|
/** The severity this section covers. */
|
|
@@ -102,8 +281,32 @@ export interface ReportModel {
|
|
|
102
281
|
readonly preparedBy?: string;
|
|
103
282
|
/** Who it is for, if stated. */
|
|
104
283
|
readonly preparedFor?: string;
|
|
284
|
+
/**
|
|
285
|
+
* Who the document says carried out the testing.
|
|
286
|
+
*
|
|
287
|
+
* Resolved once here from {@link ReportOptions.preparedBy},
|
|
288
|
+
* {@link Branding.companyName} and the default, because both renderers used
|
|
289
|
+
* to write `preparedBy ?? 'Secureport'` themselves — two copies of a default
|
|
290
|
+
* that white-labelling has to change in both places or not at all.
|
|
291
|
+
*/
|
|
292
|
+
readonly attestor: string;
|
|
293
|
+
/** Whose document this is, as given. Absent when nothing was branded. */
|
|
294
|
+
readonly branding?: Branding;
|
|
295
|
+
/** Whether to open with a cover, resolved from {@link ReportOptions.coverPage}. */
|
|
296
|
+
readonly coverPage: boolean;
|
|
297
|
+
/** Whether to print a contents list, resolved from {@link ReportOptions.tableOfContents}. */
|
|
298
|
+
readonly tableOfContents: boolean;
|
|
105
299
|
/** How the findings were produced. */
|
|
106
300
|
readonly basis: TestingBasis;
|
|
301
|
+
/**
|
|
302
|
+
* How much of each issue's evidence to print, resolved from
|
|
303
|
+
* {@link ReportOptions.evidenceVerbosity}.
|
|
304
|
+
*
|
|
305
|
+
* On the model rather than read from the options by each renderer, so the
|
|
306
|
+
* Markdown and the HTML cannot disagree about how much of an issue they are
|
|
307
|
+
* showing — the same reason every number here is derived once.
|
|
308
|
+
*/
|
|
309
|
+
readonly evidenceVerbosity: EvidenceVerbosity;
|
|
107
310
|
/**
|
|
108
311
|
* Outstanding issues grouped by severity, most urgent first, empties dropped.
|
|
109
312
|
*
|
|
@@ -117,9 +320,21 @@ export interface ReportModel {
|
|
|
117
320
|
*
|
|
118
321
|
* Kept and shown rather than dropped: "what you fixed" is the story the
|
|
119
322
|
* product exists to tell, and a report that silently omits it throws away its
|
|
120
|
-
* best evidence.
|
|
323
|
+
* best evidence. Empty when {@link ReportOptions.includeResolved} is `false`,
|
|
324
|
+
* which is the caller saying so deliberately.
|
|
121
325
|
*/
|
|
122
326
|
readonly resolved: readonly SnapshotIssue[];
|
|
327
|
+
/**
|
|
328
|
+
* What {@link ReportOptions.severityFloor} kept out of {@link ReportModel.sections}.
|
|
329
|
+
* Absent when no floor was set, or when one was set and removed nothing.
|
|
330
|
+
*
|
|
331
|
+
* Carried as data and not only as prose, because `00-DOMAIN.md` §7 says what
|
|
332
|
+
* a report does not establish travels as data too — so a consumer rendering
|
|
333
|
+
* its own view cannot drop the disclosure simply by not printing a sentence.
|
|
334
|
+
* `statement` is that sentence, derived once here so the renderers and
|
|
335
|
+
* {@link ReportModel.limitations} cannot word it three different ways.
|
|
336
|
+
*/
|
|
337
|
+
readonly omitted?: OmittedIssues;
|
|
123
338
|
/** Open and regressed issues, by severity. What the reader owes work on. */
|
|
124
339
|
readonly outstanding: Readonly<Record<Severity, number>>;
|
|
125
340
|
/** Total outstanding, so a header does not have to sum a table. */
|
|
@@ -164,6 +379,17 @@ export interface ReportModel {
|
|
|
164
379
|
* - `new` — this run's own finding, not part of what was being retested.
|
|
165
380
|
*/
|
|
166
381
|
export type RetestVerdict = 'fixed' | 'still_present' | 'returned' | 'not_retested' | 'new';
|
|
382
|
+
/**
|
|
383
|
+
* How each verdict is printed.
|
|
384
|
+
*
|
|
385
|
+
* Exported because it is printed, and a printed vocabulary is a contract —
|
|
386
|
+
* `00-DOMAIN.md` §7 says so of the verdicts themselves. It was defined
|
|
387
|
+
* identically in both renderers, which PDF and DOCX would have made four
|
|
388
|
+
* copies of, each one a chance for `not_retested` to be worded differently in
|
|
389
|
+
* the format somebody actually reads. The underscores are an implementation
|
|
390
|
+
* detail of the union and should never reach a page.
|
|
391
|
+
*/
|
|
392
|
+
export declare const VERDICT_LABELS: Readonly<Record<RetestVerdict, string>>;
|
|
167
393
|
/** One issue, with what the retest established about it. */
|
|
168
394
|
export interface RetestEntry {
|
|
169
395
|
/** The issue as the snapshot sees it. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../../src/report/model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAE9D;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEtE,4EAA4E;AAC5E,MAAM,WAAW,aAAa;IAC5B,8BAA8B;IAC9B,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAE1B,mCAAmC;IACnC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAExB,gFAAgF;IAChF,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAE7B,qBAAqB;IACrB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAE9B;;;;;;OAMG;IACH,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../../src/report/model.ts"],"names":[],"mappings":"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"}
|
package/dist/report/model.js
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
import { SEVERITY_ORDER } from '../severity.js';
|
|
2
|
+
/**
|
|
3
|
+
* How each verdict is printed.
|
|
4
|
+
*
|
|
5
|
+
* Exported because it is printed, and a printed vocabulary is a contract —
|
|
6
|
+
* `00-DOMAIN.md` §7 says so of the verdicts themselves. It was defined
|
|
7
|
+
* identically in both renderers, which PDF and DOCX would have made four
|
|
8
|
+
* copies of, each one a chance for `not_retested` to be worded differently in
|
|
9
|
+
* the format somebody actually reads. The underscores are an implementation
|
|
10
|
+
* detail of the union and should never reach a page.
|
|
11
|
+
*/
|
|
12
|
+
export const VERDICT_LABELS = Object.freeze({
|
|
13
|
+
fixed: 'fixed',
|
|
14
|
+
still_present: 'still present',
|
|
15
|
+
returned: 'returned',
|
|
16
|
+
not_retested: 'not retested',
|
|
17
|
+
new: 'new since',
|
|
18
|
+
});
|
|
2
19
|
/** Works out what a run established about the issues carried into it. */
|
|
3
20
|
function describeRetest(snapshot) {
|
|
4
21
|
const { baseline } = snapshot;
|
|
@@ -54,7 +71,7 @@ function verdictFor(entry) {
|
|
|
54
71
|
return entry.findings.length > 0 ? 'still_present' : 'not_retested';
|
|
55
72
|
}
|
|
56
73
|
/** Works out what a report cannot establish, from the run rather than a template. */
|
|
57
|
-
function describeLimitations(snapshot, basis) {
|
|
74
|
+
function describeLimitations(snapshot, basis, omitted) {
|
|
58
75
|
const limitations = [];
|
|
59
76
|
if (!basis.includesManual) {
|
|
60
77
|
limitations.push('This is evidence of automated security testing. It is not a human-led penetration ' +
|
|
@@ -73,9 +90,20 @@ function describeLimitations(snapshot, basis) {
|
|
|
73
90
|
limitations.push(`${snapshot.suppressed.length} finding${snapshot.suppressed.length === 1 ? ' has' : 's have'} been ` +
|
|
74
91
|
'suppressed and excluded from the counts above. They are listed in full in the appendix.');
|
|
75
92
|
}
|
|
93
|
+
// Immediately after the suppression sentence and before the compliance one,
|
|
94
|
+
// because it answers the same question a reader is entitled to ask: what am
|
|
95
|
+
// I not being shown? A floor that removed pages silently would be the
|
|
96
|
+
// over-claim §7 exists to stop, in a quieter form than the appendix one.
|
|
97
|
+
if (omitted !== undefined)
|
|
98
|
+
limitations.push(omitted.statement);
|
|
76
99
|
limitations.push('This document does not certify compliance with any standard, framework or regulation.');
|
|
77
100
|
return limitations;
|
|
78
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Hex triplets only. See {@link Branding.primaryColour} for why this is a
|
|
104
|
+
* refusal rather than a sanitisation.
|
|
105
|
+
*/
|
|
106
|
+
const HEX_COLOUR = /^#(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/iu;
|
|
79
107
|
const DEFAULT_TITLES = Object.freeze({
|
|
80
108
|
pen: 'Penetration Test Report',
|
|
81
109
|
vap: 'Vulnerability Assessment Report',
|
|
@@ -138,22 +166,36 @@ export function buildReportModel(snapshot, options) {
|
|
|
138
166
|
const bySeverity = new Map();
|
|
139
167
|
const resolved = [];
|
|
140
168
|
let breached = 0;
|
|
169
|
+
let omittedBelowFloor = 0;
|
|
170
|
+
// `SEVERITY_ORDER` is most-severe-first, so "at or above the floor" is a
|
|
171
|
+
// *lower* index. Same trap `describeRetest` records above, from the other
|
|
172
|
+
// direction: `severityRank` counts the opposite way and using it here would
|
|
173
|
+
// floor out everything except the advisories.
|
|
174
|
+
const floorAt = options.severityFloor === undefined
|
|
175
|
+
? SEVERITY_ORDER.length
|
|
176
|
+
: SEVERITY_ORDER.indexOf(options.severityFloor);
|
|
141
177
|
for (const entry of snapshot.issues) {
|
|
142
178
|
if (entry.issue.status === 'resolved') {
|
|
143
179
|
resolved.push(entry);
|
|
144
180
|
continue;
|
|
145
181
|
}
|
|
146
182
|
const severity = entry.issue.effectiveSeverity;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
group.push(entry);
|
|
150
|
-
else
|
|
151
|
-
bySeverity.set(severity, [entry]);
|
|
183
|
+
// Counted before the floor is consulted, deliberately: the floor decides
|
|
184
|
+
// what is *listed*, never what is *counted*.
|
|
152
185
|
if (entry.issue.status === 'open' || entry.issue.status === 'regressed') {
|
|
153
186
|
outstanding[severity]++;
|
|
154
187
|
if (entry.slaStatus === 'breached')
|
|
155
188
|
breached++;
|
|
156
189
|
}
|
|
190
|
+
if (SEVERITY_ORDER.indexOf(severity) > floorAt) {
|
|
191
|
+
omittedBelowFloor++;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const group = bySeverity.get(severity);
|
|
195
|
+
if (group)
|
|
196
|
+
group.push(entry);
|
|
197
|
+
else
|
|
198
|
+
bySeverity.set(severity, [entry]);
|
|
157
199
|
}
|
|
158
200
|
resolved.sort((a, b) => b.issue.lastSeen.getTime() - a.issue.lastSeen.getTime());
|
|
159
201
|
const sections = [];
|
|
@@ -168,6 +210,17 @@ export function buildReportModel(snapshot, options) {
|
|
|
168
210
|
issues: [...issues].sort((a, b) => b.issue.lastSeen.getTime() - a.issue.lastSeen.getTime()),
|
|
169
211
|
});
|
|
170
212
|
}
|
|
213
|
+
const floor = options.severityFloor;
|
|
214
|
+
const omitted = omittedBelowFloor === 0 || floor === undefined
|
|
215
|
+
? undefined
|
|
216
|
+
: {
|
|
217
|
+
count: omittedBelowFloor,
|
|
218
|
+
floor,
|
|
219
|
+
statement: `${omittedBelowFloor} issue${omittedBelowFloor === 1 ? '' : 's'} below ${floor} severity ` +
|
|
220
|
+
`${omittedBelowFloor === 1 ? 'is' : 'are'} not listed individually in this document. ` +
|
|
221
|
+
`${omittedBelowFloor === 1 ? 'It remains' : 'They remain'} open and ` +
|
|
222
|
+
`${omittedBelowFloor === 1 ? 'is' : 'are'} included in every count above.`,
|
|
223
|
+
};
|
|
171
224
|
const outstandingTotal = SEVERITY_ORDER.reduce((sum, s) => sum + outstanding[s], 0);
|
|
172
225
|
const basis = describeBasis(snapshot);
|
|
173
226
|
const retest = describeRetest(snapshot);
|
|
@@ -176,6 +229,26 @@ export function buildReportModel(snapshot, options) {
|
|
|
176
229
|
if (options.kind === 'retest' && retest === undefined) {
|
|
177
230
|
throw new TypeError('a retest report needs a baseline: build the snapshot with `previous` set to the run being retested');
|
|
178
231
|
}
|
|
232
|
+
const branding = options.branding;
|
|
233
|
+
const colour = branding?.primaryColour;
|
|
234
|
+
if (colour !== undefined && !HEX_COLOUR.test(colour)) {
|
|
235
|
+
throw new TypeError(`primaryColour must be a hex triplet such as #0a7 or #00aa77, not ${JSON.stringify(colour)}: ` +
|
|
236
|
+
'it is interpolated into the document stylesheet, where anything else could end the element');
|
|
237
|
+
}
|
|
238
|
+
const logo = branding?.logo;
|
|
239
|
+
if (logo !== undefined && !logo.startsWith('data:image/')) {
|
|
240
|
+
throw new TypeError(`logo must be a data: URI, not ${JSON.stringify(logo.slice(0, 40))}: ` +
|
|
241
|
+
'a linked image would make the report depend on a network at render time, ' +
|
|
242
|
+
'which is the guarantee that lets it be rendered to PDF reproducibly');
|
|
243
|
+
}
|
|
244
|
+
const attestor = options.preparedBy ?? branding?.companyName;
|
|
245
|
+
// An attestation is a statement that a named party carried out testing. With
|
|
246
|
+
// Secureport's name removed and nothing put in its place there is no such
|
|
247
|
+
// party, and the document would assert something on nobody's behalf.
|
|
248
|
+
if (attestor === undefined && branding?.whiteLabel === true && options.kind === 'attest') {
|
|
249
|
+
throw new TypeError('a white-labelled attestation must name who carried out the testing: ' +
|
|
250
|
+
'set branding.companyName or preparedBy');
|
|
251
|
+
}
|
|
179
252
|
return {
|
|
180
253
|
kind: options.kind,
|
|
181
254
|
title: options.title ?? DEFAULT_TITLES[options.kind],
|
|
@@ -183,10 +256,16 @@ export function buildReportModel(snapshot, options) {
|
|
|
183
256
|
generatedAt: options.now,
|
|
184
257
|
...(options.preparedBy === undefined ? {} : { preparedBy: options.preparedBy }),
|
|
185
258
|
...(options.preparedFor === undefined ? {} : { preparedFor: options.preparedFor }),
|
|
259
|
+
attestor: attestor ?? 'Secureport',
|
|
260
|
+
coverPage: options.coverPage === true,
|
|
261
|
+
tableOfContents: options.tableOfContents === true,
|
|
262
|
+
...(branding === undefined ? {} : { branding }),
|
|
186
263
|
basis,
|
|
187
|
-
|
|
264
|
+
evidenceVerbosity: options.evidenceVerbosity ?? 'summary',
|
|
265
|
+
limitations: describeLimitations(snapshot, basis, omitted),
|
|
188
266
|
sections,
|
|
189
|
-
resolved,
|
|
267
|
+
resolved: options.includeResolved === false ? [] : resolved,
|
|
268
|
+
...(omitted === undefined ? {} : { omitted }),
|
|
190
269
|
outstanding,
|
|
191
270
|
outstandingTotal,
|
|
192
271
|
exposureScore: snapshot.run.exposureScore,
|
package/dist/report/model.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model.js","sourceRoot":"","sources":["../../src/report/model.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"model.js","sourceRoot":"","sources":["../../src/report/model.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/finding.ts
CHANGED
|
@@ -94,15 +94,16 @@ export interface Finding {
|
|
|
94
94
|
*
|
|
95
95
|
* **Recorded, not addressed.** The port is already inside
|
|
96
96
|
* {@link Finding.location} for host-oriented engines (Nessus assembles
|
|
97
|
-
* `host:port`) and is
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* should have to do.
|
|
97
|
+
* `host:port`), and that location is what the fingerprint reads. It exists
|
|
98
|
+
* because a port was otherwise unrecoverable from a finding except by
|
|
99
|
+
* parsing its location, which is not something a consumer should have to do.
|
|
101
100
|
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
101
|
+
* **No longer a fingerprint component at all, as of `fp_v2`.** It used to
|
|
102
|
+
* fill the last slot when no parameter was named, which counted a Nessus
|
|
103
|
+
* port twice and stopped a network scan ever matching a web scan of the same
|
|
104
|
+
* endpoint. The location carries the port now, canonicalised — so a default
|
|
105
|
+
* port drops and any other is kept, which is the distinction that actually
|
|
106
|
+
* matters.
|
|
106
107
|
*/
|
|
107
108
|
readonly port?: number;
|
|
108
109
|
|
package/src/fingerprint.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { createHash } from 'node:crypto';
|
|
|
13
13
|
* If you are tempted to "just tweak" the normaliser, that is this constant's
|
|
14
14
|
* job to prevent.
|
|
15
15
|
*/
|
|
16
|
-
export const FINGERPRINT_VERSION = '
|
|
16
|
+
export const FINGERPRINT_VERSION = 'fp_v2';
|
|
17
17
|
|
|
18
18
|
/** A path segment that is entirely digits, e.g. the `123` in `/orders/123`. */
|
|
19
19
|
const NUMERIC_SEGMENT = /^\d+$/u;
|
|
@@ -80,10 +80,17 @@ function collapseSegment(segment: string): string {
|
|
|
80
80
|
* one issue into two.
|
|
81
81
|
* - **Drops the fragment**, which the server never sees.
|
|
82
82
|
*
|
|
83
|
-
*
|
|
84
|
-
* `
|
|
85
|
-
*
|
|
86
|
-
*
|
|
83
|
+
* - **Reads `host:443` and `host:80` as the URL the port implies**, so a
|
|
84
|
+
* network scanner's `app.example.com:443` and a web scanner's
|
|
85
|
+
* `https://app.example.com/` are one endpoint (B102, `fp_v2`). Only those two
|
|
86
|
+
* ports: any other is left alone rather than guessed at, because
|
|
87
|
+
* `host:8443` could be TLS or plaintext and inventing a scheme to force a
|
|
88
|
+
* match would assert something nobody measured.
|
|
89
|
+
*
|
|
90
|
+
* Anything else that is not a parseable absolute URL — a bare host, a file
|
|
91
|
+
* path — is normalised as a path alone. That is deliberate: refusing to
|
|
92
|
+
* fingerprint a non-HTTP finding would exclude whole classes of scanner from
|
|
93
|
+
* the model.
|
|
87
94
|
*
|
|
88
95
|
* @param location - Where the weakness was found.
|
|
89
96
|
* @returns The normalised location.
|
|
@@ -107,7 +114,9 @@ export function normaliseLocation(location: string): string {
|
|
|
107
114
|
// Not an absolute URL: normalise it as a bare path and stop. `new URL` would
|
|
108
115
|
// otherwise turn `example.com/x` into the `example.com:` protocol.
|
|
109
116
|
if (!url || url.protocol === '' || !url.host) {
|
|
110
|
-
|
|
117
|
+
const implied = impliedSchemeUrl(trimmed);
|
|
118
|
+
if (implied === undefined) return normalisePathOnly(trimmed);
|
|
119
|
+
url = implied;
|
|
111
120
|
}
|
|
112
121
|
|
|
113
122
|
// Not `decodeOnce` here: normalisePathOnly decodes, and decoding on the way
|
|
@@ -123,6 +132,32 @@ export function normaliseLocation(location: string): string {
|
|
|
123
132
|
return `${url.protocol}//${url.host}${path}${query}`;
|
|
124
133
|
}
|
|
125
134
|
|
|
135
|
+
/**
|
|
136
|
+
* A bare `host:port` read as the URL its port implies, or `undefined`.
|
|
137
|
+
*
|
|
138
|
+
* **Only `:443` and `:80`, and that restraint is the point.** A network scanner
|
|
139
|
+
* reports `app.example.com:443` for the weakness a web scanner reports at
|
|
140
|
+
* `https://app.example.com/`, and those are one endpoint: 443 means TLS and an
|
|
141
|
+
* empty path means the root. Both are convention rather than guarantee, but
|
|
142
|
+
* they are conventions every scanner in this model already relies on, and
|
|
143
|
+
* leaving the two unmatched means a network scan and a web scan of one host
|
|
144
|
+
* can never agree about anything — which is the duplication P6 exists to
|
|
145
|
+
* remove (B102).
|
|
146
|
+
*
|
|
147
|
+
* Any other port is left alone rather than guessed at. `app.example.com:8443`
|
|
148
|
+
* could be TLS or plaintext, and inventing a scheme to make a match happen
|
|
149
|
+
* would be asserting something nobody measured.
|
|
150
|
+
*/
|
|
151
|
+
function impliedSchemeUrl(location: string): URL | undefined {
|
|
152
|
+
const match = /^([a-z0-9.\-_]+|\[[0-9a-f:]+\]):(443|80)$/iu.exec(location);
|
|
153
|
+
if (!match) return undefined;
|
|
154
|
+
try {
|
|
155
|
+
return new URL(`${match[2] === '443' ? 'https' : 'http'}://${location}`);
|
|
156
|
+
} catch {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
126
161
|
/**
|
|
127
162
|
* Normalises a path with no scheme or host.
|
|
128
163
|
*/
|
|
@@ -197,6 +232,15 @@ export const VULN_KEY_MAP: Readonly<Record<string, string>> = Object.freeze({
|
|
|
197
232
|
// the other stops the two agreeing, so these travel together.
|
|
198
233
|
'nuclei:xss-reflected': 'xss-reflected',
|
|
199
234
|
'nuclei:missing-hsts': 'hsts-missing',
|
|
235
|
+
|
|
236
|
+
// Burp and Nessus, added in `fp_v2`. Neither had a single entry, so both fell
|
|
237
|
+
// through to the CWE for every finding — which is worse than no table at all:
|
|
238
|
+
// ZAP and Nuclei keyed `hsts-missing` while Burp and Nessus keyed `CWE-319`
|
|
239
|
+
// for the same weakness, so half a table was actively splitting issues that a
|
|
240
|
+
// plain CWE fallback would have kept together.
|
|
241
|
+
'burp:5244160': 'xss-reflected',
|
|
242
|
+
'burp:6234880': 'hsts-missing',
|
|
243
|
+
'nessus:42822': 'hsts-missing',
|
|
200
244
|
});
|
|
201
245
|
|
|
202
246
|
/**
|
|
@@ -306,10 +350,32 @@ export interface FingerprintInput {
|
|
|
306
350
|
* ```
|
|
307
351
|
*/
|
|
308
352
|
export function fingerprint(input: FingerprintInput): string {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
353
|
+
// The port is no longer part of the tail. `normaliseLocation` now canonicalises
|
|
354
|
+
// a `host:port` location, so a non-default port is already in the location and
|
|
355
|
+
// a default one is deliberately absent from it — counting the port again here
|
|
356
|
+
// made a Nessus finding on 443 unable to match a web scanner's finding on the
|
|
357
|
+
// same endpoint. `import/nessus.ts` recorded this double-count as something
|
|
358
|
+
// that had to stay because removing it would force a re-fingerprint migration;
|
|
359
|
+
// `fp_v2` is that migration.
|
|
360
|
+
const tail = input.parameter ?? '';
|
|
361
|
+
|
|
362
|
+
// A named parameter makes the query string redundant: both describe the same
|
|
363
|
+
// input, and keeping the query too means a scanner that reports the parameter
|
|
364
|
+
// separately (Burp: `/search`, parameter `q`) can never match one that leaves
|
|
365
|
+
// it in the URL (ZAP: `/search?q=test`, parameter `q`). When no parameter is
|
|
366
|
+
// named the query names stay, because then they are the only thing
|
|
367
|
+
// distinguishing one page from another — and Nuclei never names a parameter,
|
|
368
|
+
// so dropping them unconditionally would merge `/view?id` with `/view?page`.
|
|
369
|
+
const location = normaliseLocation(input.location);
|
|
370
|
+
const addressed = input.parameter === undefined ? location : stripQuery(location);
|
|
371
|
+
|
|
372
|
+
const material = [input.targetId, input.vulnKey, addressed, tail].join('|');
|
|
313
373
|
|
|
314
374
|
return createHash('sha256').update(material, 'utf8').digest('hex');
|
|
315
375
|
}
|
|
376
|
+
|
|
377
|
+
/** Everything before the `?` of an already-normalised location. */
|
|
378
|
+
function stripQuery(location: string): string {
|
|
379
|
+
const at = location.indexOf('?');
|
|
380
|
+
return at === -1 ? location : location.slice(0, at);
|
|
381
|
+
}
|