@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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +73 -0
  3. package/dist/client.d.ts +135 -0
  4. package/dist/client.d.ts.map +1 -0
  5. package/dist/client.js +191 -0
  6. package/dist/client.js.map +1 -0
  7. package/dist/errors.d.ts +80 -0
  8. package/dist/errors.d.ts.map +1 -0
  9. package/dist/errors.js +118 -0
  10. package/dist/errors.js.map +1 -0
  11. package/dist/index.d.ts +31 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +10 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/pagination.d.ts +27 -0
  16. package/dist/pagination.d.ts.map +1 -0
  17. package/dist/pagination.js +19 -0
  18. package/dist/pagination.js.map +1 -0
  19. package/dist/resources/branding.d.ts +49 -0
  20. package/dist/resources/branding.d.ts.map +1 -0
  21. package/dist/resources/branding.js +38 -0
  22. package/dist/resources/branding.js.map +1 -0
  23. package/dist/resources/issues.d.ts +248 -0
  24. package/dist/resources/issues.d.ts.map +1 -0
  25. package/dist/resources/issues.js +100 -0
  26. package/dist/resources/issues.js.map +1 -0
  27. package/dist/resources/reports.d.ts +83 -0
  28. package/dist/resources/reports.d.ts.map +1 -0
  29. package/dist/resources/reports.js +50 -0
  30. package/dist/resources/reports.js.map +1 -0
  31. package/dist/resources/runs.d.ts +474 -0
  32. package/dist/resources/runs.d.ts.map +1 -0
  33. package/dist/resources/runs.js +281 -0
  34. package/dist/resources/runs.js.map +1 -0
  35. package/dist/resources/suppression-rules.d.ts +43 -0
  36. package/dist/resources/suppression-rules.d.ts.map +1 -0
  37. package/dist/resources/suppression-rules.js +33 -0
  38. package/dist/resources/suppression-rules.js.map +1 -0
  39. package/dist/resources/targets.d.ts +126 -0
  40. package/dist/resources/targets.d.ts.map +1 -0
  41. package/dist/resources/targets.js +83 -0
  42. package/dist/resources/targets.js.map +1 -0
  43. package/package.json +55 -0
  44. package/src/client.ts +328 -0
  45. package/src/errors.ts +120 -0
  46. package/src/index.ts +100 -0
  47. package/src/pagination.ts +41 -0
  48. package/src/resources/branding.ts +59 -0
  49. package/src/resources/issues.ts +344 -0
  50. package/src/resources/reports.ts +104 -0
  51. package/src/resources/runs.ts +635 -0
  52. package/src/resources/suppression-rules.ts +69 -0
  53. 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
+ }