@pipeline-builder/pipeline-data 4.3.3 → 4.3.5

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 (47) hide show
  1. package/lib/api/crud-service.d.ts +7 -0
  2. package/lib/api/crud-service.js +14 -3
  3. package/lib/api/pipeline-step-resolution.d.ts +111 -0
  4. package/lib/api/pipeline-step-resolution.js +163 -0
  5. package/lib/api/plugin-consumption-policy.d.ts +8 -2
  6. package/lib/api/plugin-consumption-policy.js +20 -3
  7. package/lib/api/plugin-resolution.d.ts +26 -9
  8. package/lib/api/plugin-resolution.js +51 -33
  9. package/lib/api/reporting/analytics-queries.d.ts +6 -4
  10. package/lib/api/reporting/analytics-queries.js +11 -11
  11. package/lib/api/reporting/deploy-rollup.d.ts +2 -0
  12. package/lib/api/reporting/deploy-rollup.js +5 -1
  13. package/lib/api/reporting/failure-classifier.d.ts +2 -6
  14. package/lib/api/reporting/failure-classifier.js +22 -1
  15. package/lib/api/reporting/ingest.js +73 -16
  16. package/lib/api/reporting/plugin-exposure.d.ts +261 -15
  17. package/lib/api/reporting/plugin-exposure.js +423 -38
  18. package/lib/api/reporting/stakeholder/compose.d.ts +1 -1
  19. package/lib/api/reporting/stakeholder/compose.js +30 -1
  20. package/lib/api/reporting/stakeholder/store.d.ts +16 -2
  21. package/lib/api/reporting/stakeholder/store.js +17 -6
  22. package/lib/api/reporting/types.d.ts +80 -1
  23. package/lib/api/reporting/types.js +1 -1
  24. package/lib/api/reporting-service.d.ts +11 -4
  25. package/lib/api/reporting-service.js +106 -43
  26. package/lib/api/soft-delete-sweep.d.ts +8 -18
  27. package/lib/api/soft-delete-sweep.js +35 -5
  28. package/lib/api/version-references.d.ts +80 -0
  29. package/lib/api/version-references.js +135 -0
  30. package/lib/core/query-filters.d.ts +3 -3
  31. package/lib/core/query-filters.js +1 -1
  32. package/lib/database/drizzle-schema.d.ts +1238 -143
  33. package/lib/database/drizzle-schema.js +6 -3
  34. package/lib/database/index.d.ts +1 -1
  35. package/lib/database/index.js +2 -2
  36. package/lib/database/schema/ecosystem.d.ts +53 -0
  37. package/lib/database/schema/ecosystem.js +14 -3
  38. package/lib/database/schema/pipeline.d.ts +38 -0
  39. package/lib/database/schema/pipeline.js +20 -2
  40. package/lib/database/schema/plugin.d.ts +265 -0
  41. package/lib/database/schema/plugin.js +34 -3
  42. package/lib/database/schema/reporting-analytics.d.ts +786 -4
  43. package/lib/database/schema/reporting-analytics.js +111 -5
  44. package/lib/index.d.ts +5 -3
  45. package/lib/index.js +5 -3
  46. package/lib/testing/exports.json +24 -0
  47. package/package.json +4 -3
@@ -1,3 +1,5 @@
1
+ import { type PluginScanFinding, type VulnAcceptance } from '@pipeline-builder/api-core';
2
+ import { type SQL } from 'drizzle-orm';
1
3
  import type { ExposureSource } from '../../database/schema/reporting-analytics.js';
2
4
  /** How far behind a resolved version is from the newest one available. */
3
5
  export type VersionGap = 'major' | 'minor' | 'patch' | 'none';
@@ -11,8 +13,14 @@ export interface ResolvedStep {
11
13
  pluginName: string;
12
14
  declaredVersion?: string | null;
13
15
  resolvedVersion?: string | null;
16
+ /** The resolved record: a `plugins` row id or a listing version id. */
17
+ resolvedPluginId?: string | null;
18
+ /** The image digest the resolved version is pinned to (null = image-less). */
19
+ resolvedDigest?: string | null;
14
20
  latestVersion?: string | null;
15
21
  withinPolicy?: boolean;
22
+ /** The root org of a team's pipeline (see the column). */
23
+ rootOrgId?: string | null;
16
24
  }
