@secureport/sdk 0.5.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/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/client.d.ts +135 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +191 -0
- package/dist/client.js.map +1 -0
- package/dist/errors.d.ts +80 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +118 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/pagination.d.ts +27 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +19 -0
- package/dist/pagination.js.map +1 -0
- package/dist/resources/branding.d.ts +49 -0
- package/dist/resources/branding.d.ts.map +1 -0
- package/dist/resources/branding.js +38 -0
- package/dist/resources/branding.js.map +1 -0
- package/dist/resources/issues.d.ts +248 -0
- package/dist/resources/issues.d.ts.map +1 -0
- package/dist/resources/issues.js +100 -0
- package/dist/resources/issues.js.map +1 -0
- package/dist/resources/reports.d.ts +83 -0
- package/dist/resources/reports.d.ts.map +1 -0
- package/dist/resources/reports.js +50 -0
- package/dist/resources/reports.js.map +1 -0
- package/dist/resources/runs.d.ts +474 -0
- package/dist/resources/runs.d.ts.map +1 -0
- package/dist/resources/runs.js +281 -0
- package/dist/resources/runs.js.map +1 -0
- package/dist/resources/suppression-rules.d.ts +43 -0
- package/dist/resources/suppression-rules.d.ts.map +1 -0
- package/dist/resources/suppression-rules.js +33 -0
- package/dist/resources/suppression-rules.js.map +1 -0
- package/dist/resources/targets.d.ts +126 -0
- package/dist/resources/targets.d.ts.map +1 -0
- package/dist/resources/targets.js +83 -0
- package/dist/resources/targets.js.map +1 -0
- package/package.json +55 -0
- package/src/client.ts +328 -0
- package/src/errors.ts +120 -0
- package/src/index.ts +100 -0
- package/src/pagination.ts +41 -0
- package/src/resources/branding.ts +59 -0
- package/src/resources/issues.ts +344 -0
- package/src/resources/reports.ts +104 -0
- package/src/resources/runs.ts +635 -0
- package/src/resources/suppression-rules.ts +69 -0
- package/src/resources/targets.ts +179 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
// Issue, finding and comment types, and the /issues resource.
|
|
2
|
+
import type { Requester } from '../client.js';
|
|
3
|
+
import type { Page, PageQuery } from '../pagination.js';
|
|
4
|
+
import { paginate } from '../pagination.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Severity, most to least urgent: `critical`, `high`, `medium`, `low`,
|
|
8
|
+
* `advisory` — SDK-local, not imported from `@secureport/core`.
|
|
9
|
+
*/
|
|
10
|
+
export type Severity = 'critical' | 'high' | 'medium' | 'low' | 'advisory';
|
|
11
|
+
|
|
12
|
+
/** Where an issue is in its lifecycle. There is no `triaging` state — see `@secureport/core`. */
|
|
13
|
+
export type IssueStatus = 'open' | 'resolved' | 'regressed' | 'ignored';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* How an issue came into existence. Matters because **manual-origin issues
|
|
17
|
+
* never auto-resolve** — a human closes what a human opened.
|
|
18
|
+
*/
|
|
19
|
+
export type IssueOrigin = 'scan' | 'upload' | 'manual';
|
|
20
|
+
|
|
21
|
+
/** Why an issue was suppressed — the fixed set the suppressed appendix groups by. */
|
|
22
|
+
export type IgnoreReason = 'false_positive' | 'accepted_risk' | 'out_of_scope' | 'duplicate';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* How widely an ignore applies — `target` (this issue only, the default) or
|
|
26
|
+
* `org` (same weakness anywhere, matched on `vulnKey` and location).
|
|
27
|
+
*/
|
|
28
|
+
export type IgnoreScope = 'target' | 'org';
|
|
29
|
+
|
|
30
|
+
/** What happened to an issue — the audit trail every issue carries. */
|
|
31
|
+
export type IssueEventType =
|
|
32
|
+
| 'created'
|
|
33
|
+
| 'seen'
|
|
34
|
+
| 'severity_detected_changed'
|
|
35
|
+
| 'severity_overridden'
|
|
36
|
+
| 'resolved'
|
|
37
|
+
| 'reopened'
|
|
38
|
+
| 'ignored'
|
|
39
|
+
| 'unignored'
|
|
40
|
+
| 'commented'
|
|
41
|
+
| 'assigned'
|
|
42
|
+
| 'merged'
|
|
43
|
+
| 'retitled'
|
|
44
|
+
| 'exported';
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The tracked record that findings reconcile into. Mirrors
|
|
48
|
+
* `@secureport/core`'s `Issue`, with ISO 8601 strings in place of `Date` — parse what you need yourself.
|
|
49
|
+
*/
|
|
50
|
+
export interface Issue {
|
|
51
|
+
readonly id: string;
|
|
52
|
+
readonly orgId: string;
|
|
53
|
+
readonly targetId: string;
|
|
54
|
+
readonly fingerprint: string;
|
|
55
|
+
readonly fingerprintVersion: string;
|
|
56
|
+
readonly title: string;
|
|
57
|
+
readonly vulnKey: string;
|
|
58
|
+
readonly cwe?: string;
|
|
59
|
+
readonly location: string;
|
|
60
|
+
readonly parameter?: string;
|
|
61
|
+
readonly status: IssueStatus;
|
|
62
|
+
readonly detectedSeverity: Severity;
|
|
63
|
+
readonly effectiveSeverity: Severity;
|
|
64
|
+
readonly severityOverrideReason?: string;
|
|
65
|
+
readonly severityOverriddenBy?: string;
|
|
66
|
+
readonly severityOverriddenAt?: string;
|
|
67
|
+
|
|
68
|
+
/** Never reset — not by a regression, a severity change, or anything else. */
|
|
69
|
+
readonly firstSeen: string;
|
|
70
|
+
readonly lastSeen: string;
|
|
71
|
+
readonly resolvedAt?: string;
|
|
72
|
+
readonly reopenedAt?: string;
|
|
73
|
+
readonly consecutiveMisses: number;
|
|
74
|
+
readonly origin: IssueOrigin;
|
|
75
|
+
readonly assignee?: string;
|
|
76
|
+
readonly externalRef?: string;
|
|
77
|
+
|
|
78
|
+
/** Present only while `status` is `ignored`. */
|
|
79
|
+
readonly ignoreReason?: IgnoreReason;
|
|
80
|
+
readonly ignoreComment?: string;
|
|
81
|
+
readonly ignoreScope?: IgnoreScope;
|
|
82
|
+
readonly ignoreExpiresAt?: string;
|
|
83
|
+
readonly ignoredBy?: string;
|
|
84
|
+
|
|
85
|
+
/** Detected severity at the moment it was ignored — an increase re-surfaces the issue. */
|
|
86
|
+
readonly severityAtIgnore?: Severity;
|
|
87
|
+
|
|
88
|
+
/** `null` where the effective severity carries no SLA deadline. */
|
|
89
|
+
readonly slaDueAt: string | null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** One entry in an issue's append-only history. */
|
|
93
|
+
export interface IssueEvent {
|
|
94
|
+
readonly id: string;
|
|
95
|
+
readonly orgId: string;
|
|
96
|
+
readonly issueId: string;
|
|
97
|
+
|
|
98
|
+
/** The run that caused it, where a run did. Absent for human actions. */
|
|
99
|
+
readonly runId?: string;
|
|
100
|
+
readonly type: IssueEventType;
|
|
101
|
+
|
|
102
|
+
/** A user id, an API key id, or the reconciler. Never absent. */
|
|
103
|
+
readonly actor: string;
|
|
104
|
+
readonly payload?: Readonly<Record<string, unknown>>;
|
|
105
|
+
readonly createdAt: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* One detection, in one run — immutable evidence. Mirrors `@secureport/core`'s
|
|
110
|
+
* `Finding`; see {@link Issue} for why this is SDK-local rather than imported.
|
|
111
|
+
*/
|
|
112
|
+
export interface Finding {
|
|
113
|
+
readonly id: string;
|
|
114
|
+
readonly orgId: string;
|
|
115
|
+
readonly runId: string;
|
|
116
|
+
readonly fingerprint: string;
|
|
117
|
+
readonly fingerprintVersion: string;
|
|
118
|
+
readonly title: string;
|
|
119
|
+
readonly description?: string;
|
|
120
|
+
readonly detectedSeverity: Severity;
|
|
121
|
+
readonly severitySource: 'explicit' | 'cvss' | 'engine_default' | 'published_advisory';
|
|
122
|
+
readonly cvssScore?: number;
|
|
123
|
+
readonly cvssVector?: string;
|
|
124
|
+
readonly cwe?: string;
|
|
125
|
+
readonly cve?: string;
|
|
126
|
+
readonly vulnKey: string;
|
|
127
|
+
readonly category?: string;
|
|
128
|
+
readonly location: string;
|
|
129
|
+
readonly parameter?: string;
|
|
130
|
+
readonly port?: number;
|
|
131
|
+
readonly evidenceUri?: readonly string[];
|
|
132
|
+
readonly recommendation?: string;
|
|
133
|
+
readonly references?: readonly string[];
|
|
134
|
+
readonly sourceEngine: string;
|
|
135
|
+
readonly sourceRuleId?: string;
|
|
136
|
+
readonly confidence?: number;
|
|
137
|
+
readonly createdAt: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* A comment on an issue — API-local, with no `@secureport/core` mirror.
|
|
142
|
+
* Permanent: never edited, never deleted.
|
|
143
|
+
*/
|
|
144
|
+
export interface IssueComment {
|
|
145
|
+
readonly id: string;
|
|
146
|
+
readonly orgId: string;
|
|
147
|
+
readonly issueId: string;
|
|
148
|
+
readonly author: string;
|
|
149
|
+
readonly body: string;
|
|
150
|
+
readonly createdAt: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* One issue with everything hanging off it — what {@link IssuesResource.get}
|
|
155
|
+
* returns, not a bare {@link Issue}.
|
|
156
|
+
*/
|
|
157
|
+
export interface IssueDetail {
|
|
158
|
+
readonly issue: Issue;
|
|
159
|
+
readonly events: readonly IssueEvent[];
|
|
160
|
+
readonly findings: readonly Finding[];
|
|
161
|
+
readonly comments: readonly IssueComment[];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export type IssuePage = Page<Issue>;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The filters `list`/`listAll` take, beside the page. All optional and
|
|
168
|
+
* combinable.
|
|
169
|
+
*/
|
|
170
|
+
export interface IssueFilters {
|
|
171
|
+
readonly status?: IssueStatus;
|
|
172
|
+
|
|
173
|
+
/** The effective severity — the one a human may have overridden. */
|
|
174
|
+
readonly severity?: Severity;
|
|
175
|
+
readonly targetId?: string;
|
|
176
|
+
|
|
177
|
+
/** ISO 8601. Issues seen on or after this instant (`lastSeen`). */
|
|
178
|
+
readonly since?: string;
|
|
179
|
+
|
|
180
|
+
/** e.g. `'CWE-79'`. */
|
|
181
|
+
readonly cwe?: string;
|
|
182
|
+
|
|
183
|
+
/** Full-text over title, vulnerability key and location. */
|
|
184
|
+
readonly search?: string;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface NewIssue {
|
|
188
|
+
readonly targetId: string;
|
|
189
|
+
readonly title: string;
|
|
190
|
+
readonly vulnKey: string;
|
|
191
|
+
readonly location: string;
|
|
192
|
+
readonly parameter?: string;
|
|
193
|
+
readonly cwe?: string;
|
|
194
|
+
readonly severity: Severity;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Retitles, assigns, or overrides severity — never status, which has its
|
|
199
|
+
* own endpoints (`resolve`/`ignore`/`unignore`), each with its own event.
|
|
200
|
+
*/
|
|
201
|
+
export interface IssuePatch {
|
|
202
|
+
/** A corrected title. */
|
|
203
|
+
readonly title?: string;
|
|
204
|
+
|
|
205
|
+
/** `null` unassigns. */
|
|
206
|
+
readonly assignee?: string | null;
|
|
207
|
+
|
|
208
|
+
/** A human's severity, with the reason it differs from the detected one. */
|
|
209
|
+
readonly severityOverride?: {
|
|
210
|
+
readonly severity: Severity;
|
|
211
|
+
readonly reason: string;
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface NewIssueComment {
|
|
216
|
+
readonly body: string;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export interface ResolveInput {
|
|
220
|
+
/** Optional. What was changed, for whoever reads the history later. */
|
|
221
|
+
readonly comment?: string;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export interface IgnoreInput {
|
|
225
|
+
readonly reason: IgnoreReason;
|
|
226
|
+
|
|
227
|
+
/** Required. The register an auditor reads is only as good as this sentence. */
|
|
228
|
+
readonly comment: string;
|
|
229
|
+
|
|
230
|
+
/** `target` (default) or `org`. */
|
|
231
|
+
readonly scope?: IgnoreScope;
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* When the suppression lapses — omit for the server default (90 days for
|
|
235
|
+
* `accepted_risk`, none otherwise); pass `null` to force "never expires".
|
|
236
|
+
*/
|
|
237
|
+
readonly expiresAt?: string | null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Issues — the tracked entity findings reconcile into. Two scanners
|
|
242
|
+
* reporting one weakness produce one issue with two findings, not two.
|
|
243
|
+
*/
|
|
244
|
+
export class IssuesResource {
|
|
245
|
+
constructor(private readonly request: Requester) {}
|
|
246
|
+
|
|
247
|
+
list(query?: PageQuery & IssueFilters): Promise<IssuePage> {
|
|
248
|
+
return this.request({ method: 'GET', path: '/issues', query: { ...query } });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Every issue matching `filters`, across every page, as an async iterator. */
|
|
252
|
+
listAll(filters?: IssueFilters): AsyncIterable<Issue> {
|
|
253
|
+
return paginate((page) => this.list({ ...filters, page }));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The issue, its event history, every finding that has evidenced it across
|
|
258
|
+
* runs, and its comments — the view an auditor reads.
|
|
259
|
+
*/
|
|
260
|
+
get(id: string): Promise<IssueDetail> {
|
|
261
|
+
return this.request({ method: 'GET', path: `/issues/${encodeURIComponent(id)}` });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Opens an issue by hand, fingerprinted like a finding so a later scan meets it.
|
|
266
|
+
*
|
|
267
|
+
* @throws {SecureportError} `conflict` (duplicate fingerprint) or `not_found` (`targetId`).
|
|
268
|
+
*/
|
|
269
|
+
create(input: NewIssue): Promise<Issue> {
|
|
270
|
+
return this.request({ method: 'POST', path: '/issues', body: input });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
update(id: string, patch: IssuePatch): Promise<Issue> {
|
|
274
|
+
return this.request({
|
|
275
|
+
method: 'PATCH',
|
|
276
|
+
path: `/issues/${encodeURIComponent(id)}`,
|
|
277
|
+
body: patch,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Adds a comment. **Returns the created comment, not the issue** — the one
|
|
283
|
+
* response-shape outlier among this resource's routes.
|
|
284
|
+
*/
|
|
285
|
+
comment(id: string, input: NewIssueComment): Promise<IssueComment> {
|
|
286
|
+
return this.request({
|
|
287
|
+
method: 'POST',
|
|
288
|
+
path: `/issues/${encodeURIComponent(id)}/comments`,
|
|
289
|
+
body: input,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Marks an issue fixed — a claim, not a fact: a later run finding the
|
|
295
|
+
* weakness again regresses it, same as an automatic resolution.
|
|
296
|
+
*
|
|
297
|
+
* @throws {SecureportError} `conflict` if already resolved or ignored — one code, three messages; read `message` to tell them apart.
|
|
298
|
+
*/
|
|
299
|
+
resolve(id: string, input?: ResolveInput): Promise<Issue> {
|
|
300
|
+
return this.request({
|
|
301
|
+
method: 'POST',
|
|
302
|
+
path: `/issues/${encodeURIComponent(id)}/resolve`,
|
|
303
|
+
body: input ?? {},
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Suppresses an issue — keeps reconciling, drops out of every count but
|
|
309
|
+
* its own, resurfaces if detected severity rises or {@link IgnoreInput.expiresAt} passes.
|
|
310
|
+
*
|
|
311
|
+
* @throws {SecureportError} `conflict` if the issue is already ignored.
|
|
312
|
+
*/
|
|
313
|
+
ignore(id: string, input: IgnoreInput): Promise<Issue> {
|
|
314
|
+
return this.request({
|
|
315
|
+
method: 'POST',
|
|
316
|
+
path: `/issues/${encodeURIComponent(id)}/ignore`,
|
|
317
|
+
body: input,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Lifts a suppression by hand — returns to `open`, the same state
|
|
323
|
+
* reconciliation restores on expiry or a severity rise.
|
|
324
|
+
*
|
|
325
|
+
* @throws {SecureportError} `conflict` if the issue is not ignored.
|
|
326
|
+
*/
|
|
327
|
+
unignore(id: string): Promise<Issue> {
|
|
328
|
+
return this.request({ method: 'POST', path: `/issues/${encodeURIComponent(id)}/unignore` });
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Merges `duplicateId` into `survivorId` (never the reverse) — nothing is
|
|
333
|
+
* rewritten; `duplicateId`'s evidence attaches to `survivorId`, which inherits the earlier `firstSeen`, and `duplicateId` becomes `ignored: duplicate`.
|
|
334
|
+
*
|
|
335
|
+
* @returns The surviving issue (`survivorId`).
|
|
336
|
+
* @throws {SecureportError} `conflict` if the two issues cannot be merged.
|
|
337
|
+
*/
|
|
338
|
+
merge(survivorId: string, duplicateId: string): Promise<Issue> {
|
|
339
|
+
return this.request({
|
|
340
|
+
method: 'POST',
|
|
341
|
+
path: `/issues/${encodeURIComponent(survivorId)}/merge/${encodeURIComponent(duplicateId)}`,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { RawRequester } from '../client.js';
|
|
2
|
+
import type { Severity } from './issues.js';
|
|
3
|
+
|
|
4
|
+
/** Which of the five report templates to render. */
|
|
5
|
+
export type ReportKind = 'pen' | 'vap' | 'exec' | 'attest' | 'retest';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The formats an API key may request.
|
|
9
|
+
*
|
|
10
|
+
* `pdf` and `html` exist on the wire but are refused with `forbidden` for
|
|
11
|
+
* any machine credential — those are documents requested from the dashboard
|
|
12
|
+
* by a signed-in user, matching the API-key boundary `@secureport/cli`'s
|
|
13
|
+
* hosted mode already settled. Restricted to this set at the type level so a
|
|
14
|
+
* caller cannot even construct the request; {@link ReportsResource.request}
|
|
15
|
+
* backs it with a runtime check for a JS consumer with no type checker.
|
|
16
|
+
*/
|
|
17
|
+
export type ReportFormat = 'json' | 'markdown' | 'csv' | 'sarif';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* How much evidence a document prints. **Has no effect on `json`, `csv` or
|
|
21
|
+
* `sarif`** — those formats carry what they carry, and a consumer filters;
|
|
22
|
+
* only `markdown` reads this.
|
|
23
|
+
*/
|
|
24
|
+
export type EvidenceVerbosity = 'none' | 'summary' | 'full';
|
|
25
|
+
|
|
26
|
+
export interface ReportRequest {
|
|
27
|
+
readonly kind: ReportKind;
|
|
28
|
+
readonly format: ReportFormat;
|
|
29
|
+
readonly title?: string;
|
|
30
|
+
readonly preparedFor?: string;
|
|
31
|
+
readonly coverPage?: boolean;
|
|
32
|
+
readonly tableOfContents?: boolean;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Rows below this severity are dropped from the document. **The counts
|
|
36
|
+
* never move** — `outstanding`, the exposure score and the breach count
|
|
37
|
+
* describe the target, not the document, so the floor only removes rows;
|
|
38
|
+
* the document states how many it removed.
|
|
39
|
+
*/
|
|
40
|
+
readonly severityFloor?: Severity;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Whether resolved issues are still listed. Omitting evidence of
|
|
44
|
+
* remediation only understates the good news, never the risk, so unlike
|
|
45
|
+
* {@link ReportRequest.severityFloor} this needs no disclosure in the document.
|
|
46
|
+
*
|
|
47
|
+
* @defaultValue true
|
|
48
|
+
*/
|
|
49
|
+
readonly includeResolved?: boolean;
|
|
50
|
+
|
|
51
|
+
/** @defaultValue 'summary' */
|
|
52
|
+
readonly evidenceVerbosity?: EvidenceVerbosity;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const MACHINE_FORMATS = new Set<ReportFormat>(['json', 'markdown', 'csv', 'sarif']);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Reports — the one resource whose response is never JSON. Every format
|
|
59
|
+
* `request` accepts is a raw, streamed body (`text/csv`,
|
|
60
|
+
* `application/sarif+json`, plain `application/json` text, …), so this
|
|
61
|
+
* resource is built on {@link SecureportClient.requestRaw} rather than the
|
|
62
|
+
* usual `request<T>()`, and hands the `Response` itself back to the caller
|
|
63
|
+
* to read as text, parse, or stream to a file.
|
|
64
|
+
*/
|
|
65
|
+
export class ReportsResource {
|
|
66
|
+
constructor(private readonly requestRaw: RawRequester) {}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Renders a run into a report and streams it back. **Nothing is stored on
|
|
70
|
+
* the API side** — the bytes are produced from the run's snapshot on
|
|
71
|
+
* demand and exist only in this response; call this again for the same
|
|
72
|
+
* run and it renders again from whatever the run's snapshot is by then.
|
|
73
|
+
*
|
|
74
|
+
* **A failure partway through the stream cannot become an HTTP status.**
|
|
75
|
+
* The status and headers this method's `Response` carries are whatever was
|
|
76
|
+
* sent before the first byte of the body; nothing that goes wrong later in
|
|
77
|
+
* the stream can change them, so a truncated document looks identical to a
|
|
78
|
+
* complete one at this layer. Verify the body's own shape — that a JSON or
|
|
79
|
+
* SARIF document parses, that a CSV has the trailer row it expects — if a
|
|
80
|
+
* caller needs to be sure nothing was cut short.
|
|
81
|
+
*
|
|
82
|
+
* @throws {Error} synchronously, before any request is made, if
|
|
83
|
+
* `input.format` is `'pdf'` or `'html'` — this method exists only for
|
|
84
|
+
* `'json' | 'csv' | 'sarif' | 'markdown'`; a caller bypassing the type
|
|
85
|
+
* (plain JS, no type checker) is told so without spending a round trip.
|
|
86
|
+
* @throws {SecureportError} `conflict` if the run has not been reconciled
|
|
87
|
+
* — still running, or ended `failed`; `forbidden` should be unreachable
|
|
88
|
+
* given the format check above, but is still what the API itself would
|
|
89
|
+
* answer for `pdf`/`html`.
|
|
90
|
+
*/
|
|
91
|
+
request(runId: string, input: ReportRequest): Promise<Response> {
|
|
92
|
+
if (!MACHINE_FORMATS.has(input.format)) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`'${input.format}' reports are requested from the dashboard by a signed-in user — an ` +
|
|
95
|
+
'API key can export json, csv, sarif or markdown.',
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return this.requestRaw({
|
|
99
|
+
method: 'POST',
|
|
100
|
+
path: `/runs/${encodeURIComponent(runId)}/reports`,
|
|
101
|
+
body: input,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|