@secureport/core 0.2.1 → 0.4.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 (100) hide show
  1. package/README.md +146 -33
  2. package/dist/coverage.d.ts +21 -0
  3. package/dist/coverage.d.ts.map +1 -0
  4. package/dist/coverage.js +65 -0
  5. package/dist/coverage.js.map +1 -0
  6. package/dist/finding.d.ts +89 -0
  7. package/dist/finding.d.ts.map +1 -0
  8. package/dist/finding.js +2 -0
  9. package/dist/finding.js.map +1 -0
  10. package/dist/fingerprint.d.ts +185 -0
  11. package/dist/fingerprint.d.ts.map +1 -0
  12. package/dist/fingerprint.js +247 -0
  13. package/dist/fingerprint.js.map +1 -0
  14. package/dist/import/burp.d.ts +19 -0
  15. package/dist/import/burp.d.ts.map +1 -0
  16. package/dist/import/burp.js +114 -0
  17. package/dist/import/burp.js.map +1 -0
  18. package/dist/import/generic.d.ts +90 -0
  19. package/dist/import/generic.d.ts.map +1 -0
  20. package/dist/import/generic.js +159 -0
  21. package/dist/import/generic.js.map +1 -0
  22. package/dist/import/nessus.d.ts +32 -0
  23. package/dist/import/nessus.d.ts.map +1 -0
  24. package/dist/import/nessus.js +125 -0
  25. package/dist/import/nessus.js.map +1 -0
  26. package/dist/import/nuclei.d.ts +39 -0
  27. package/dist/import/nuclei.d.ts.map +1 -0
  28. package/dist/import/nuclei.js +115 -0
  29. package/dist/import/nuclei.js.map +1 -0
  30. package/dist/import/xml.d.ts +47 -0
  31. package/dist/import/xml.d.ts.map +1 -0
  32. package/dist/import/xml.js +157 -0
  33. package/dist/import/xml.js.map +1 -0
  34. package/dist/import/zap.d.ts +26 -0
  35. package/dist/import/zap.d.ts.map +1 -0
  36. package/dist/import/zap.js +119 -0
  37. package/dist/import/zap.js.map +1 -0
  38. package/dist/index.d.ts +41 -2
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +29 -2
  41. package/dist/index.js.map +1 -1
  42. package/dist/issue.d.ts +190 -11
  43. package/dist/issue.d.ts.map +1 -1
  44. package/dist/reconcile.d.ts +115 -10
  45. package/dist/reconcile.d.ts.map +1 -1
  46. package/dist/reconcile.js +306 -12
  47. package/dist/reconcile.js.map +1 -1
  48. package/dist/report/html.d.ts +21 -0
  49. package/dist/report/html.d.ts.map +1 -0
  50. package/dist/report/html.js +324 -0
  51. package/dist/report/html.js.map +1 -0
  52. package/dist/report/json.d.ts +81 -0
  53. package/dist/report/json.d.ts.map +1 -0
  54. package/dist/report/json.js +47 -0
  55. package/dist/report/json.js.map +1 -0
  56. package/dist/report/markdown.d.ts +24 -0
  57. package/dist/report/markdown.d.ts.map +1 -0
  58. package/dist/report/markdown.js +304 -0
  59. package/dist/report/markdown.js.map +1 -0
  60. package/dist/report/model.d.ts +215 -0
  61. package/dist/report/model.d.ts.map +1 -0
  62. package/dist/report/model.js +197 -0
  63. package/dist/report/model.js.map +1 -0
  64. package/dist/run.d.ts +134 -0
  65. package/dist/run.d.ts.map +1 -0
  66. package/dist/run.js +2 -0
  67. package/dist/run.js.map +1 -0
  68. package/dist/severity.d.ts +191 -0
  69. package/dist/severity.d.ts.map +1 -0
  70. package/dist/severity.js +171 -0
  71. package/dist/severity.js.map +1 -0
  72. package/dist/snapshot-builder.d.ts +78 -0
  73. package/dist/snapshot-builder.d.ts.map +1 -0
  74. package/dist/snapshot-builder.js +172 -0
  75. package/dist/snapshot-builder.js.map +1 -0
  76. package/dist/snapshot.d.ts +125 -0
  77. package/dist/snapshot.d.ts.map +1 -0
  78. package/dist/snapshot.js +2 -0
  79. package/dist/snapshot.js.map +1 -0
  80. package/package.json +5 -4
  81. package/src/coverage.ts +65 -0
  82. package/src/finding.ts +112 -0
  83. package/src/fingerprint.ts +315 -0
  84. package/src/import/burp.ts +126 -0
  85. package/src/import/generic.ts +258 -0
  86. package/src/import/nessus.ts +136 -0
  87. package/src/import/nuclei.ts +173 -0
  88. package/src/import/xml.ts +187 -0
  89. package/src/import/zap.ts +161 -0
  90. package/src/index.ts +75 -2
  91. package/src/issue.ts +244 -11
  92. package/src/reconcile.ts +421 -17
  93. package/src/report/html.ts +449 -0
  94. package/src/report/json.ts +134 -0
  95. package/src/report/markdown.ts +435 -0
  96. package/src/report/model.ts +462 -0
  97. package/src/run.ts +163 -0
  98. package/src/severity.ts +250 -0
  99. package/src/snapshot-builder.ts +225 -0
  100. package/src/snapshot.ts +146 -0
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Every {@link Severity}, most urgent first.
3
+ *
4
+ * Iteration order is part of the contract: report sections, severity
5
+ * breakdowns and count tables all render in this order, so a reader sees the
6
+ * same shape everywhere.
7
+ */
8
+ export const SEVERITY_ORDER = ['critical', 'high', 'medium', 'low', 'advisory'];
9
+ /**
10
+ * Rank of a {@link Severity}, where a **higher number is more urgent**.
11
+ *
12
+ * Use this to compare severities rather than comparing the strings, which sort
13
+ * alphabetically and would put `advisory` above `critical`.
14
+ *
15
+ * @param severity - The severity to rank.
16
+ * @returns `4` for `critical` down to `0` for `advisory`.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * severityRank(finding.detectedSeverity) > severityRank(issue.severityAtIgnore);
21
+ * ```
22
+ */
23
+ export function severityRank(severity) {
24
+ return SEVERITY_ORDER.length - 1 - SEVERITY_ORDER.indexOf(severity);
25
+ }
26
+ /**
27
+ * Exposure-score weight for each {@link Severity}.
28
+ *
29
+ * `exposure_score = Σ over open issues of weight × min(daysOpen, 90)`.
30
+ *
31
+ * `advisory` weighs nothing, so advisories never inflate the number — they are
32
+ * tracked and reported, but they do not represent exposure.
33
+ *
34
+ * These weights are **placeholders** until ten design partners have looked at
35
+ * the number, and are stated as such in `00-DOMAIN.md` §8. Treat a change to
36
+ * them as a change to a published contract.
37
+ */
38
+ export const SEVERITY_WEIGHTS = Object.freeze({
39
+ critical: 10,
40
+ high: 5,
41
+ medium: 2,
42
+ low: 1,
43
+ advisory: 0,
44
+ });
45
+ /**
46
+ * Every {@link SeveritySource}, most authoritative first.
47
+ *
48
+ * The order is the precedence rule: when two sources offer a severity for the
49
+ * same finding, the one appearing earlier here wins.
50
+ */
51
+ export const SEVERITY_SOURCE_PRECEDENCE = [
52
+ 'explicit',
53
+ 'cvss',
54
+ 'engine_default',
55
+ 'advisory',
56
+ ];
57
+ /**
58
+ * Maps a CVSS base score to a {@link Severity}.
59
+ *
60
+ * Uses the CVSS v3.1 qualitative severity rating scale unchanged, so a score
61
+ * rated "High" by any other tool is rated `high` here. The one adaptation is
62
+ * at the bottom: CVSS calls `0.0` "None", and this model has no "none", so it
63
+ * becomes `advisory`.
64
+ *
65
+ * | CVSS score | CVSS rating | {@link Severity} |
66
+ * | ----------- | ----------- | ---------------- |
67
+ * | 9.0 – 10.0 | Critical | `critical` |
68
+ * | 7.0 – 8.9 | High | `high` |
69
+ * | 4.0 – 6.9 | Medium | `medium` |
70
+ * | 0.1 – 3.9 | Low | `low` |
71
+ * | 0.0 | None | `advisory` |
72
+ *
73
+ * @param score - A CVSS base score between 0 and 10.
74
+ * @returns The corresponding severity.
75
+ * @throws RangeError If `score` is outside 0–10 or is not a number. A score
76
+ * that cannot be mapped is a data problem worth surfacing, not something to
77
+ * silently round into `advisory`.
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * severityFromCvss(9.8); // 'critical'
82
+ * severityFromCvss(0); // 'advisory'
83
+ * ```
84
+ */
85
+ export function severityFromCvss(score) {
86
+ if (!Number.isFinite(score) || score < 0 || score > 10) {
87
+ throw new RangeError(`CVSS score must be between 0 and 10, received ${String(score)}`);
88
+ }
89
+ if (score >= 9)
90
+ return 'critical';
91
+ if (score >= 7)
92
+ return 'high';
93
+ if (score >= 4)
94
+ return 'medium';
95
+ if (score > 0)
96
+ return 'low';
97
+ return 'advisory';
98
+ }
99
+ /**
100
+ * The default remediation windows.
101
+ *
102
+ * Chosen to be defensible rather than derived: they line up with the windows
103
+ * most vulnerability-management programmes already run, so a customer can
104
+ * adopt them without an argument and override them without a migration.
105
+ *
106
+ * `advisory` has no deadline at all. Giving a zero-weight severity a due date
107
+ * would manufacture breaches out of things nobody agreed to fix.
108
+ *
109
+ * | Severity | Window |
110
+ * | ---------- | -------- |
111
+ * | `critical` | 7 days |
112
+ * | `high` | 30 days |
113
+ * | `medium` | 90 days |
114
+ * | `low` | 180 days |
115
+ * | `advisory` | none |
116
+ *
117
+ * **These are defaults, not the domain.** `00-DOMAIN.md` says only that
118
+ * `sla_due_at` is "derived from effective severity"; the numbers are this
119
+ * package's opinion and are meant to be replaced by an org's own policy.
120
+ */
121
+ export const DEFAULT_SLA_POLICY = Object.freeze({
122
+ durationDays: Object.freeze({
123
+ critical: 7,
124
+ high: 30,
125
+ medium: 90,
126
+ low: 180,
127
+ advisory: null,
128
+ }),
129
+ dueSoonDays: 7,
130
+ });
131
+ /**
132
+ * When an issue of this severity, first seen at this moment, is due.
133
+ *
134
+ * Pure: the deadline is computed from the arguments alone, so the same inputs
135
+ * always give the same answer and a test needs no control over the clock.
136
+ *
137
+ * @param severity - The issue's **effective** severity, not its detected one —
138
+ * an override is a deliberate statement about how urgent something is, and the
139
+ * deadline should follow it.
140
+ * @param firstSeen - When the issue was first seen. Never the current run:
141
+ * `first_seen` is never reset, so neither is the deadline (invariant 4).
142
+ * @param policy - The remediation windows to apply.
143
+ * @returns The deadline, or `null` where the severity has no window.
144
+ */
145
+ export function slaDueAt(severity, firstSeen, policy = DEFAULT_SLA_POLICY) {
146
+ const days = policy.durationDays[severity];
147
+ if (days === null)
148
+ return null;
149
+ return new Date(firstSeen.getTime() + days * 24 * 60 * 60 * 1000);
150
+ }
151
+ /**
152
+ * Where an issue stands against a deadline, as of a given moment.
153
+ *
154
+ * Pure, and takes `now` explicitly rather than reading the clock, so a report
155
+ * rendered for a past run reports the status *as of that run* rather than as of
156
+ * today.
157
+ *
158
+ * @param dueAt - The deadline from {@link slaDueAt}, or `null` for no deadline.
159
+ * @param now - The moment to evaluate against.
160
+ * @param policy - Supplies the `due_soon` window.
161
+ * @returns The status. Always `within` when `dueAt` is `null`.
162
+ */
163
+ export function slaStatus(dueAt, now, policy = DEFAULT_SLA_POLICY) {
164
+ if (dueAt === null)
165
+ return 'within';
166
+ if (now.getTime() > dueAt.getTime())
167
+ return 'breached';
168
+ const warnFrom = dueAt.getTime() - policy.dueSoonDays * 24 * 60 * 60 * 1000;
169
+ return now.getTime() >= warnFrom ? 'due_soon' : 'within';
170
+ }
171
+ //# sourceMappingURL=severity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"severity.js","sourceRoot":"","sources":["../src/severity.ts"],"names":[],"mappings":"AAaA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,CAAU,CAAC;AAEzF;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,YAAY,CAAC,QAAkB;IAC7C,OAAO,cAAc,CAAC,MAAM,GAAG,CAAC,GAAG,cAAc,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;AACtE,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAuC,MAAM,CAAC,MAAM,CAAC;IAChF,QAAQ,EAAE,EAAE;IACZ,IAAI,EAAE,CAAC;IACP,MAAM,EAAE,CAAC;IACT,GAAG,EAAE,CAAC;IACN,QAAQ,EAAE,CAAC;CACZ,CAAC,CAAC;AAqBH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG;IACxC,UAAU;IACV,MAAM;IACN,gBAAgB;IAChB,UAAU;CACF,CAAC;AAEX;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAa;IAC5C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,IAAI,KAAK,GAAG,EAAE,EAAE,CAAC;QACvD,MAAM,IAAI,UAAU,CAAC,iDAAiD,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACzF,CAAC;IACD,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,UAAU,CAAC;IAClC,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,MAAM,CAAC;IAC9B,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,QAAQ,CAAC;IAChC,IAAI,KAAK,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IAC5B,OAAO,UAAU,CAAC;AACpB,CAAC;AA8BD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAc,MAAM,CAAC,MAAM,CAAC;IACzD,YAAY,EAAE,MAAM,CAAC,MAAM,CAAC;QAC1B,QAAQ,EAAE,CAAC;QACX,IAAI,EAAE,EAAE;QACR,MAAM,EAAE,EAAE;QACV,GAAG,EAAE,GAAG;QACR,QAAQ,EAAE,IAAI;KACf,CAAC;IACF,WAAW,EAAE,CAAC;CACf,CAAC,CAAC;AAcH;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,QAAQ,CACtB,QAAkB,EAClB,SAAe,EACf,SAAoB,kBAAkB;IAEtC,MAAM,IAAI,GAAG,MAAM,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;IAC3C,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC/B,OAAO,IAAI,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,GAAG,IAAI,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;AACpE,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,SAAS,CACvB,KAAkB,EAClB,GAAS,EACT,SAAoB,kBAAkB;IAEtC,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,QAAQ,CAAC;IACpC,IAAI,GAAG,CAAC,OAAO,EAAE,GAAG,KAAK,CAAC,OAAO,EAAE;QAAE,OAAO,UAAU,CAAC;IACvD,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,EAAE,GAAG,MAAM,CAAC,WAAW,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC5E,OAAO,GAAG,CAAC,OAAO,EAAE,IAAI,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC;AAC3D,CAAC"}
@@ -0,0 +1,78 @@
1
+ import type { Finding } from './finding.js';
2
+ import type { Run } from './run.js';
3
+ import type { Snapshot, Target } from './snapshot.js';
4
+ import { type SlaPolicy } from './severity.js';
5
+ /**
6
+ * Everything needed to turn a run's findings into a report-ready snapshot.
7
+ */
8
+ export interface BuildSnapshotInput {
9
+ /** The system under test. */
10
+ readonly target: Target;
11
+ /** The run being reported on. */
12
+ readonly run: Run;
13
+ /** The findings it produced. */
14
+ readonly findings: readonly Finding[];
15
+ /**
16
+ * The previous snapshot, if there is one.
17
+ *
18
+ * **This is what makes a retest work with no account and no database.** The
19
+ * prior snapshot carries the issues, so a second run can tell what is new,
20
+ * what is still open, what got fixed and what came back — from a JSON file on
21
+ * disk. Parse it with {@link parseSnapshot}.
22
+ */
23
+ readonly previous?: Snapshot;
24
+ /** The moment being reported at. */
25
+ readonly now: Date;
26
+ /** Supplies ids for issues and events. */
27
+ readonly newId: () => string;
28
+ /** Remediation windows. */
29
+ readonly slaPolicy?: SlaPolicy;
30
+ /** Consecutive covering misses before an issue resolves. */
31
+ readonly autoResolveThreshold?: number;
32
+ /** How long the run took. */
33
+ readonly durationMs?: number;
34
+ }
35
+ /**
36
+ * Builds the {@link Snapshot} a report renders from.
37
+ *
38
+ * Runs {@link reconcile} over the previous snapshot's issues and this run's
39
+ * findings, then works out what changed for each issue and precomputes the
40
+ * things a template would otherwise each derive slightly differently —
41
+ * `daysOpen`, `slaStatus`, and which findings are the evidence.
42
+ *
43
+ * **Two runs on disk are enough.** No account, no database: the previous
44
+ * snapshot is the state, and this function is the only thing that has to
45
+ * understand it. That is the whole bring-your-own-findings path.
46
+ *
47
+ * Suppressed issues are separated out into {@link Snapshot.suppressed} and do
48
+ * not appear in {@link Snapshot.issues}, so a template cannot count an accepted
49
+ * risk as an open one (invariant 7).
50
+ *
51
+ * @param input - The run, its findings, and optionally the previous snapshot.
52
+ * @returns A snapshot, ready to render.
53
+ */
54
+ export declare function buildSnapshot(input: BuildSnapshotInput): Snapshot;
55
+ /**
56
+ * Parses a snapshot written to disk, restoring the `Date` objects JSON lost.
57
+ *
58
+ * `JSON.stringify` turns every date into a string and `JSON.parse` leaves it as
59
+ * one, so a snapshot round-tripped through a file is **not** a `Snapshot` — its
60
+ * dates are strings wearing the type. Everything that then compares or
61
+ * subtracts them silently produces nonsense rather than failing.
62
+ *
63
+ * Use this rather than `JSON.parse` for any snapshot read back from disk.
64
+ *
65
+ * **It checks the shape, because this is where untrusted JSON enters.** A
66
+ * snapshot arrives from a file somebody chose, and the signature promises a
67
+ * `Snapshot` — so returning whatever parsed and letting the caller discover
68
+ * the difference is the function lying about its own return type. The check is
69
+ * deliberately shallow: the top-level shape, which is enough to tell a
70
+ * snapshot from any other JSON document and to make the failure say so.
71
+ *
72
+ * @param json - The contents of a snapshot file.
73
+ * @returns The snapshot, with dates revived.
74
+ * @throws SyntaxError If the text is not valid JSON.
75
+ * @throws TypeError If the JSON parsed but is not a snapshot.
76
+ */
77
+ export declare function parseSnapshot(json: string): Snapshot;
78
+ //# sourceMappingURL=snapshot-builder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot-builder.d.ts","sourceRoot":"","sources":["../src/snapshot-builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AACpC,OAAO,KAAK,EAAe,QAAQ,EAAkC,MAAM,EAAE,MAAM,eAAe,CAAC;AACnG,OAAO,EAAiC,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AAG9E;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,6BAA6B;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAExB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC;IAElB,gCAAgC;IAChC,QAAQ,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAC;IAEtC;;;;;;;OAOG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAE7B,oCAAoC;IACpC,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC;IAEnB,0CAA0C;IAC1C,QAAQ,CAAC,KAAK,EAAE,MAAM,MAAM,CAAC;IAE7B,2BAA2B;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;IAE/B,4DAA4D;IAC5D,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAEvC,6BAA6B;IAC7B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAuBD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,QAAQ,CAyEjE;AAiBD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CA0BpD"}
@@ -0,0 +1,172 @@
1
+ import { DEFAULT_SLA_POLICY, slaStatus } from './severity.js';
2
+ import { reconcile } from './reconcile.js';
3
+ /**
4
+ * The key a finding and an issue share when they are the same issue.
5
+ *
6
+ * Mirrors the one reconciliation uses, including the version: a fingerprint
7
+ * means nothing without the algorithm that produced it. The separator is an
8
+ * escape rather than a literal control character — writing it as a raw byte has
9
+ * now put an invisible NUL into two source files, and `scripts/check-text.sh`
10
+ * exists because nothing else noticed.
11
+ */
12
+ function issueKey(of) {
13
+ return `${of.fingerprintVersion}\u0000${of.fingerprint}`;
14
+ }
15
+ /** Whole days between two moments, floored. */
16
+ function daysBetween(from, to) {
17
+ return Math.max(0, Math.floor((to.getTime() - from.getTime()) / 86_400_000));
18
+ }
19
+ /**
20
+ * Builds the {@link Snapshot} a report renders from.
21
+ *
22
+ * Runs {@link reconcile} over the previous snapshot's issues and this run's
23
+ * findings, then works out what changed for each issue and precomputes the
24
+ * things a template would otherwise each derive slightly differently —
25
+ * `daysOpen`, `slaStatus`, and which findings are the evidence.
26
+ *
27
+ * **Two runs on disk are enough.** No account, no database: the previous
28
+ * snapshot is the state, and this function is the only thing that has to
29
+ * understand it. That is the whole bring-your-own-findings path.
30
+ *
31
+ * Suppressed issues are separated out into {@link Snapshot.suppressed} and do
32
+ * not appear in {@link Snapshot.issues}, so a template cannot count an accepted
33
+ * risk as an open one (invariant 7).
34
+ *
35
+ * @param input - The run, its findings, and optionally the previous snapshot.
36
+ * @returns A snapshot, ready to render.
37
+ */
38
+ export function buildSnapshot(input) {
39
+ const slaPolicy = input.slaPolicy ?? DEFAULT_SLA_POLICY;
40
+ const priorIssues = [
41
+ ...(input.previous?.issues.map((i) => i.issue) ?? []),
42
+ ...(input.previous?.suppressed.map((s) => s.issue) ?? []),
43
+ ];
44
+ const priorById = new Map(priorIssues.map((i) => [i.id, i]));
45
+ const { issues, summary } = reconcile({
46
+ issues: priorIssues,
47
+ findings: input.findings,
48
+ run: input.run,
49
+ now: input.now,
50
+ newId: input.newId,
51
+ slaPolicy,
52
+ ...(input.autoResolveThreshold === undefined
53
+ ? {}
54
+ : { autoResolveThreshold: input.autoResolveThreshold }),
55
+ ...(input.durationMs === undefined ? {} : { durationMs: input.durationMs }),
56
+ });
57
+ // Evidence, grouped by the issue it belongs to.
58
+ const byFingerprint = new Map();
59
+ for (const finding of input.findings) {
60
+ const key = issueKey(finding);
61
+ const existing = byFingerprint.get(key);
62
+ if (existing)
63
+ existing.push(finding);
64
+ else
65
+ byFingerprint.set(key, [finding]);
66
+ }
67
+ const reported = [];
68
+ const suppressed = [];
69
+ for (const issue of issues) {
70
+ if (issue.status === 'ignored') {
71
+ suppressed.push({
72
+ issue,
73
+ reason: issue.ignoreReason ?? 'accepted_risk',
74
+ comment: issue.ignoreComment ?? '',
75
+ ignoredBy: issue.ignoredBy ?? 'unknown',
76
+ ignoredAt: issue.lastSeen,
77
+ ...(issue.ignoreExpiresAt === undefined ? {} : { expiresAt: issue.ignoreExpiresAt }),
78
+ severityAtIgnore: issue.severityAtIgnore ?? issue.effectiveSeverity,
79
+ });
80
+ continue;
81
+ }
82
+ const prior = priorById.get(issue.id);
83
+ let change;
84
+ if (prior === undefined)
85
+ change = 'new';
86
+ else if (issue.status === 'resolved')
87
+ change = 'resolved';
88
+ else if (issue.status === 'regressed' && prior.status !== 'regressed')
89
+ change = 'regressed';
90
+ else
91
+ change = 'still_open';
92
+ reported.push({
93
+ issue,
94
+ change,
95
+ // From firstSeen, which is never reset, so a regression does not make a
96
+ // year-old problem look new.
97
+ daysOpen: daysBetween(issue.firstSeen, input.now),
98
+ slaStatus: slaStatus(issue.slaDueAt ?? null, input.now, slaPolicy),
99
+ findings: byFingerprint.get(issueKey(issue)) ?? [],
100
+ });
101
+ }
102
+ return {
103
+ run: summary,
104
+ ...(input.previous === undefined ? {} : { baseline: input.previous.run }),
105
+ target: input.target,
106
+ issues: reported,
107
+ suppressed,
108
+ };
109
+ }
110
+ /** Every field in the snapshot tree that JSON turns into a string. */
111
+ const DATE_FIELDS = new Set([
112
+ 'createdAt',
113
+ 'firstSeen',
114
+ 'lastSeen',
115
+ 'resolvedAt',
116
+ 'reopenedAt',
117
+ 'slaDueAt',
118
+ 'severityOverriddenAt',
119
+ 'ignoreExpiresAt',
120
+ 'ignoredAt',
121
+ 'startedAt',
122
+ 'finishedAt',
123
+ ]);
124
+ /**
125
+ * Parses a snapshot written to disk, restoring the `Date` objects JSON lost.
126
+ *
127
+ * `JSON.stringify` turns every date into a string and `JSON.parse` leaves it as
128
+ * one, so a snapshot round-tripped through a file is **not** a `Snapshot` — its
129
+ * dates are strings wearing the type. Everything that then compares or
130
+ * subtracts them silently produces nonsense rather than failing.
131
+ *
132
+ * Use this rather than `JSON.parse` for any snapshot read back from disk.
133
+ *
134
+ * **It checks the shape, because this is where untrusted JSON enters.** A
135
+ * snapshot arrives from a file somebody chose, and the signature promises a
136
+ * `Snapshot` — so returning whatever parsed and letting the caller discover
137
+ * the difference is the function lying about its own return type. The check is
138
+ * deliberately shallow: the top-level shape, which is enough to tell a
139
+ * snapshot from any other JSON document and to make the failure say so.
140
+ *
141
+ * @param json - The contents of a snapshot file.
142
+ * @returns The snapshot, with dates revived.
143
+ * @throws SyntaxError If the text is not valid JSON.
144
+ * @throws TypeError If the JSON parsed but is not a snapshot.
145
+ */
146
+ export function parseSnapshot(json) {
147
+ const parsed = JSON.parse(json, (key, value) => {
148
+ if (DATE_FIELDS.has(key) && typeof value === 'string') {
149
+ const parsed = new Date(value);
150
+ if (!Number.isNaN(parsed.getTime()))
151
+ return parsed;
152
+ }
153
+ return value;
154
+ });
155
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
156
+ throw new TypeError('not a snapshot: expected a JSON object');
157
+ }
158
+ const record = parsed;
159
+ for (const field of ['run', 'target']) {
160
+ const value = record[field];
161
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
162
+ throw new TypeError(`not a snapshot: \`${field}\` is missing`);
163
+ }
164
+ }
165
+ for (const field of ['issues', 'suppressed']) {
166
+ if (!Array.isArray(record[field])) {
167
+ throw new TypeError(`not a snapshot: \`${field}\` is missing`);
168
+ }
169
+ }
170
+ return parsed;
171
+ }
172
+ //# sourceMappingURL=snapshot-builder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot-builder.js","sourceRoot":"","sources":["../src/snapshot-builder.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,125 @@
1
+ import type { Finding } from './finding.js';
2
+ import type { IgnoreReason, Issue } from './issue.js';
3
+ import type { RunSummary } from './run.js';
4
+ import type { Severity, SlaStatus } from './severity.js';
5
+ /**
6
+ * A system under test.
7
+ *
8
+ * Targets are the unit of authorisation as well as of organisation: staging,
9
+ * production and dev are separate targets with separate tokens and separate
10
+ * verification states.
11
+ */
12
+ export interface Target {
13
+ /** Unique id. */
14
+ readonly id: string;
15
+ /** Organisation it belongs to. */
16
+ readonly orgId: string;
17
+ /** Human-readable name, as it appears on a report cover. */
18
+ readonly name: string;
19
+ /** What is being tested — a URL, host or repository. */
20
+ readonly url: string;
21
+ }
22
+ /**
23
+ * What happened to an issue between the baseline and this run.
24
+ *
25
+ * This is the axis every report opens on, because it is the question a reader
26
+ * actually has: not "what is wrong" but "what changed since last time".
27
+ */
28
+ export type IssueChange = 'new' | 'still_open' | 'regressed' | 'resolved' | 'ignored';
29
+ /**
30
+ * An issue as a report sees it: its own state, plus everything a template
31
+ * would otherwise have to compute.
32
+ *
33
+ * The derived fields are computed once, when the snapshot is built, so that
34
+ * five report templates in three formats cannot each work out `daysOpen`
35
+ * slightly differently.
36
+ */
37
+ export interface SnapshotIssue {
38
+ /** The issue itself. */
39
+ readonly issue: Issue;
40
+ /** What happened to it relative to the baseline. */
41
+ readonly change: IssueChange;
42
+ /**
43
+ * Whole days between `firstSeen` and the run this snapshot is for.
44
+ *
45
+ * Measured from `firstSeen`, which is never reset, so a regression does not
46
+ * make a year-old problem look new.
47
+ */
48
+ readonly daysOpen: number;
49
+ /** Where it stands against its remediation deadline, as of this run. */
50
+ readonly slaStatus: SlaStatus;
51
+ /**
52
+ * The evidence behind it.
53
+ *
54
+ * Findings from this run, and from earlier ones where the report shows
55
+ * history. This is what makes a report checkable rather than merely
56
+ * assertive.
57
+ */
58
+ readonly findings: readonly Finding[];
59
+ }
60
+ /**
61
+ * A suppressed issue, as the appendix shows it.
62
+ *
63
+ * Deliberately its own shape rather than a flag on {@link SnapshotIssue}: the
64
+ * suppressed appendix answers a different question — not "what is wrong" but
65
+ * "what did you decide not to fix, and who decided it".
66
+ *
67
+ * **Every report carries this appendix, on by default.** It is an
68
+ * accepted-risk register, which is exactly what an auditor asks for, and
69
+ * hiding it would be both less useful and less honest.
70
+ */
71
+ export interface SuppressedIssue {
72
+ /** The issue itself. */
73
+ readonly issue: Issue;
74
+ /** Why it was suppressed. */
75
+ readonly reason: IgnoreReason;
76
+ /** The justification given at the time. */
77
+ readonly comment: string;
78
+ /** Who suppressed it. */
79
+ readonly ignoredBy: string;
80
+ /** When. */
81
+ readonly ignoredAt: Date;
82
+ /** When the suppression lapses, if it does. */
83
+ readonly expiresAt?: Date;
84
+ /** Detected severity at the moment it was suppressed. */
85
+ readonly severityAtIgnore: Severity;
86
+ }
87
+ /**
88
+ * Issue state as of a run, plus a baseline to compare against.
89
+ *
90
+ * **The report engine's only input.** Every template consumes a `Snapshot` and
91
+ * nothing else — no database handle, no network, no clock. That is what makes
92
+ * a report reproducible, testable against golden files, and renderable by a
93
+ * third party who has findings on disk and no account.
94
+ *
95
+ * **The snapshot is the artefact; a PDF is a view of it.** Reports are rendered
96
+ * on demand and never stored (invariant 12). Snapshots are small, deterministic
97
+ * JSON, so caching one is cheap where storing a rendered document would buy
98
+ * retention policy, signed-URL expiry, encryption review and a deletion
99
+ * obligation in exchange for saving a render.
100
+ */
101
+ export interface Snapshot {
102
+ /** The run this snapshot describes. */
103
+ readonly run: RunSummary;
104
+ /**
105
+ * What to compare against.
106
+ *
107
+ * The previous run for an ordinary report, or a specific named run for a
108
+ * retest. Absent for a first run, where there is nothing to compare to and
109
+ * every issue is `new`.
110
+ */
111
+ readonly baseline?: RunSummary;
112
+ /** The system under test. */
113
+ readonly target: Target;
114
+ /**
115
+ * Every issue in scope, with what changed and the evidence for it.
116
+ *
117
+ * Excludes suppressed issues, which appear in {@link Snapshot.suppressed}
118
+ * instead — the two lists are disjoint by construction so a template cannot
119
+ * accidentally count an accepted risk as an open one.
120
+ */
121
+ readonly issues: readonly SnapshotIssue[];
122
+ /** The accepted-risk register. */
123
+ readonly suppressed: readonly SuppressedIssue[];
124
+ }
125
+ //# sourceMappingURL=snapshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot.d.ts","sourceRoot":"","sources":["../src/snapshot.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAEzD;;;;;;GAMG;AACH,MAAM,WAAW,MAAM;IACrB,iBAAiB;IACjB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAEpB,kCAAkC;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAEvB,4DAA4D;IAC5D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,wDAAwD;IACxD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,YAAY,GAAG,WAAW,GAAG,UAAU,GAAG,SAAS,CAAC;AAEtF;;;;;;;GAOG;AACH,MAAM,WAAW,aAAa;IAC5B,wBAAwB;IACxB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IAEtB,oDAAoD;IACpD,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAE7B;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAE1B,wEAAwE;IACxE,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAE9B;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAC;CACvC;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,eAAe;IAC9B,wBAAwB;IACxB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IAEtB,6BAA6B;IAC7B,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAE9B,2CAA2C;IAC3C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAEzB,yBAAyB;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAE3B,YAAY;IACZ,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC;IAEzB,+CAA+C;IAC/C,QAAQ,CAAC,SAAS,CAAC,EAAE,IAAI,CAAC;IAE1B,yDAAyD;IACzD,QAAQ,CAAC,gBAAgB,EAAE,QAAQ,CAAC;CACrC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,QAAQ;IACvB,uCAAuC;IACvC,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC;IAEzB;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,UAAU,CAAC;IAE/B,6BAA6B;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAExB;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,CAAC;IAE1C,kCAAkC;IAClC,QAAQ,CAAC,UAAU,EAAE,SAAS,eAAe,EAAE,CAAC;CACjD"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=snapshot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../src/snapshot.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@secureport/core",
3
- "version": "0.2.1",
3
+ "version": "0.4.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",
@@ -18,9 +18,9 @@
18
18
  "nessus",
19
19
  "security-reporting"
20
20
  ],
21
- "homepage": "https://github.com/agbjordan/secureport/tree/main/packages/core#readme",
21
+ "homepage": "https://secureport.io/",
22
22
  "bugs": {
23
- "url": "https://github.com/agbjordan/secureport/issues"
23
+ "email": "hello@secureport.io"
24
24
  },
25
25
  "type": "module",
26
26
  "main": "./dist/index.js",
@@ -37,6 +37,7 @@
37
37
  "LICENSE"
38
38
  ],
39
39
  "devDependencies": {
40
+ "@types/node": "^22.20.1",
40
41
  "typescript": "^5.7.2"
41
42
  },
42
43
  "license": "MIT",
@@ -49,7 +50,7 @@
49
50
  "access": "public"
50
51
  },
51
52
  "scripts": {
52
- "build": "tsc -p tsconfig.json",
53
+ "build": "rm -rf dist && tsc -p tsconfig.json",
53
54
  "typecheck": "tsc --noEmit"
54
55
  }
55
56
  }