17
25
  /**
18
26
  * How far `resolved` is behind `latest`.
@@ -39,8 +47,17 @@ export declare function replacePipelineResolution(pipelineId: string, steps: rea
39
47
  * the instance that declares the plugin, and re-resolving each of those pipelines
40
48
  * from scratch would make publishing cost O(pipelines) work — so only the two
41
49
  * columns a publish can change are touched.
50
+ *
51
+ * `owner` says whose plugin it is: a listing (`publisher` handle — every org's
52
+ * pipelines that declare it, so the caller must be elevated), or an org's own
53
+ * plugin (`orgId` — that org's pipelines only: an org plugin's name is unique
54
+ * only within its org, so another org's same-named plugin is a different one).
42
55
  */
43
- export declare function refreshLatestForPlugin(pluginName: string, pluginPublisher: string | null, latestVersion: string): Promise<number>;
56
+ export declare function refreshLatestForPlugin(pluginName: string, owner: {
57
+ publisher: string;
58
+ } | {
59
+ orgId: string;
60
+ }, latestVersion: string): Promise<number>;
44
61
  /** One flagged version, as the rescan reports it. */
45
62
  export interface ExposureInput {
46
63
  orgId: string;
@@ -53,11 +70,55 @@ export interface ExposureInput {
53
70
  criticalCount: number;
54
71
  highCount: number;
55
72
  topFindings?: Array<Record<string, unknown>>;
73
+ /** Every Critical/High finding key of the version (see the column). */
74
+ findingKeys?: string[];
75
+ }
76
+ /**
77
+ * Whether an exposure row is LIVE: not fixed, not dismissed as a false positive,
78
+ * and not under an acceptance that is still in force.
79
+ *
80
+ * The ONE predicate every reader uses (the report section, needs-attention, the
81
+ * org's open list). Two readers that disagree about what "exposed" means produce
82
+ * a report whose vulnerability panel and needs-attention list contradict each
83
+ * other — which is how this predicate came to exist. An acceptance LAPSES at its
84
+ * deadline and the row is live again; one with no deadline (which
85
+ * `triageExposure` refuses to write) is treated as in force, as every reader
86
+ * already did.
87
+ *
88
+ * `alias` is the table name or alias the caller's query uses.
89
+ */
90
+ export declare function liveExposurePredicate(now: Date, alias?: string): SQL;
91
+ /** What a version's scan state contributes to an exposure (see {@link exposureFromScan}). */
92
+ export interface ScanState {
93
+ /** The digest the step runs. A scan summary describing another image is ignored. */
94
+ imageDigest?: string | null;
95
+ scanSummary?: unknown;
96
+ scanFlag?: unknown;
97
+ scanFlaggedAt?: Date | string | null;
98
+ }
99
+ /** The counts an exposure row carries. */
100
+ export interface ExposureCounts {
101
+ criticalCount: number;
102
+ highCount: number;
103
+ topFindings: PluginScanFinding[];
104
+ /** Keys of EVERY Critical/High finding itemized (fixable or not), sorted. */
105
+ findingKeys: string[];
56
106
  }
107
+ /**
108
+ * The exposure a version's scan state amounts to, or null when it has none.
109
+ *
110
+ * Reads the rescan's `scan_summary` (fixable Critical AND High, whether or not the
111
+ * gate tripped) and falls back to `scan_flag` for a version flagged before any
112
+ * summary was written. A summary whose digest is not the step's is IGNORED: a
113
+ * version rebuilt on a patched base is a different artifact, and its findings say
114
+ * nothing about the image the pipeline runs. Only FIXABLE findings count — an
115
+ * exposure is something a rebuild or an upgrade can clear.
116
+ */
117
+ export declare function exposureFromScan(state: ScanState): ExposureCounts | null;
57
118
  /**
58
119
  * Open or refresh exposures.
59
120
  *
60
- * Upserts on (pipeline, plugin, version, source) so a nightly rescan REFRESHES the
121
+ * Upserts on (pipeline, plugin, version, digest, source) so a nightly rescan REFRESHES the
61
122
  * counts rather than opening a new row every night — otherwise the report would
62
123
  * show one exposure per night of the same problem, which reads as an escalating
63
124
  * situation when nothing changed.
@@ -68,18 +129,117 @@ export interface ExposureInput {
68
129
  * acceptance, because that is what the deadline was for.
69
130
  */
70
131
  export declare function upsertExposures(inputs: readonly ExposureInput[], now?: Date): Promise<number>;
132
+ /** One version a pipeline still declares / runs WITH open findings. */
133
+ export interface LiveVersion {
134
+ pluginName: string;
135
+ pluginVersion: string;
136
+ imageDigest?: string | null;
137
+ }
71
138
  /**
72
- * Close the exposures a pipeline has moved off.
139
+ * Close the exposures of one SOURCE a pipeline has moved off — `fixed` when the
140
+ * pipeline still uses the plugin at another version, `removed` when no live step
141
+ * uses the plugin at all (the triage fields stay, so a re-added step gets an
142
+ * unexpired acceptance back — see {@link upsertExposures}).
73
143
  *
74
- * Called when a deploy records a new step manifest: anything flagged for that
75
- * pipeline whose version is NOT in the set it now runs is fixed. Closing by
144
+ * Called when a deploy records a new step manifest (`deployed`) or a pipeline
145
+ * write re-resolves its config (`declared`): anything open for that pipeline and
146
+ * source whose (version, digest) is NOT in `liveVersions` is fixed. Closing by
76
147
  * absence rather than waiting for a rescan to notice is what makes "we fixed it"
77
148
  * show up in the report on the next deploy instead of the next night.
149
+ *
150
+ * Per source, because the two sides move independently: a config updated to the
151
+ * fixed version closes the DECLARED exposure while the old image keeps running,
152
+ * and its DEPLOYED exposure must stay open until the deploy lands.
153
+ */
154
+ export declare function closeFixedExposures(pipelineId: string, source: ExposureSource, liveVersions: readonly LiveVersion[], now?: Date, opts?: {
155
+ livePlugins?: readonly string[];
156
+ }): Promise<number>;
157
+ /**
158
+ * Close every open exposure of a pipeline (or of one source) as `removed` — the
159
+ * pipeline was deleted, or deregistered (its deploy is gone). The triage fields
160
+ * stay: a restore re-records the pipeline's resolution, and
161
+ * {@link upsertExposures} reopens the rows (restoring an acceptance still in
162
+ * force). Returns the rows closed.
163
+ */
164
+ export declare function closePipelineExposures(pipelineId: string, state: 'removed', opts?: {
165
+ source?: ExposureSource;
166
+ }, now?: Date): Promise<number>;
167
+ /** One step of a pipeline as {@link reconcilePipelineExposures} takes it. */
168
+ export interface ExposureStep {
169
+ pluginPublisher?: string | null;
170
+ pluginName: string;
171
+ pluginVersion: string;
172
+ imageDigest?: string | null;
173
+ /** The resolved version's scan state, as stored on its row. */
174
+ scan: ScanState;
175
+ }
176
+ /**
177
+ * Bring one pipeline's exposures of one source in line with the steps it now
178
+ * declares (`declared`) or runs (`deployed`): open / refresh a row for every step
179
+ * whose version has open fixable findings, close the rest by absence.
180
+ *
181
+ * Never throws — exposure tracking is reporting, and a pipeline write or a deploy
182
+ * registration must not fail because the report table could not be written.
78
183
  */
79
- export declare function closeFixedExposures(pipelineId: string, liveVersions: ReadonlyArray<{
184
+ export declare function reconcilePipelineExposures(input: {
185
+ orgId: string;
186
+ pipelineId: string;
187
+ source: ExposureSource;
188
+ steps: readonly ExposureStep[];
189
+ }, now?: Date): Promise<{
190
+ opened: number;
191
+ closed: number;
192
+ }>;
193
+ /** A plugin version (optionally one image of it) whose pipelines are wanted. */
194
+ export interface PluginVersionRef {
80
195
  pluginName: string;
81
196
  pluginVersion: string;
82
- }>, now?: Date): Promise<number>;
197
+ /** Listing publisher handle; null = an org's own plugin row; omitted = either. */
198
+ pluginPublisher?: string | null;
199
+ /** Narrow to pipelines on this image. Omitted = any image of the version. */
200
+ imageDigest?: string | null;
201
+ /** Narrow to these orgs. Omitted = every org the caller's scope can read. */
202
+ orgIds?: readonly string[];
203
+ }
204
+ /** A pipeline that declares or runs a given plugin version. */
205
+ export interface PipelineOnVersion {
206
+ orgId: string;
207
+ pipelineId: string;
208
+ pipelineName: string | null;
209
+ /** `deployed` = its last deploy's step manifest; `declared` = its config resolves to it. */
210
+ source: ExposureSource;
211
+ pluginPublisher: string | null;
212
+ imageDigest: string | null;
213
+ /** The steps on it: `synth` or `<stage>/<step>` (declared) / `<stage>/<action>` (deployed). */
214
+ steps: string[];
215
+ }
216
+ /**
217
+ * Every LIVE pipeline declaring (`pipeline_plugin_resolution`) or running
218
+ * (`pipeline_step_manifests`) a plugin version, one entry per (pipeline, source)
219
+ * with the steps on it. A soft-deleted pipeline is NOT affected: counting it
220
+ * would put a pipeline nobody can open into a notice or a report. Reads under
221
+ * the CALLER's tenant scope: an org-scoped caller sees its own pipelines, the
222
+ * rescan (elevated) sees every org's.
223
+ */
224
+ export declare function pipelinesOnVersion(ref: PluginVersionRef): Promise<PipelineOnVersion[]>;
225
+ /**
226
+ * The rescan's entry point: one rescanned version's findings, fanned out to every
227
+ * pipeline that declares or runs it.
228
+ *
229
+ * A version with open fixable findings opens / refreshes one exposure per
230
+ * (pipeline, source); a version that came back CLEAN closes every open exposure
231
+ * on it (`fixed`), so a rebuild-in-place or an advisory withdrawn upstream shows
232
+ * as fixed on the next night rather than lingering until each pipeline redeploys.
233
+ *
234
+ * Must run with a scope that can read and write every org's rows (the rescan's
235
+ * elevated context). Never throws.
236
+ */
237
+ export declare function syncVersionExposures(ref: PluginVersionRef, scan: ScanState, now?: Date): Promise<{
238
+ opened: number;
239
+ closed: number;
240
+ }>;
241
+ /** Close every open exposure on a version (optionally one image of it). */
242
+ export declare function closeVersionExposures(ref: PluginVersionRef, now?: Date): Promise<number>;
83
243
  /**
84
244
  * The org's live exposures, for the report section.
85
245
  *
@@ -99,20 +259,106 @@ export declare function openExposures(orgId: string): Promise<Array<{
99
259
  triageState: string;
100
260
  flaggedAt: Date;
101
261
  }>>;
262
+ /** One exposure row as the per-pipeline security views read it. */
263
+ export interface PipelineExposure {
264
+ id: string;
265
+ orgId: string;
266
+ pipelineId: string;
267
+ pluginPublisher: string | null;
268
+ pluginName: string;
269
+ pluginVersion: string;
270
+ imageDigest: string | null;
271
+ source: ExposureSource;
272
+ criticalCount: number;
273
+ highCount: number;
274
+ topFindings: Array<Record<string, unknown>>;
275
+ findingKeys: string[];
276
+ /** As stored; an `accepted` row past its deadline reads `open` (see {@link liveExposurePredicate}). */
277
+ triageState: string;
278
+ /** An acceptance still in force right now. */
279
+ acceptanceActive: boolean;
280
+ acceptedUntil: Date | null;
281
+ acceptedFindingKeys: string[];
282
+ acceptedKev: boolean;
283
+ triageReason: string | null;
284
+ triagedBy: string | null;
285
+ triagedAt: Date | null;
286
+ flaggedAt: Date;
287
+ fixedAt: Date | null;
288
+ }
102
289
  /**
103
- * Record a triage decision.
104
- *
105
- * `accepted` REQUIRES a deadline and a reason. A permanent, unexplained mute is
106
- * how a finding stops being anybody's problem, and there is no good reason to
107
- * support it — the caller gets an error rather than a silently unbounded
108
- * acceptance.
290
+ * One pipeline's exposures: the unfixed ones (open, accepted, false positive),
291
+ * plus the fixed ones when asked — most severe first. Reads under the CALLER's
292
+ * scope, narrowed to `orgId`.
109
293
  */
110
- export declare function triageExposure(orgId: string, id: string, decision: {
294
+ export declare function pipelineExposures(orgId: string, pipelineId: string, opts?: {
295
+ includeFixed?: boolean;
296
+ now?: Date;
297
+ }): Promise<PipelineExposure[]>;
298
+ /** One exposure row of `orgId`, or null. */
299
+ export declare function exposureById(orgId: string, id: string, now?: Date): Promise<PipelineExposure | null>;
300
+ /** A triage decision. */
301
+ export interface TriageDecision {
111
302
  state: 'open' | 'accepted' | 'false_positive';
112
303
  reason?: string;
113
304
  acceptedUntil?: Date;
305
+ /** Accept KEV-listed findings too. Never implied: an acceptance without it does not cover them. */
306
+ kev?: boolean;
114
307
  actor: string;
115
- }): Promise<boolean>;
308
+ }
309
+ /**
310
+ * Record a triage decision on one exposure — and on its SIBLINGS: the same
311
+ * pipeline's other open row for the same version and image (the declared and
312
+ * the deployed side). A risk decision is about what the pipeline runs, and
313
+ * leaving one side open would have the report and the lookup disagree.
314
+ *
315
+ * `accepted` REQUIRES a deadline and a reason. A permanent, unexplained mute is
316
+ * how a finding stops being anybody's problem, and there is no good reason to
317
+ * support it — the caller gets an error rather than a silently unbounded
318
+ * acceptance. The accepted finding keys are FROZEN at the decision: an
319
+ * acceptance covers what was there when it was made, never a finding since.
320
+ *
321
+ * Returns the updated rows (empty when `id` is not `orgId`'s unfixed exposure).
322
+ */
323
+ export declare function triageExposure(orgId: string, id: string, decision: TriageDecision): Promise<PipelineExposure[]>;
324
+ /** `name@version@digest` — the key {@link activeAcceptances} answers under. */
325
+ export declare const acceptanceKey: (pluginName: string, pluginVersion: string, imageDigest?: string | null) => string;
326
+ /** A pipeline's acceptances in force, keyed by {@link acceptanceKey}, plus the accepted versions per plugin. */
327
+ export interface PipelineAcceptances {
328
+ byKey: Map<string, VulnAcceptance & {
329
+ exposureIds: string[];
330
+ }>;
331
+ /** `pluginName` → versions with an acceptance (the resolver's `acceptedVersions` hint). */
332
+ versionsByPlugin: Map<string, Set<string>>;
333
+ }
334
+ /**
335
+ * The acceptances IN FORCE for one pipeline (`accepted`, unfixed, deadline in the
336
+ * future), merged per version and image: the union of the finding keys the rows
337
+ * froze, KEV only when every merged row accepted it. Reads under the caller's
338
+ * scope narrowed to `orgId`; a failed read is "no acceptance" — the stricter answer.
339
+ */
340
+ export declare function activeAcceptances(orgId: string, pipelineId: string, now?: Date): Promise<PipelineAcceptances>;
341
+ /** An acceptance that lapsed, as {@link reopenLapsedAcceptances} returns it. */
342
+ export interface LapsedAcceptance {
343
+ id: string;
344
+ orgId: string;
345
+ pipelineId: string;
346
+ pluginPublisher: string | null;
347
+ pluginName: string;
348
+ pluginVersion: string;
349
+ imageDigest: string | null;
350
+ triagedBy: string | null;
351
+ triageReason: string | null;
352
+ acceptedUntil: Date | null;
353
+ }
354
+ /**
355
+ * Reopen every acceptance whose deadline has passed, and return them so the
356
+ * pipeline owners can be told (N33). Readers already treat such a row as open;
357
+ * writing it makes the stored state the truth and gives the notice exactly one
358
+ * moment to fire. Must run with a scope that can write every org's rows (the
359
+ * nightly rescan's elevated context). Never throws.
360
+ */
361
+ export declare function reopenLapsedAcceptances(now?: Date): Promise<LapsedAcceptance[]>;
116
362
  /**
117
363
  * Exposures whose acceptance has lapsed, for the org's security notifications.
118
364
  *