@secureport/core 2.1.0 → 2.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/dist/index.d.ts +9 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/issue.d.ts +1 -1
- package/dist/issue.d.ts.map +1 -1
- package/dist/reconcile.d.ts +46 -1
- package/dist/reconcile.d.ts.map +1 -1
- package/dist/reconcile.js +34 -1
- package/dist/reconcile.js.map +1 -1
- package/dist/report/anchors.d.ts +39 -0
- package/dist/report/anchors.d.ts.map +1 -0
- package/dist/report/anchors.js +73 -0
- package/dist/report/anchors.js.map +1 -0
- package/dist/report/csv.d.ts +41 -0
- package/dist/report/csv.d.ts.map +1 -0
- package/dist/report/csv.js +152 -0
- package/dist/report/csv.js.map +1 -0
- package/dist/report/html.d.ts.map +1 -1
- package/dist/report/html.js +126 -22
- package/dist/report/html.js.map +1 -1
- package/dist/report/json.d.ts +15 -0
- package/dist/report/json.d.ts.map +1 -1
- package/dist/report/json.js +1 -0
- package/dist/report/json.js.map +1 -1
- package/dist/report/markdown.d.ts +24 -2
- package/dist/report/markdown.d.ts.map +1 -1
- package/dist/report/markdown.js +133 -31
- package/dist/report/markdown.js.map +1 -1
- package/dist/report/model.d.ts +288 -2
- package/dist/report/model.d.ts.map +1 -1
- package/dist/report/model.js +153 -21
- package/dist/report/model.js.map +1 -1
- package/dist/report/sarif.d.ts +60 -0
- package/dist/report/sarif.d.ts.map +1 -0
- package/dist/report/sarif.js +125 -0
- package/dist/report/sarif.js.map +1 -0
- package/dist/report/stream.d.ts +108 -0
- package/dist/report/stream.d.ts.map +1 -0
- package/dist/report/stream.js +534 -0
- package/dist/report/stream.js.map +1 -0
- package/package.json +4 -2
- package/src/index.ts +11 -2
- package/src/issue.ts +1 -0
- package/src/reconcile.ts +86 -2
- package/src/report/anchors.ts +96 -0
- package/src/report/csv.ts +180 -0
- package/src/report/html.ts +146 -26
- package/src/report/json.ts +17 -0
- package/src/report/markdown.ts +146 -30
- package/src/report/model.ts +433 -21
- package/src/report/sarif.ts +166 -0
- package/src/report/stream.ts +702 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { Severity } from '../severity.js';
|
|
2
|
+
import type { SnapshotIssue } from '../snapshot.js';
|
|
3
|
+
import type { ReportHeader } from './stream.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What a SARIF export can be asked for.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately its own type rather than {@link ReportOptions}: SARIF has no
|
|
9
|
+
* `kind` — it is always "the issues open right now", never a retest or an
|
|
10
|
+
* attestation — and no branding, cover page or evidence verbosity. Accepting
|
|
11
|
+
* `ReportOptions` and silently ignoring most of its fields would be a worse
|
|
12
|
+
* API than a type that only offers what SARIF actually uses.
|
|
13
|
+
*/
|
|
14
|
+
export interface SarifOptions {
|
|
15
|
+
/**
|
|
16
|
+
* Keep issues below this severity out of `results`.
|
|
17
|
+
*
|
|
18
|
+
* Unlike the other formats, there is no `omitted` count to report: SARIF
|
|
19
|
+
* has no prose to carry a disclosure sentence in, and a consumer asking
|
|
20
|
+
* for `critical` only is asking to scope a code-scanning feed, not
|
|
21
|
+
* reading a document that owes it an explanation of what it left out.
|
|
22
|
+
*/
|
|
23
|
+
readonly severityFloor?: Severity;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** SARIF's four severity levels, from the SARIF 2.1.0 spec §3.27.10. */
|
|
27
|
+
type SarifLevel = 'error' | 'warning' | 'note' | 'none';
|
|
28
|
+
|
|
29
|
+
const SARIF_LEVEL: Readonly<Record<Severity, SarifLevel>> = Object.freeze({
|
|
30
|
+
critical: 'error',
|
|
31
|
+
high: 'error',
|
|
32
|
+
medium: 'warning',
|
|
33
|
+
low: 'note',
|
|
34
|
+
advisory: 'note',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The `security-severity` GitHub code scanning reads to rank an alert, as a
|
|
39
|
+
* string 0.0–10.0 — GitHub's convention, not part of the SARIF spec itself.
|
|
40
|
+
* Fixed bands rather than a CVSS score, because not every finding has one
|
|
41
|
+
* and two issues at the same {@link Severity} should rank the same.
|
|
42
|
+
*/
|
|
43
|
+
const SECURITY_SEVERITY: Readonly<Record<Severity, string>> = Object.freeze({
|
|
44
|
+
critical: '9.0',
|
|
45
|
+
high: '7.5',
|
|
46
|
+
medium: '5.0',
|
|
47
|
+
low: '3.0',
|
|
48
|
+
advisory: '1.0',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/** `SEVERITY_ORDER`, duplicated as an index rather than imported, to keep this file's only dependency on `severity.ts` the type. */
|
|
52
|
+
const FLOOR_RANK: Readonly<Record<Severity, number>> = Object.freeze({
|
|
53
|
+
critical: 0,
|
|
54
|
+
high: 1,
|
|
55
|
+
medium: 2,
|
|
56
|
+
low: 3,
|
|
57
|
+
advisory: 4,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/** `"key":value`, run through `JSON.stringify` so nothing is hand-escaped. */
|
|
61
|
+
function field(key: string, value: unknown): string {
|
|
62
|
+
return `${JSON.stringify(key)}:${JSON.stringify(value)}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Renders a snapshot's open issues as SARIF 2.1.0, from a header and a
|
|
67
|
+
* cursor — the machine path's format for GitHub code scanning (P8), and the
|
|
68
|
+
* streaming sibling of {@link renderJsonStream} and
|
|
69
|
+
* {@link renderMarkdownStream}. See {@link renderJsonStream} for the memory
|
|
70
|
+
* argument.
|
|
71
|
+
*
|
|
72
|
+
* **Resolved issues never appear, unconditionally — there is no
|
|
73
|
+
* `includeResolved` option.** SARIF is not a document a person reads; it is
|
|
74
|
+
* the input to GitHub's own diffing, which closes an alert when a new
|
|
75
|
+
* upload no longer contains it. Including a resolved issue would tell
|
|
76
|
+
* GitHub the finding is still open, which is the opposite of what resolving
|
|
77
|
+
* it meant.
|
|
78
|
+
*
|
|
79
|
+
* **Suppressed issues never appear either, and take no `suppressed`
|
|
80
|
+
* parameter at all.** An accepted risk is not a code-scanning alert; SARIF
|
|
81
|
+
* has no accepted-risk register to carry it in the way the appendix does
|
|
82
|
+
* for the human and other machine formats.
|
|
83
|
+
*
|
|
84
|
+
* **`ruleId` is `Issue.vulnKey`, not `Finding.sourceRuleId` or
|
|
85
|
+
* `sourceEngine`.** `vulnKey` is engine-independent by construction
|
|
86
|
+
* (`fingerprint.ts`), and naming a scanner in a customer-facing artefact is
|
|
87
|
+
* the thing this package's report options have twice had to stop doing
|
|
88
|
+
* (`ReportOptions.evidenceVerbosity`'s own TSDoc records it). `rules[]` is
|
|
89
|
+
* built from the same stream, deduplicated on `vulnKey` — bounded by the
|
|
90
|
+
* number of distinct weakness classes, not by issue count, so buffering it
|
|
91
|
+
* (unlike `results`) costs nothing worth avoiding.
|
|
92
|
+
*
|
|
93
|
+
* @param header - The run and the target. Never scales with issue count.
|
|
94
|
+
* @param issues - Every open issue, in any order — SARIF results are an
|
|
95
|
+
* unordered set, so this format is the one place in the streaming
|
|
96
|
+
* contract with no ordering requirement on its input.
|
|
97
|
+
* @param options - A severity floor, and nothing else.
|
|
98
|
+
* @returns Chunks of JSON text; concatenated, they are one SARIF document.
|
|
99
|
+
*/
|
|
100
|
+
export async function* renderSarifStream(
|
|
101
|
+
header: ReportHeader,
|
|
102
|
+
issues: AsyncIterable<SnapshotIssue>,
|
|
103
|
+
options: SarifOptions = {},
|
|
104
|
+
): AsyncGenerator<string> {
|
|
105
|
+
const floor = options.severityFloor;
|
|
106
|
+
const floorAt = floor === undefined ? 4 : FLOOR_RANK[floor];
|
|
107
|
+
// Small — bounded by distinct weakness classes, not by issue count — and
|
|
108
|
+
// known only once the stream is exhausted. So `results` (the one array
|
|
109
|
+
// that must never be buffered) is written first and `tool.driver.rules`
|
|
110
|
+
// last, the one key order that lets both be true without a second pass
|
|
111
|
+
// over `issues`: JSON does not care which key comes first.
|
|
112
|
+
const rules = new Map<string, { readonly title: string; readonly cwe: string | undefined }>();
|
|
113
|
+
|
|
114
|
+
yield '{';
|
|
115
|
+
// The schema's own `id`, not a GitHub raw link — checked by hand, because
|
|
116
|
+
// an unreachable $schema is a broken document a validator would still call
|
|
117
|
+
// valid, and getting it wrong once cost the trip to the OASIS repo to
|
|
118
|
+
// learn its real path (`tests/fixtures/README.md` records that trip).
|
|
119
|
+
yield field(
|
|
120
|
+
'$schema',
|
|
121
|
+
'https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json',
|
|
122
|
+
);
|
|
123
|
+
yield ',' + field('version', '2.1.0');
|
|
124
|
+
yield ',"runs":[{"results":[';
|
|
125
|
+
|
|
126
|
+
let firstResult = true;
|
|
127
|
+
for await (const entry of issues) {
|
|
128
|
+
const { issue } = entry;
|
|
129
|
+
if (issue.status === 'resolved') continue;
|
|
130
|
+
if (FLOOR_RANK[issue.effectiveSeverity] > floorAt) continue;
|
|
131
|
+
|
|
132
|
+
if (!rules.has(issue.vulnKey)) {
|
|
133
|
+
rules.set(issue.vulnKey, { title: issue.title, cwe: issue.cwe });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const message = entry.findings[0]?.description ?? issue.title;
|
|
137
|
+
const result = {
|
|
138
|
+
ruleId: issue.vulnKey,
|
|
139
|
+
level: SARIF_LEVEL[issue.effectiveSeverity],
|
|
140
|
+
message: { text: message },
|
|
141
|
+
locations: [
|
|
142
|
+
{
|
|
143
|
+
physicalLocation: {
|
|
144
|
+
artifactLocation: { uri: issue.location },
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
partialFingerprints: { secureportFingerprint: issue.fingerprint },
|
|
149
|
+
properties: { 'security-severity': SECURITY_SEVERITY[issue.effectiveSeverity] },
|
|
150
|
+
};
|
|
151
|
+
yield (firstResult ? '' : ',') + JSON.stringify(result);
|
|
152
|
+
firstResult = false;
|
|
153
|
+
}
|
|
154
|
+
yield ']';
|
|
155
|
+
|
|
156
|
+
const ruleDefs = [...rules.entries()].map(([vulnKey, rule]) => ({
|
|
157
|
+
id: vulnKey,
|
|
158
|
+
shortDescription: { text: rule.title },
|
|
159
|
+
...(rule.cwe === undefined ? {} : { properties: { tags: [rule.cwe] } }),
|
|
160
|
+
}));
|
|
161
|
+
yield ',"tool":{"driver":{';
|
|
162
|
+
yield field('name', 'Secureport');
|
|
163
|
+
yield ',' + field('informationUri', 'https://secureport.io/');
|
|
164
|
+
yield ',"rules":' + JSON.stringify(ruleDefs);
|
|
165
|
+
yield '}}}]}';
|
|
166
|
+
}
|