@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.
Files changed (51) hide show
  1. package/README.md +2 -1
  2. package/dist/finding.d.ts +9 -8
  3. package/dist/finding.d.ts.map +1 -1
  4. package/dist/fingerprint.d.ts +12 -5
  5. package/dist/fingerprint.d.ts.map +1 -1
  6. package/dist/fingerprint.js +74 -8
  7. package/dist/fingerprint.js.map +1 -1
  8. package/dist/import/nessus.d.ts +9 -6
  9. package/dist/import/nessus.d.ts.map +1 -1
  10. package/dist/import/nessus.js +9 -6
  11. package/dist/import/nessus.js.map +1 -1
  12. package/dist/index.d.ts +6 -3
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +3 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/issue.d.ts +1 -1
  17. package/dist/issue.d.ts.map +1 -1
  18. package/dist/reconcile.d.ts +71 -1
  19. package/dist/reconcile.d.ts.map +1 -1
  20. package/dist/reconcile.js +64 -4
  21. package/dist/reconcile.js.map +1 -1
  22. package/dist/refingerprint.d.ts +116 -0
  23. package/dist/refingerprint.d.ts.map +1 -0
  24. package/dist/refingerprint.js +190 -0
  25. package/dist/refingerprint.js.map +1 -0
  26. package/dist/report/anchors.d.ts +39 -0
  27. package/dist/report/anchors.d.ts.map +1 -0
  28. package/dist/report/anchors.js +73 -0
  29. package/dist/report/anchors.js.map +1 -0
  30. package/dist/report/html.d.ts.map +1 -1
  31. package/dist/report/html.js +126 -22
  32. package/dist/report/html.js.map +1 -1
  33. package/dist/report/markdown.d.ts.map +1 -1
  34. package/dist/report/markdown.js +112 -26
  35. package/dist/report/markdown.js.map +1 -1
  36. package/dist/report/model.d.ts +227 -1
  37. package/dist/report/model.d.ts.map +1 -1
  38. package/dist/report/model.js +87 -8
  39. package/dist/report/model.js.map +1 -1
  40. package/package.json +1 -1
  41. package/src/finding.ts +9 -8
  42. package/src/fingerprint.ts +76 -10
  43. package/src/import/nessus.ts +9 -6
  44. package/src/index.ts +14 -2
  45. package/src/issue.ts +1 -0
  46. package/src/reconcile.ts +121 -5
  47. package/src/refingerprint.ts +282 -0
  48. package/src/report/anchors.ts +96 -0
  49. package/src/report/html.ts +146 -26
  50. package/src/report/markdown.ts +125 -25
  51. package/src/report/model.ts +344 -7
@@ -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":"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"}
1
+ {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../../src/report/model.ts"],"names":[],"mappings":"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"}
@@ -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
- const group = bySeverity.get(severity);
148
- if (group)
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
- limitations: describeLimitations(snapshot, basis),
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,
@@ -1 +1 @@
1
- {"version":3,"file":"model.js","sourceRoot":"","sources":["../../src/report/model.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"model.js","sourceRoot":"","sources":["../../src/report/model.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@secureport/core",
3
- "version": "1.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "Shared domain model for Secureport. Imported by the hosted API the same way a third party would.",
5
5
  "keywords": [
6
6
  "security",
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 already a fingerprint component, so this field changes
98
- * neither. It exists because a port was otherwise unrecoverable from a
99
- * finding except by parsing its location, which is not something a consumer
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
- * Deliberately **not** added to the fingerprint inputs: doing so would move
103
- * every existing Nessus fingerprint and force a re-fingerprint migration,
104
- * which is a far worse outcome than a field being merely inconvenient to
105
- * read.
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
 
@@ -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 = 'fp_v1';
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
- * Anything that is not a parseable absolute URL — a bare host, a file path, a
84
- * `host:port` pair from a network scan — is normalised as a path alone. That is
85
- * deliberate: refusing to fingerprint a non-HTTP finding would exclude whole
86
- * classes of scanner from the model.
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
- return normalisePathOnly(trimmed);
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
- const tail = input.parameter ?? (input.port !== undefined ? String(input.port) : '');
310
- const material = [input.targetId, input.vulnKey, normaliseLocation(input.location), tail].join(
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
+ }