@secureport/core 0.2.1 → 0.3.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 +67 -31
- package/dist/coverage.d.ts +21 -0
- package/dist/coverage.d.ts.map +1 -0
- package/dist/coverage.js +65 -0
- package/dist/coverage.js.map +1 -0
- package/dist/finding.d.ts +89 -0
- package/dist/finding.d.ts.map +1 -0
- package/dist/finding.js +2 -0
- package/dist/finding.js.map +1 -0
- package/dist/fingerprint.d.ts +185 -0
- package/dist/fingerprint.d.ts.map +1 -0
- package/dist/fingerprint.js +247 -0
- package/dist/fingerprint.js.map +1 -0
- package/dist/import/nuclei.d.ts +39 -0
- package/dist/import/nuclei.d.ts.map +1 -0
- package/dist/import/nuclei.js +115 -0
- package/dist/import/nuclei.js.map +1 -0
- package/dist/import/zap.d.ts +26 -0
- package/dist/import/zap.d.ts.map +1 -0
- package/dist/import/zap.js +119 -0
- package/dist/import/zap.js.map +1 -0
- package/dist/index.d.ts +31 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -2
- package/dist/index.js.map +1 -1
- package/dist/issue.d.ts +190 -11
- package/dist/issue.d.ts.map +1 -1
- package/dist/reconcile.d.ts +115 -10
- package/dist/reconcile.d.ts.map +1 -1
- package/dist/reconcile.js +306 -12
- package/dist/reconcile.js.map +1 -1
- package/dist/run.d.ts +134 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +2 -0
- package/dist/run.js.map +1 -0
- package/dist/severity.d.ts +191 -0
- package/dist/severity.d.ts.map +1 -0
- package/dist/severity.js +171 -0
- package/dist/severity.js.map +1 -0
- package/dist/snapshot-builder.d.ts +70 -0
- package/dist/snapshot-builder.d.ts.map +1 -0
- package/dist/snapshot-builder.js +148 -0
- package/dist/snapshot-builder.js.map +1 -0
- package/dist/snapshot.d.ts +125 -0
- package/dist/snapshot.d.ts.map +1 -0
- package/dist/snapshot.js +2 -0
- package/dist/snapshot.js.map +1 -0
- package/package.json +3 -2
- package/src/coverage.ts +65 -0
- package/src/finding.ts +112 -0
- package/src/fingerprint.ts +315 -0
- package/src/import/nuclei.ts +173 -0
- package/src/import/zap.ts +161 -0
- package/src/index.ts +56 -2
- package/src/issue.ts +244 -11
- package/src/reconcile.ts +421 -17
- package/src/run.ts +163 -0
- package/src/severity.ts +250 -0
- package/src/snapshot-builder.ts +199 -0
- package/src/snapshot.ts +146 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How urgent an issue is.
|
|
3
|
+
*
|
|
4
|
+
* Five levels, ordered most to least urgent. `advisory` is the bottom of the
|
|
5
|
+
* scale rather than a separate "informational" category: it carries a weight of
|
|
6
|
+
* zero in the exposure score and no SLA, but it is still an issue, still
|
|
7
|
+
* tracked across runs, and still appears in reports.
|
|
8
|
+
*
|
|
9
|
+
* @see {@link SEVERITY_ORDER} for comparing two severities.
|
|
10
|
+
* @see {@link SEVERITY_WEIGHTS} for the exposure-score weights.
|
|
11
|
+
*/
|
|
12
|
+
export type Severity = 'critical' | 'high' | 'medium' | 'low' | 'advisory';
|
|
13
|
+
/**
|
|
14
|
+
* Every {@link Severity}, most urgent first.
|
|
15
|
+
*
|
|
16
|
+
* Iteration order is part of the contract: report sections, severity
|
|
17
|
+
* breakdowns and count tables all render in this order, so a reader sees the
|
|
18
|
+
* same shape everywhere.
|
|
19
|
+
*/
|
|
20
|
+
export declare const SEVERITY_ORDER: readonly ["critical", "high", "medium", "low", "advisory"];
|
|
21
|
+
/**
|
|
22
|
+
* Rank of a {@link Severity}, where a **higher number is more urgent**.
|
|
23
|
+
*
|
|
24
|
+
* Use this to compare severities rather than comparing the strings, which sort
|
|
25
|
+
* alphabetically and would put `advisory` above `critical`.
|
|
26
|
+
*
|
|
27
|
+
* @param severity - The severity to rank.
|
|
28
|
+
* @returns `4` for `critical` down to `0` for `advisory`.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* severityRank(finding.detectedSeverity) > severityRank(issue.severityAtIgnore);
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare function severityRank(severity: Severity): number;
|
|
36
|
+
/**
|
|
37
|
+
* Exposure-score weight for each {@link Severity}.
|
|
38
|
+
*
|
|
39
|
+
* `exposure_score = Σ over open issues of weight × min(daysOpen, 90)`.
|
|
40
|
+
*
|
|
41
|
+
* `advisory` weighs nothing, so advisories never inflate the number — they are
|
|
42
|
+
* tracked and reported, but they do not represent exposure.
|
|
43
|
+
*
|
|
44
|
+
* These weights are **placeholders** until ten design partners have looked at
|
|
45
|
+
* the number, and are stated as such in `00-DOMAIN.md` §8. Treat a change to
|
|
46
|
+
* them as a change to a published contract.
|
|
47
|
+
*/
|
|
48
|
+
export declare const SEVERITY_WEIGHTS: Readonly<Record<Severity, number>>;
|
|
49
|
+
/**
|
|
50
|
+
* Why a finding was given the severity it has.
|
|
51
|
+
*
|
|
52
|
+
* Recorded on every finding because auditors ask, and because a severity with
|
|
53
|
+
* no provenance is not evidence. Listed here in precedence order — an earlier
|
|
54
|
+
* source wins over a later one:
|
|
55
|
+
*
|
|
56
|
+
* 1. `explicit` — stated by the scanner or the operator for this exact finding.
|
|
57
|
+
* 2. `cvss` — derived from a CVSS base score via {@link severityFromCvss}.
|
|
58
|
+
* 3. `engine_default` — the engine's own rating for the rule that fired.
|
|
59
|
+
* 4. `advisory` — taken from a published advisory for the associated CVE.
|
|
60
|
+
*
|
|
61
|
+
* Note the unfortunate collision: `advisory` is both the lowest
|
|
62
|
+
* {@link Severity} and the least-preferred severity *source*. They are
|
|
63
|
+
* unrelated — a finding can be `critical` from an `advisory` source. The names
|
|
64
|
+
* come from `00-DOMAIN.md` §3 and are kept so code and document agree.
|
|
65
|
+
*/
|
|
66
|
+
export type SeveritySource = 'explicit' | 'cvss' | 'engine_default' | 'advisory';
|
|
67
|
+
/**
|
|
68
|
+
* Every {@link SeveritySource}, most authoritative first.
|
|
69
|
+
*
|
|
70
|
+
* The order is the precedence rule: when two sources offer a severity for the
|
|
71
|
+
* same finding, the one appearing earlier here wins.
|
|
72
|
+
*/
|
|
73
|
+
export declare const SEVERITY_SOURCE_PRECEDENCE: readonly ["explicit", "cvss", "engine_default", "advisory"];
|
|
74
|
+
/**
|
|
75
|
+
* Maps a CVSS base score to a {@link Severity}.
|
|
76
|
+
*
|
|
77
|
+
* Uses the CVSS v3.1 qualitative severity rating scale unchanged, so a score
|
|
78
|
+
* rated "High" by any other tool is rated `high` here. The one adaptation is
|
|
79
|
+
* at the bottom: CVSS calls `0.0` "None", and this model has no "none", so it
|
|
80
|
+
* becomes `advisory`.
|
|
81
|
+
*
|
|
82
|
+
* | CVSS score | CVSS rating | {@link Severity} |
|
|
83
|
+
* | ----------- | ----------- | ---------------- |
|
|
84
|
+
* | 9.0 – 10.0 | Critical | `critical` |
|
|
85
|
+
* | 7.0 – 8.9 | High | `high` |
|
|
86
|
+
* | 4.0 – 6.9 | Medium | `medium` |
|
|
87
|
+
* | 0.1 – 3.9 | Low | `low` |
|
|
88
|
+
* | 0.0 | None | `advisory` |
|
|
89
|
+
*
|
|
90
|
+
* @param score - A CVSS base score between 0 and 10.
|
|
91
|
+
* @returns The corresponding severity.
|
|
92
|
+
* @throws RangeError If `score` is outside 0–10 or is not a number. A score
|
|
93
|
+
* that cannot be mapped is a data problem worth surfacing, not something to
|
|
94
|
+
* silently round into `advisory`.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```ts
|
|
98
|
+
* severityFromCvss(9.8); // 'critical'
|
|
99
|
+
* severityFromCvss(0); // 'advisory'
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export declare function severityFromCvss(score: number): Severity;
|
|
103
|
+
/**
|
|
104
|
+
* How long each severity may stay open before it breaches, and how long before
|
|
105
|
+
* breaching an issue starts warning.
|
|
106
|
+
*
|
|
107
|
+
* A policy rather than a constant because remediation windows are a customer
|
|
108
|
+
* agreement, not a property of the domain: a payments company and a hobby
|
|
109
|
+
* project do not owe the same turnaround on a `high`.
|
|
110
|
+
*
|
|
111
|
+
* @see {@link DEFAULT_SLA_POLICY} for the shipped default.
|
|
112
|
+
*/
|
|
113
|
+
export interface SlaPolicy {
|
|
114
|
+
/**
|
|
115
|
+
* Days allowed to remediate an issue of each severity, from `firstSeen`.
|
|
116
|
+
*
|
|
117
|
+
* `null` means no deadline applies — the issue is tracked and reported, but
|
|
118
|
+
* it can never be "due" or "breached". `advisory` is `null` by default.
|
|
119
|
+
*/
|
|
120
|
+
readonly durationDays: Readonly<Record<Severity, number | null>>;
|
|
121
|
+
/**
|
|
122
|
+
* How many days before the deadline an issue starts reporting `due_soon`.
|
|
123
|
+
*
|
|
124
|
+
* Exists so a report can distinguish "you have time" from "this is about to
|
|
125
|
+
* breach" without the reader doing arithmetic.
|
|
126
|
+
*/
|
|
127
|
+
readonly dueSoonDays: number;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The default remediation windows.
|
|
131
|
+
*
|
|
132
|
+
* Chosen to be defensible rather than derived: they line up with the windows
|
|
133
|
+
* most vulnerability-management programmes already run, so a customer can
|
|
134
|
+
* adopt them without an argument and override them without a migration.
|
|
135
|
+
*
|
|
136
|
+
* `advisory` has no deadline at all. Giving a zero-weight severity a due date
|
|
137
|
+
* would manufacture breaches out of things nobody agreed to fix.
|
|
138
|
+
*
|
|
139
|
+
* | Severity | Window |
|
|
140
|
+
* | ---------- | -------- |
|
|
141
|
+
* | `critical` | 7 days |
|
|
142
|
+
* | `high` | 30 days |
|
|
143
|
+
* | `medium` | 90 days |
|
|
144
|
+
* | `low` | 180 days |
|
|
145
|
+
* | `advisory` | none |
|
|
146
|
+
*
|
|
147
|
+
* **These are defaults, not the domain.** `00-DOMAIN.md` says only that
|
|
148
|
+
* `sla_due_at` is "derived from effective severity"; the numbers are this
|
|
149
|
+
* package's opinion and are meant to be replaced by an org's own policy.
|
|
150
|
+
*/
|
|
151
|
+
export declare const DEFAULT_SLA_POLICY: SlaPolicy;
|
|
152
|
+
/**
|
|
153
|
+
* Where an issue stands against its remediation deadline.
|
|
154
|
+
*
|
|
155
|
+
* - `within` — inside the window, and not close enough to warn about.
|
|
156
|
+
* - `due_soon` — inside the window but within {@link SlaPolicy.dueSoonDays}.
|
|
157
|
+
* - `breached` — past the deadline.
|
|
158
|
+
*
|
|
159
|
+
* An issue with no deadline (see {@link DEFAULT_SLA_POLICY}) is always
|
|
160
|
+
* `within`: it cannot breach something it was never given.
|
|
161
|
+
*/
|
|
162
|
+
export type SlaStatus = 'within' | 'due_soon' | 'breached';
|
|
163
|
+
/**
|
|
164
|
+
* When an issue of this severity, first seen at this moment, is due.
|
|
165
|
+
*
|
|
166
|
+
* Pure: the deadline is computed from the arguments alone, so the same inputs
|
|
167
|
+
* always give the same answer and a test needs no control over the clock.
|
|
168
|
+
*
|
|
169
|
+
* @param severity - The issue's **effective** severity, not its detected one —
|
|
170
|
+
* an override is a deliberate statement about how urgent something is, and the
|
|
171
|
+
* deadline should follow it.
|
|
172
|
+
* @param firstSeen - When the issue was first seen. Never the current run:
|
|
173
|
+
* `first_seen` is never reset, so neither is the deadline (invariant 4).
|
|
174
|
+
* @param policy - The remediation windows to apply.
|
|
175
|
+
* @returns The deadline, or `null` where the severity has no window.
|
|
176
|
+
*/
|
|
177
|
+
export declare function slaDueAt(severity: Severity, firstSeen: Date, policy?: SlaPolicy): Date | null;
|
|
178
|
+
/**
|
|
179
|
+
* Where an issue stands against a deadline, as of a given moment.
|
|
180
|
+
*
|
|
181
|
+
* Pure, and takes `now` explicitly rather than reading the clock, so a report
|
|
182
|
+
* rendered for a past run reports the status *as of that run* rather than as of
|
|
183
|
+
* today.
|
|
184
|
+
*
|
|
185
|
+
* @param dueAt - The deadline from {@link slaDueAt}, or `null` for no deadline.
|
|
186
|
+
* @param now - The moment to evaluate against.
|
|
187
|
+
* @param policy - Supplies the `due_soon` window.
|
|
188
|
+
* @returns The status. Always `within` when `dueAt` is `null`.
|
|
189
|
+
*/
|
|
190
|
+
export declare function slaStatus(dueAt: Date | null, now: Date, policy?: SlaPolicy): SlaStatus;
|
|
191
|
+
//# sourceMappingURL=severity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"severity.d.ts","sourceRoot":"","sources":["../src/severity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,MAAM,QAAQ,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,GAAG,UAAU,CAAC;AAE3E;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,4DAA6D,CAAC;AAEzF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAEvD;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAM9D,CAAC;AAEH;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,MAAM,GAAG,gBAAgB,GAAG,UAAU,CAAC;AAEjF;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,6DAK7B,CAAC;AAEX;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,CASxD;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,SAAS;IACxB;;;;;OAKG;IACH,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC;IAEjE;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,kBAAkB,EAAE,SAS/B,CAAC;AAEH;;;;;;;;;GASG;AACH,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,UAAU,GAAG,UAAU,CAAC;AAE3D;;;;;;;;;;;;;GAaG;AACH,wBAAgB,QAAQ,CACtB,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,IAAI,EACf,MAAM,GAAE,SAA8B,GACrC,IAAI,GAAG,IAAI,CAIb;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CACvB,KAAK,EAAE,IAAI,GAAG,IAAI,EAClB,GAAG,EAAE,IAAI,EACT,MAAM,GAAE,SAA8B,GACrC,SAAS,CAKX"}
|
package/dist/severity.js
ADDED
|
@@ -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,70 @@
|
|
|
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
|
+
* @param json - The contents of a snapshot file.
|
|
66
|
+
* @returns The snapshot, with dates revived.
|
|
67
|
+
* @throws SyntaxError If the text is not valid JSON.
|
|
68
|
+
*/
|
|
69
|
+
export declare function parseSnapshot(json: string): Snapshot;
|
|
70
|
+
//# 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;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAQpD"}
|
|
@@ -0,0 +1,148 @@
|
|
|
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
|
+
* @param json - The contents of a snapshot file.
|
|
135
|
+
* @returns The snapshot, with dates revived.
|
|
136
|
+
* @throws SyntaxError If the text is not valid JSON.
|
|
137
|
+
*/
|
|
138
|
+
export function parseSnapshot(json) {
|
|
139
|
+
return JSON.parse(json, (key, value) => {
|
|
140
|
+
if (DATE_FIELDS.has(key) && typeof value === 'string') {
|
|
141
|
+
const parsed = new Date(value);
|
|
142
|
+
if (!Number.isNaN(parsed.getTime()))
|
|
143
|
+
return parsed;
|
|
144
|
+
}
|
|
145
|
+
return value;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
//# 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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"}
|