@secureport/core 2.1.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/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -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 +46 -1
- package/dist/reconcile.d.ts.map +1 -1
- package/dist/reconcile.js +34 -1
- package/dist/reconcile.js.map +1 -1
- 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/index.ts +5 -2
- package/src/issue.ts +1 -0
- package/src/reconcile.ts +86 -2
- 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/src/report/model.ts
CHANGED
|
@@ -43,8 +43,185 @@ export interface ReportOptions {
|
|
|
43
43
|
* with the copy somebody was sent.
|
|
44
44
|
*/
|
|
45
45
|
readonly now: Date;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Keep issues below this severity out of the detail sections.
|
|
49
|
+
*
|
|
50
|
+
* **The counts do not move, and that asymmetry is the whole design.**
|
|
51
|
+
* {@link ReportModel.outstanding}, the exposure score and the breach count
|
|
52
|
+
* describe the target, not the document, so a floor that changed them would
|
|
53
|
+
* let somebody produce a report saying "3 outstanding" about a target with
|
|
54
|
+
* forty. What the floor removes is pages, not facts: the header still sums
|
|
55
|
+
* everything, and {@link ReportModel.limitations} gains a sentence naming
|
|
56
|
+
* how many issues were left out.
|
|
57
|
+
*
|
|
58
|
+
* That is the same shape the suppressed appendix already uses — excluded
|
|
59
|
+
* from the sections, stated in the limitations — because it answers the
|
|
60
|
+
* same question, which is what a reader is not being shown.
|
|
61
|
+
*
|
|
62
|
+
* Omitted entirely by default: every issue is listed.
|
|
63
|
+
*/
|
|
64
|
+
readonly severityFloor?: Severity;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Whether to list the issues this run resolved. Defaults to `true`.
|
|
68
|
+
*
|
|
69
|
+
* Off is a legitimate choice for a document that only needs to state what is
|
|
70
|
+
* outstanding, and unlike {@link ReportOptions.severityFloor} it needs no disclosure —
|
|
71
|
+
* omitting evidence of remediation understates the good news rather than the
|
|
72
|
+
* bad, so a reader cannot be misled about risk by its absence.
|
|
73
|
+
*
|
|
74
|
+
* It does not touch {@link ReportModel.retest}: a retest's `fixed` verdict is
|
|
75
|
+
* the document's entire point, and a caller asking for less detail about
|
|
76
|
+
* resolved issues is not asking for a retest that cannot do its job.
|
|
77
|
+
*/
|
|
78
|
+
readonly includeResolved?: boolean;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* How much of each issue's evidence to print. Defaults to `'summary'`.
|
|
82
|
+
*
|
|
83
|
+
* - `'none'` — no evidence at all. The issue, its severity, its location and
|
|
84
|
+
* what to do about it. External references are still listed: an advisory is
|
|
85
|
+
* reading, not evidence.
|
|
86
|
+
* - `'summary'` — how many findings support the issue, and nothing about any
|
|
87
|
+
* one of them. What every report printed before this option existed.
|
|
88
|
+
* - `'full'` — each supporting finding in its own right: where it was seen,
|
|
89
|
+
* when, at what severity, with its CVSS and CVE where the engine supplied
|
|
90
|
+
* them, and pointers to any stored capture.
|
|
91
|
+
*
|
|
92
|
+
* **No verbosity names an engine, and that is not negotiable.** Which scanner
|
|
93
|
+
* produced a finding is an implementation detail of the assessment, and
|
|
94
|
+
* naming the stack in a customer-facing document gives away more than it
|
|
95
|
+
* explains — `00-DOMAIN.md` §7 and {@link TestingBasis.engines}. The engine
|
|
96
|
+
* name has leaked into rendered output twice already, so `'full'` prints
|
|
97
|
+
* what was found and never who found it.
|
|
98
|
+
*/
|
|
99
|
+
readonly evidenceVerbosity?: EvidenceVerbosity;
|
|
100
|
+
|
|
101
|
+
/** Whose document this is. See {@link Branding}. */
|
|
102
|
+
readonly branding?: Branding;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Open the report with a cover page. Defaults to `false`.
|
|
106
|
+
*
|
|
107
|
+
* **On the options rather than on {@link Branding}, and the split is
|
|
108
|
+
* deliberate.** Branding is org-level configuration, stored once and the
|
|
109
|
+
* same for every document; whether a particular render wants a cover is a
|
|
110
|
+
* per-report layout decision — a PDF for a client does, a Markdown export
|
|
111
|
+
* piped into a terminal does not.
|
|
112
|
+
*
|
|
113
|
+
* Markdown has no pages, so there it is a block at the top rather than a
|
|
114
|
+
* page of its own. The statement is the same; the format has one form for
|
|
115
|
+
* it.
|
|
116
|
+
*/
|
|
117
|
+
readonly coverPage?: boolean;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Open the body with a contents list, and give every section an anchor.
|
|
121
|
+
* Defaults to `false`.
|
|
122
|
+
*
|
|
123
|
+
* **The anchors are what a PDF needs, not the list.** Chromium derives a
|
|
124
|
+
* document outline from the heading structure, which is how a reader
|
|
125
|
+
* navigates a sixty-page report in a viewer's sidebar; the printed contents
|
|
126
|
+
* list is for whoever has it on paper. Both come from the same scan of the
|
|
127
|
+
* rendered headings, so a list entry cannot point at an anchor that is not
|
|
128
|
+
* there.
|
|
129
|
+
*
|
|
130
|
+
* Individual issues (`<h4>`) are left out on purpose: a report with sixty
|
|
131
|
+
* findings would have a contents list longer than its summary.
|
|
132
|
+
*/
|
|
133
|
+
readonly tableOfContents?: boolean;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Whose document this is — the marks on it, and the name on it.
|
|
138
|
+
*
|
|
139
|
+
* Per `00-TIER-MATRIX.md`: free-tier reports are watermarked, branding is
|
|
140
|
+
* `[branding]`, and white-label is Enterprise only. **None of that is enforced
|
|
141
|
+
* here.** This package has no idea what anyone is paying, and a pure function
|
|
142
|
+
* that consulted an entitlement would be the wrong place to find out; the
|
|
143
|
+
* caller passes what the caller is entitled to.
|
|
144
|
+
*/
|
|
145
|
+
export interface Branding {
|
|
146
|
+
/**
|
|
147
|
+
* The name that appears where Secureport's otherwise would — on an
|
|
148
|
+
* Attestation Letter, as the party who carried out the testing.
|
|
149
|
+
*
|
|
150
|
+
* {@link ReportOptions.preparedBy} wins over it when both are given, because
|
|
151
|
+
* it is the more specific statement: branding is who owns the document,
|
|
152
|
+
* `preparedBy` is who did the work, and they are not always the same party.
|
|
153
|
+
*/
|
|
154
|
+
readonly companyName?: string;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Accent colour for headings and rules, as a hex triplet — `#0a7`,
|
|
158
|
+
* `#00aa77`, or `#00aa77ff`.
|
|
159
|
+
*
|
|
160
|
+
* **Hex only, and the restriction is a security boundary rather than
|
|
161
|
+
* fussiness.** This value is interpolated into the document's `<style>`
|
|
162
|
+
* block, which is a context the HTML renderer's escaping does not protect:
|
|
163
|
+
* it escapes text nodes, and a colour of `red</style><script>…` would close
|
|
164
|
+
* the element and run. Anything that does not match the pattern is refused by
|
|
165
|
+
* {@link buildReportModel} rather than sanitised, because silently altering
|
|
166
|
+
* somebody's brand colour is its own kind of wrong.
|
|
167
|
+
*/
|
|
168
|
+
readonly primaryColour?: string;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Remove Secureport's own marks from the document entirely.
|
|
172
|
+
*
|
|
173
|
+
* An Attestation Letter with this set and no name to put in Secureport's
|
|
174
|
+
* place is refused: a formal statement that testing was carried out has to
|
|
175
|
+
* say who carried it out, and an unattributed one is worth nothing to the
|
|
176
|
+
* auditor it exists for.
|
|
177
|
+
*/
|
|
178
|
+
readonly whiteLabel?: boolean;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Text printed across every page — what the free tier stamps on a report.
|
|
182
|
+
*
|
|
183
|
+
* Rendered as an element rather than through CSS `content`, so it goes
|
|
184
|
+
* through the same escaping as every other value from outside.
|
|
185
|
+
*/
|
|
186
|
+
readonly watermark?: string;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The logo, as a `data:` URI — never a URL.
|
|
190
|
+
*
|
|
191
|
+
* **The restriction is what keeps a report a single file.** `renderHtml`
|
|
192
|
+
* emits a self-contained document with its stylesheet inlined and nothing
|
|
193
|
+
* linked, which is what lets P7 render it through Chromium with no network
|
|
194
|
+
* and get the same bytes in CI as on a laptop. One `<img src="https://…">`
|
|
195
|
+
* would trade that for a logo, and trade it silently: the report would look
|
|
196
|
+
* right on the machine that rendered it and lose its mark for a reader
|
|
197
|
+
* offline, or three months later when the URL stops resolving.
|
|
198
|
+
*
|
|
199
|
+
* Refused by {@link buildReportModel} rather than fetched, because a pure
|
|
200
|
+
* function that reached the network would stop being one.
|
|
201
|
+
*
|
|
202
|
+
* Rendered into an `<img>`, which is also why an SVG data URI is allowed: an
|
|
203
|
+
* image context does not execute script, where inlining the same markup into
|
|
204
|
+
* the document would.
|
|
205
|
+
*/
|
|
206
|
+
readonly logo?: string;
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Address, registration number, contact — whatever belongs under the name on
|
|
210
|
+
* a cover page, one line each.
|
|
211
|
+
*
|
|
212
|
+
* Only shown when {@link ReportOptions.coverPage} is set, because there is
|
|
213
|
+
* nowhere else in the document these belong.
|
|
214
|
+
*/
|
|
215
|
+
readonly companyDetails?: readonly string[];
|
|
46
216
|
}
|
|
47
217
|
|
|
218
|
+
/**
|
|
219
|
+
* How much of an issue's supporting evidence a report prints.
|
|
220
|
+
*
|
|
221
|
+
* See {@link ReportOptions.evidenceVerbosity} for what each level shows.
|
|
222
|
+
*/
|
|
223
|
+
export type EvidenceVerbosity = 'none' | 'summary' | 'full';
|
|
224
|
+
|
|
48
225
|
/**
|
|
49
226
|
* How the findings in a report were actually produced.
|
|
50
227
|
*
|
|
@@ -94,6 +271,24 @@ export interface TestingBasis {
|
|
|
94
271
|
readonly statement: string;
|
|
95
272
|
}
|
|
96
273
|
|
|
274
|
+
/**
|
|
275
|
+
* What a severity floor kept out of a report's detail sections.
|
|
276
|
+
*
|
|
277
|
+
* The issues are still open, still counted and still the target's problem;
|
|
278
|
+
* only the pages describing them are gone. That distinction is the reason this
|
|
279
|
+
* is reported at all rather than silently applied.
|
|
280
|
+
*/
|
|
281
|
+
export interface OmittedIssues {
|
|
282
|
+
/** How many outstanding issues were left out. Never `0` — the field is absent instead. */
|
|
283
|
+
readonly count: number;
|
|
284
|
+
|
|
285
|
+
/** The floor that excluded them. */
|
|
286
|
+
readonly floor: Severity;
|
|
287
|
+
|
|
288
|
+
/** One sentence, ready to print, so no two renderers word it differently. */
|
|
289
|
+
readonly statement: string;
|
|
290
|
+
}
|
|
291
|
+
|
|
97
292
|
/** A group of issues sharing a severity, most urgent first. */
|
|
98
293
|
export interface ReportSection {
|
|
99
294
|
/** The severity this section covers. */
|
|
@@ -123,9 +318,38 @@ export interface ReportModel {
|
|
|
123
318
|
/** Who it is for, if stated. */
|
|
124
319
|
readonly preparedFor?: string;
|
|
125
320
|
|
|
321
|
+
/**
|
|
322
|
+
* Who the document says carried out the testing.
|
|
323
|
+
*
|
|
324
|
+
* Resolved once here from {@link ReportOptions.preparedBy},
|
|
325
|
+
* {@link Branding.companyName} and the default, because both renderers used
|
|
326
|
+
* to write `preparedBy ?? 'Secureport'` themselves — two copies of a default
|
|
327
|
+
* that white-labelling has to change in both places or not at all.
|
|
328
|
+
*/
|
|
329
|
+
readonly attestor: string;
|
|
330
|
+
|
|
331
|
+
/** Whose document this is, as given. Absent when nothing was branded. */
|
|
332
|
+
readonly branding?: Branding;
|
|
333
|
+
|
|
334
|
+
/** Whether to open with a cover, resolved from {@link ReportOptions.coverPage}. */
|
|
335
|
+
readonly coverPage: boolean;
|
|
336
|
+
|
|
337
|
+
/** Whether to print a contents list, resolved from {@link ReportOptions.tableOfContents}. */
|
|
338
|
+
readonly tableOfContents: boolean;
|
|
339
|
+
|
|
126
340
|
/** How the findings were produced. */
|
|
127
341
|
readonly basis: TestingBasis;
|
|
128
342
|
|
|
343
|
+
/**
|
|
344
|
+
* How much of each issue's evidence to print, resolved from
|
|
345
|
+
* {@link ReportOptions.evidenceVerbosity}.
|
|
346
|
+
*
|
|
347
|
+
* On the model rather than read from the options by each renderer, so the
|
|
348
|
+
* Markdown and the HTML cannot disagree about how much of an issue they are
|
|
349
|
+
* showing — the same reason every number here is derived once.
|
|
350
|
+
*/
|
|
351
|
+
readonly evidenceVerbosity: EvidenceVerbosity;
|
|
352
|
+
|
|
129
353
|
/**
|
|
130
354
|
* Outstanding issues grouped by severity, most urgent first, empties dropped.
|
|
131
355
|
*
|
|
@@ -140,10 +364,23 @@ export interface ReportModel {
|
|
|
140
364
|
*
|
|
141
365
|
* Kept and shown rather than dropped: "what you fixed" is the story the
|
|
142
366
|
* product exists to tell, and a report that silently omits it throws away its
|
|
143
|
-
* best evidence.
|
|
367
|
+
* best evidence. Empty when {@link ReportOptions.includeResolved} is `false`,
|
|
368
|
+
* which is the caller saying so deliberately.
|
|
144
369
|
*/
|
|
145
370
|
readonly resolved: readonly SnapshotIssue[];
|
|
146
371
|
|
|
372
|
+
/**
|
|
373
|
+
* What {@link ReportOptions.severityFloor} kept out of {@link ReportModel.sections}.
|
|
374
|
+
* Absent when no floor was set, or when one was set and removed nothing.
|
|
375
|
+
*
|
|
376
|
+
* Carried as data and not only as prose, because `00-DOMAIN.md` §7 says what
|
|
377
|
+
* a report does not establish travels as data too — so a consumer rendering
|
|
378
|
+
* its own view cannot drop the disclosure simply by not printing a sentence.
|
|
379
|
+
* `statement` is that sentence, derived once here so the renderers and
|
|
380
|
+
* {@link ReportModel.limitations} cannot word it three different ways.
|
|
381
|
+
*/
|
|
382
|
+
readonly omitted?: OmittedIssues;
|
|
383
|
+
|
|
147
384
|
/** Open and regressed issues, by severity. What the reader owes work on. */
|
|
148
385
|
readonly outstanding: Readonly<Record<Severity, number>>;
|
|
149
386
|
|
|
@@ -195,6 +432,24 @@ export interface ReportModel {
|
|
|
195
432
|
*/
|
|
196
433
|
export type RetestVerdict = 'fixed' | 'still_present' | 'returned' | 'not_retested' | 'new';
|
|
197
434
|
|
|
435
|
+
/**
|
|
436
|
+
* How each verdict is printed.
|
|
437
|
+
*
|
|
438
|
+
* Exported because it is printed, and a printed vocabulary is a contract —
|
|
439
|
+
* `00-DOMAIN.md` §7 says so of the verdicts themselves. It was defined
|
|
440
|
+
* identically in both renderers, which PDF and DOCX would have made four
|
|
441
|
+
* copies of, each one a chance for `not_retested` to be worded differently in
|
|
442
|
+
* the format somebody actually reads. The underscores are an implementation
|
|
443
|
+
* detail of the union and should never reach a page.
|
|
444
|
+
*/
|
|
445
|
+
export const VERDICT_LABELS: Readonly<Record<RetestVerdict, string>> = Object.freeze({
|
|
446
|
+
fixed: 'fixed',
|
|
447
|
+
still_present: 'still present',
|
|
448
|
+
returned: 'returned',
|
|
449
|
+
not_retested: 'not retested',
|
|
450
|
+
new: 'new since',
|
|
451
|
+
});
|
|
452
|
+
|
|
198
453
|
/** One issue, with what the retest established about it. */
|
|
199
454
|
export interface RetestEntry {
|
|
200
455
|
/** The issue as the snapshot sees it. */
|
|
@@ -288,7 +543,11 @@ function verdictFor(entry: SnapshotIssue): RetestVerdict {
|
|
|
288
543
|
}
|
|
289
544
|
|
|
290
545
|
/** Works out what a report cannot establish, from the run rather than a template. */
|
|
291
|
-
function describeLimitations(
|
|
546
|
+
function describeLimitations(
|
|
547
|
+
snapshot: Snapshot,
|
|
548
|
+
basis: TestingBasis,
|
|
549
|
+
omitted: OmittedIssues | undefined,
|
|
550
|
+
): string[] {
|
|
292
551
|
const limitations: string[] = [];
|
|
293
552
|
|
|
294
553
|
if (!basis.includesManual) {
|
|
@@ -320,6 +579,11 @@ function describeLimitations(snapshot: Snapshot, basis: TestingBasis): string[]
|
|
|
320
579
|
'suppressed and excluded from the counts above. They are listed in full in the appendix.',
|
|
321
580
|
);
|
|
322
581
|
}
|
|
582
|
+
// Immediately after the suppression sentence and before the compliance one,
|
|
583
|
+
// because it answers the same question a reader is entitled to ask: what am
|
|
584
|
+
// I not being shown? A floor that removed pages silently would be the
|
|
585
|
+
// over-claim §7 exists to stop, in a quieter form than the appendix one.
|
|
586
|
+
if (omitted !== undefined) limitations.push(omitted.statement);
|
|
323
587
|
limitations.push(
|
|
324
588
|
'This document does not certify compliance with any standard, framework or regulation.',
|
|
325
589
|
);
|
|
@@ -327,6 +591,12 @@ function describeLimitations(snapshot: Snapshot, basis: TestingBasis): string[]
|
|
|
327
591
|
return limitations;
|
|
328
592
|
}
|
|
329
593
|
|
|
594
|
+
/**
|
|
595
|
+
* Hex triplets only. See {@link Branding.primaryColour} for why this is a
|
|
596
|
+
* refusal rather than a sanitisation.
|
|
597
|
+
*/
|
|
598
|
+
const HEX_COLOUR = /^#(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/iu;
|
|
599
|
+
|
|
330
600
|
const DEFAULT_TITLES: Readonly<Record<ReportKind, string>> = Object.freeze({
|
|
331
601
|
pen: 'Penetration Test Report',
|
|
332
602
|
vap: 'Vulnerability Assessment Report',
|
|
@@ -399,6 +669,16 @@ export function buildReportModel(snapshot: Snapshot, options: ReportOptions): Re
|
|
|
399
669
|
const bySeverity = new Map<Severity, SnapshotIssue[]>();
|
|
400
670
|
const resolved: SnapshotIssue[] = [];
|
|
401
671
|
let breached = 0;
|
|
672
|
+
let omittedBelowFloor = 0;
|
|
673
|
+
|
|
674
|
+
// `SEVERITY_ORDER` is most-severe-first, so "at or above the floor" is a
|
|
675
|
+
// *lower* index. Same trap `describeRetest` records above, from the other
|
|
676
|
+
// direction: `severityRank` counts the opposite way and using it here would
|
|
677
|
+
// floor out everything except the advisories.
|
|
678
|
+
const floorAt =
|
|
679
|
+
options.severityFloor === undefined
|
|
680
|
+
? SEVERITY_ORDER.length
|
|
681
|
+
: SEVERITY_ORDER.indexOf(options.severityFloor);
|
|
402
682
|
|
|
403
683
|
for (const entry of snapshot.issues) {
|
|
404
684
|
if (entry.issue.status === 'resolved') {
|
|
@@ -407,14 +687,22 @@ export function buildReportModel(snapshot: Snapshot, options: ReportOptions): Re
|
|
|
407
687
|
}
|
|
408
688
|
|
|
409
689
|
const severity = entry.issue.effectiveSeverity;
|
|
410
|
-
const group = bySeverity.get(severity);
|
|
411
|
-
if (group) group.push(entry);
|
|
412
|
-
else bySeverity.set(severity, [entry]);
|
|
413
690
|
|
|
691
|
+
// Counted before the floor is consulted, deliberately: the floor decides
|
|
692
|
+
// what is *listed*, never what is *counted*.
|
|
414
693
|
if (entry.issue.status === 'open' || entry.issue.status === 'regressed') {
|
|
415
694
|
outstanding[severity]++;
|
|
416
695
|
if (entry.slaStatus === 'breached') breached++;
|
|
417
696
|
}
|
|
697
|
+
|
|
698
|
+
if (SEVERITY_ORDER.indexOf(severity) > floorAt) {
|
|
699
|
+
omittedBelowFloor++;
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const group = bySeverity.get(severity);
|
|
704
|
+
if (group) group.push(entry);
|
|
705
|
+
else bySeverity.set(severity, [entry]);
|
|
418
706
|
}
|
|
419
707
|
resolved.sort((a, b) => b.issue.lastSeen.getTime() - a.issue.lastSeen.getTime());
|
|
420
708
|
|
|
@@ -430,6 +718,20 @@ export function buildReportModel(snapshot: Snapshot, options: ReportOptions): Re
|
|
|
430
718
|
});
|
|
431
719
|
}
|
|
432
720
|
|
|
721
|
+
const floor = options.severityFloor;
|
|
722
|
+
const omitted: OmittedIssues | undefined =
|
|
723
|
+
omittedBelowFloor === 0 || floor === undefined
|
|
724
|
+
? undefined
|
|
725
|
+
: {
|
|
726
|
+
count: omittedBelowFloor,
|
|
727
|
+
floor,
|
|
728
|
+
statement:
|
|
729
|
+
`${omittedBelowFloor} issue${omittedBelowFloor === 1 ? '' : 's'} below ${floor} severity ` +
|
|
730
|
+
`${omittedBelowFloor === 1 ? 'is' : 'are'} not listed individually in this document. ` +
|
|
731
|
+
`${omittedBelowFloor === 1 ? 'It remains' : 'They remain'} open and ` +
|
|
732
|
+
`${omittedBelowFloor === 1 ? 'is' : 'are'} included in every count above.`,
|
|
733
|
+
};
|
|
734
|
+
|
|
433
735
|
const outstandingTotal = SEVERITY_ORDER.reduce((sum, s) => sum + outstanding[s], 0);
|
|
434
736
|
const basis = describeBasis(snapshot);
|
|
435
737
|
const retest = describeRetest(snapshot);
|
|
@@ -442,6 +744,35 @@ export function buildReportModel(snapshot: Snapshot, options: ReportOptions): Re
|
|
|
442
744
|
);
|
|
443
745
|
}
|
|
444
746
|
|
|
747
|
+
const branding = options.branding;
|
|
748
|
+
const colour = branding?.primaryColour;
|
|
749
|
+
if (colour !== undefined && !HEX_COLOUR.test(colour)) {
|
|
750
|
+
throw new TypeError(
|
|
751
|
+
`primaryColour must be a hex triplet such as #0a7 or #00aa77, not ${JSON.stringify(colour)}: ` +
|
|
752
|
+
'it is interpolated into the document stylesheet, where anything else could end the element',
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const logo = branding?.logo;
|
|
757
|
+
if (logo !== undefined && !logo.startsWith('data:image/')) {
|
|
758
|
+
throw new TypeError(
|
|
759
|
+
`logo must be a data: URI, not ${JSON.stringify(logo.slice(0, 40))}: ` +
|
|
760
|
+
'a linked image would make the report depend on a network at render time, ' +
|
|
761
|
+
'which is the guarantee that lets it be rendered to PDF reproducibly',
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const attestor = options.preparedBy ?? branding?.companyName;
|
|
766
|
+
// An attestation is a statement that a named party carried out testing. With
|
|
767
|
+
// Secureport's name removed and nothing put in its place there is no such
|
|
768
|
+
// party, and the document would assert something on nobody's behalf.
|
|
769
|
+
if (attestor === undefined && branding?.whiteLabel === true && options.kind === 'attest') {
|
|
770
|
+
throw new TypeError(
|
|
771
|
+
'a white-labelled attestation must name who carried out the testing: ' +
|
|
772
|
+
'set branding.companyName or preparedBy',
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
|
|
445
776
|
return {
|
|
446
777
|
kind: options.kind,
|
|
447
778
|
title: options.title ?? DEFAULT_TITLES[options.kind],
|
|
@@ -449,10 +780,16 @@ export function buildReportModel(snapshot: Snapshot, options: ReportOptions): Re
|
|
|
449
780
|
generatedAt: options.now,
|
|
450
781
|
...(options.preparedBy === undefined ? {} : { preparedBy: options.preparedBy }),
|
|
451
782
|
...(options.preparedFor === undefined ? {} : { preparedFor: options.preparedFor }),
|
|
783
|
+
attestor: attestor ?? 'Secureport',
|
|
784
|
+
coverPage: options.coverPage === true,
|
|
785
|
+
tableOfContents: options.tableOfContents === true,
|
|
786
|
+
...(branding === undefined ? {} : { branding }),
|
|
452
787
|
basis,
|
|
453
|
-
|
|
788
|
+
evidenceVerbosity: options.evidenceVerbosity ?? 'summary',
|
|
789
|
+
limitations: describeLimitations(snapshot, basis, omitted),
|
|
454
790
|
sections,
|
|
455
|
-
resolved,
|
|
791
|
+
resolved: options.includeResolved === false ? [] : resolved,
|
|
792
|
+
...(omitted === undefined ? {} : { omitted }),
|
|
456
793
|
outstanding,
|
|
457
794
|
outstandingTotal,
|
|
458
795
|
exposureScore: snapshot.run.exposureScore,
|