@preventive/triage 1.0.0-alpha.50 → 1.0.0-alpha.52
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/common/managed/finding-detail.d.ts +2 -1
- package/common/managed/finding-detail.js +42 -11
- package/common/managed/finding-values.d.ts +2 -0
- package/common/managed/finding-values.js +44 -0
- package/common/managed/lite-name.d.ts +2 -0
- package/common/managed/lite-name.js +20 -0
- package/common/managed/npm-packages.d.ts +2 -0
- package/common/managed/npm-packages.js +18 -1
- package/common/managed/routes.js +19 -7
- package/common/managed/team-app.d.ts +1 -0
- package/common/managed/team-app.js +17 -1
- package/out/activity.js +4 -4
- package/out/{chunk-75YLHBCM.js → chunk-2K6UN3PC.js} +1 -1
- package/out/{chunk-HTBYHSPG.js → chunk-CHRBUGVN.js} +1 -1
- package/out/{chunk-4XMP7YVH.js → chunk-FMXWXDR4.js} +1 -1
- package/out/chunk-HT2SSA3V.js +1 -0
- package/out/{chunk-2WDZPXVL.js → chunk-IMM274IW.js} +1 -1
- package/out/{chunk-FKBXNUW4.js → chunk-KOGNQDSJ.js} +1 -1
- package/out/chunk-LJ7XRXFU.js +52 -0
- package/out/{chunk-PBZPYIG7.js → chunk-MYGJBYYJ.js} +1 -1
- package/out/chunk-NCN3M5EP.js +3 -0
- package/out/chunk-OGAYMEER.js +232 -0
- package/out/client-managed-import.js +1 -1
- package/out/client-managed.js +26 -26
- package/out/compare.js +14 -14
- package/out/dialogs.js +31 -31
- package/out/graph.js +1 -1
- package/out/index.html +7 -7
- package/out/scan.js +2 -2
- package/out/view.css +1 -1
- package/out/view.js +11 -11
- package/package.json +8 -1
- package/server-managed/agent-merges.ts +1 -1
- package/server-managed/app-agents.ts +4 -2
- package/server-managed/commit-lockfiles.ts +7 -4
- package/server-managed/comparison.ts +13 -2
- package/server-managed/db-neon.ts +13 -3
- package/server-managed/db.ts +2 -1
- package/server-managed/dependency-review-comments.ts +16 -18
- package/server-managed/dependency-reviews.ts +45 -27
- package/server-managed/github-app.ts +3 -42
- package/server-managed/github-requests.ts +127 -0
- package/server-managed/http.ts +433 -129
- package/server-managed/listed-activity.ts +39 -0
- package/server-managed/locked-packages.ts +28 -5
- package/server-managed/npm-kept.ts +18 -4
- package/server-managed/npm-patch.ts +325 -0
- package/server-managed/pull-dependencies.ts +9 -6
- package/server-managed/pull-dependency-reviews.ts +19 -13
- package/server-managed/repository-agents.ts +162 -69
- package/server-managed/repository-dependency-reviews.ts +6 -5
- package/server-managed/repository-pulls.ts +114 -58
- package/server-managed/repository-pulse.ts +5 -3
- package/server-managed/repository-stamp.ts +41 -0
- package/server-managed/reviewed-dependencies.ts +4 -5
- package/server-managed/team-app.ts +3 -1
- package/server-managed/team-reports.ts +29 -10
- package/out/chunk-6LNQTKAV.js +0 -52
- package/out/chunk-6S2U2UYG.js +0 -232
- package/out/chunk-FC65TPUB.js +0 -1
- package/out/chunk-UK7RGJT6.js +0 -3
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export const FINDING_DETAIL_FIELDS: readonly string[]
|
|
2
|
-
export function liteReportData(data: unknown): unknown
|
|
2
|
+
export function liteReportData(data: unknown, options?: { fold?: boolean }): unknown
|
|
3
|
+
export function liteWorkspace<T extends { filename: string; data: unknown }>(reports: readonly T[], links: readonly (readonly string[])[]): { reports: T[]; folded: boolean }
|
|
@@ -1,17 +1,24 @@
|
|
|
1
|
-
import { findingTitle } from '@preventive/report'
|
|
1
|
+
import { findingTitle, reportEntries, revalidateKindOf } from '@preventive/report'
|
|
2
|
+
import { managedTeamRevalidationConflicted } from './team-app.js'
|
|
3
|
+
import { liteName } from './lite-name.js'
|
|
2
4
|
|
|
3
5
|
// A finding's detail: what its popup, the views besides the board and an
|
|
4
6
|
// export read, and the board does not. Its narrative and evidence, the hash
|
|
5
|
-
// its Code button finds a bundle by,
|
|
7
|
+
// its Code button finds a bundle by, the export it names, the facts its card
|
|
8
|
+
// and an export tell (write-md-finding.js PLAIN_FIELDS, CODE_FIELDS, but for
|
|
9
|
+
// `priority`, which the board sorts and filters by), and the run meta its
|
|
10
|
+
// card's run line and a report's header read (its `model` and `type`, which
|
|
11
|
+
// the board's filters read, stay). A team's
|
|
6
12
|
// reports are first sent without it (server-managed/team-reports.ts), the
|
|
7
13
|
// board drawn from them, and their findings filled in from the whole reports
|
|
8
14
|
// (ui/view/managed-details.js).
|
|
9
|
-
export const FINDING_DETAIL_FIELDS = Object.freeze([
|
|
10
|
-
'
|
|
15
|
+
export const FINDING_DETAIL_FIELDS = Object.freeze([
|
|
16
|
+
'description', 'evidence', 'impact', 'reproduction', 'recommendation', 'confidenceReason', 'revalidateVerdict', 'revalidateRecommendation',
|
|
17
|
+
'fileHash', 'exportName', 'methodName',
|
|
18
|
+
'severityReason', 'commitHash', 'auditedCommit', 'detectedAt', 'committedAt', 'dateCreated', 'status', 'branch', 'pocStatus', 'parent', 'slug', 'reportPath',
|
|
19
|
+
'category', 'discoveredIn', 'effort', 'exportsMode',
|
|
20
|
+
])
|
|
11
21
|
const DETAIL = new Set(FINDING_DETAIL_FIELDS)
|
|
12
|
-
// More of a name than a board card's three lines show.
|
|
13
|
-
const NAME_CHARS = 200
|
|
14
|
-
|
|
15
22
|
// A finding without its detail. One without a title of its own is named by its
|
|
16
23
|
// description's first line (findingTitle), which it keeps as its description,
|
|
17
24
|
// as much of it as a card shows.
|
|
@@ -20,13 +27,37 @@ function liteFinding(finding) {
|
|
|
20
27
|
const lite = Object.fromEntries(Object.entries(finding).filter(([key]) => !DETAIL.has(key)))
|
|
21
28
|
const titled = typeof finding.title === 'string' && finding.title.trim() !== ''
|
|
22
29
|
const title = titled ? '' : findingTitle(finding)
|
|
23
|
-
if (title) lite.description = title
|
|
30
|
+
if (title) lite.description = liteName(title)
|
|
24
31
|
return lite
|
|
25
32
|
}
|
|
26
33
|
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
const isPass = finding => revalidateKindOf(finding) === 'revalidation'
|
|
35
|
+
|
|
36
|
+
// A report's data, its findings without their detail. A workspace opening in
|
|
37
|
+
// App mode shows of an entry holding the revalidation pass's row that row
|
|
38
|
+
// alone (ui/view/group.js foldedTabs): with `fold`, such an entry is sent as
|
|
39
|
+
// the pass's rows, the rows beneath them left for the whole reports
|
|
40
|
+
// (ui/view/managed-details.js).
|
|
41
|
+
export function liteReportData(data, { fold = false } = {}) {
|
|
29
42
|
const key = Array.isArray(data?.findings) ? 'findings' : Array.isArray(data?.groups) ? 'groups' : null
|
|
30
43
|
if (!key) return data
|
|
31
|
-
|
|
44
|
+
const entries = data[key].map(entry => {
|
|
45
|
+
if (!Array.isArray(entry)) return liteFinding(entry)
|
|
46
|
+
const shown = fold && entry.length > 1 && entry.some(isPass) ? entry.filter(isPass) : entry
|
|
47
|
+
return shown.map(liteFinding)
|
|
48
|
+
})
|
|
49
|
+
return { ...data, [key]: entries }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const rows = data => (reportEntries(data) ?? []).flat().length
|
|
53
|
+
|
|
54
|
+
// A team's reports as its board is first sent them (server-managed/team-reports.ts),
|
|
55
|
+
// each `liteReportData`, folded where nothing in the workspace contradicts the
|
|
56
|
+
// revalidation pass, which opens a view of it in App mode
|
|
57
|
+
// (ui/view/render.js configureReportRevalidation); `folded` where that left
|
|
58
|
+
// rows out.
|
|
59
|
+
export function liteWorkspace(reports, links) {
|
|
60
|
+
const fold = !managedTeamRevalidationConflicted(Object.assign(reports.map(({ filename, data }) => ({ filename, data })), { links }))
|
|
61
|
+
const lite = reports.map(report => ({ ...report, data: liteReportData(report.data, { fold }) }))
|
|
62
|
+
return { reports: lite, folded: fold && lite.some((report, index) => rows(report.data) < rows(reports[index].data)) }
|
|
32
63
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// A finding's `package` and `repo`: the same few across a team's many
|
|
2
|
+
// findings. A team's reports are sent with each once, in the response's
|
|
3
|
+
// `values`, and each finding's as its index there
|
|
4
|
+
// (server-managed/team-reports.ts), put back as they are read
|
|
5
|
+
// (client/managed/session.js).
|
|
6
|
+
const FIELDS = ['package', 'repo']
|
|
7
|
+
|
|
8
|
+
const entriesKey = data => Array.isArray(data?.findings) ? 'findings' : Array.isArray(data?.groups) ? 'groups' : null
|
|
9
|
+
|
|
10
|
+
// `values`, and `index(data)`: a report's data with its findings' values as
|
|
11
|
+
// their index in it, one value at one index across every report it is given.
|
|
12
|
+
export function findingValueIndex() {
|
|
13
|
+
const indexes = new Map(), values = []
|
|
14
|
+
const indexOf = value => {
|
|
15
|
+
const key = JSON.stringify(value)
|
|
16
|
+
if (!indexes.has(key)) indexes.set(key, values.push(value) - 1)
|
|
17
|
+
return indexes.get(key)
|
|
18
|
+
}
|
|
19
|
+
const indexed = finding => {
|
|
20
|
+
if (!finding || typeof finding !== 'object' || !FIELDS.some(field => finding[field] != null)) return finding
|
|
21
|
+
const copy = { ...finding }
|
|
22
|
+
for (const field of FIELDS) if (finding[field] != null) copy[field] = indexOf(finding[field])
|
|
23
|
+
return copy
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
values,
|
|
27
|
+
index(data) {
|
|
28
|
+
const key = entriesKey(data)
|
|
29
|
+
return key ? { ...data, [key]: data[key].map(entry => Array.isArray(entry) ? entry.map(indexed) : indexed(entry)) } : data
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// A report's data, as read, its findings' values put in place of their index
|
|
35
|
+
// in a response's `values`; one sent without any as it is.
|
|
36
|
+
export function expandFindingValues(data, values) {
|
|
37
|
+
const key = entriesKey(data)
|
|
38
|
+
if (!key || !Array.isArray(values)) return data
|
|
39
|
+
for (const finding of data[key].flat()) {
|
|
40
|
+
if (!finding || typeof finding !== 'object') continue
|
|
41
|
+
for (const field of FIELDS) if (Number.isInteger(finding[field])) finding[field] = values[finding[field]]
|
|
42
|
+
}
|
|
43
|
+
return data
|
|
44
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { reportEntries } from '@preventive/report'
|
|
2
|
+
|
|
3
|
+
// As much of a name as a heading takes (@preventive/report write-md-finding.js
|
|
4
|
+
// HEADING_MAX), about a board card's three lines. A board's copy of a team's
|
|
5
|
+
// reports (finding-detail.js) cuts one longer to end in `CUT`, an ellipsis
|
|
6
|
+
// once read.
|
|
7
|
+
const CUT = '...', NAME_CHARS = 120
|
|
8
|
+
|
|
9
|
+
export const liteName = name => name.length > NAME_CHARS ? `${name.slice(0, NAME_CHARS - CUT.length)}${CUT}` : name
|
|
10
|
+
|
|
11
|
+
// A report's data from its board's copy, as read: a name cut ends in an
|
|
12
|
+
// ellipsis. One as long as a cut leaves it and ending in `CUT` is one; one
|
|
13
|
+
// shorter ends as it does.
|
|
14
|
+
export function readLiteReportData(data) {
|
|
15
|
+
for (const finding of (reportEntries(data) ?? []).flat()) {
|
|
16
|
+
const name = finding?.description
|
|
17
|
+
if (typeof name === 'string' && name.length === NAME_CHARS && name.endsWith(CUT)) finding.description = `${name.slice(0, -CUT.length)}…`
|
|
18
|
+
}
|
|
19
|
+
return data
|
|
20
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export const MAX_TEAM_NPM_SCOPES: number
|
|
2
2
|
export function isNpmPackageName(value: unknown): value is string
|
|
3
3
|
export function isNpmPackageSpec(value: unknown): value is string
|
|
4
|
+
export function isPatchHash(value: unknown): value is string
|
|
5
|
+
export function isPatchPath(value: unknown): value is string
|
|
4
6
|
export function npmPackageScope(name: string): string | null
|
|
5
7
|
export function normalizeNpmScope(value: unknown): string | null
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// npm package names, versions and scopes as the managed npm viewer takes
|
|
2
|
-
// them, shared by its routes, the
|
|
2
|
+
// them, and the pnpm patches its reviews read, shared by its routes, the
|
|
3
|
+
// server and the Teams page.
|
|
3
4
|
|
|
4
5
|
// Upstream's rule (@preventive/upstream src/args.js): a scope, then a name
|
|
5
6
|
// starting with none of `.`, `_` and `-`, 214 characters in all.
|
|
@@ -9,6 +10,9 @@ const PACKAGE_SPEC = /^(?=.{1,256}$)[\dA-Za-z][\w.+-]*$/u
|
|
|
9
10
|
// npm's rule for a scope: lowercase, and not starting with `.` or `_`.
|
|
10
11
|
const SCOPE = /^@[\da-z~-][\w.~-]*$/u
|
|
11
12
|
export const MAX_TEAM_NPM_SCOPES = 100
|
|
13
|
+
// A pnpm patch's hash as a lockfile names it: a sha256 in hex, or pnpm 9's
|
|
14
|
+
// md5 in base32.
|
|
15
|
+
const PATCH_HASH = /^(?:[\da-f]{64}|[2-7a-z]{26})$/u
|
|
12
16
|
|
|
13
17
|
export function isNpmPackageName(value) {
|
|
14
18
|
return typeof value === 'string' && PACKAGE_NAME.test(value)
|
|
@@ -18,6 +22,19 @@ export function isNpmPackageSpec(value) {
|
|
|
18
22
|
return typeof value === 'string' && PACKAGE_SPEC.test(value)
|
|
19
23
|
}
|
|
20
24
|
|
|
25
|
+
export function isPatchHash(value) {
|
|
26
|
+
return typeof value === 'string' && PATCH_HASH.test(value)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// A path as GitHub's contents take it, as a pnpm patch's file is read
|
|
30
|
+
// (server-managed/npm-patch.ts) and a file of the package it changes is
|
|
31
|
+
// named: names with no empty, `.` or `..` one among them, nor control
|
|
32
|
+
// characters or backslashes.
|
|
33
|
+
export function isPatchPath(value) {
|
|
34
|
+
return typeof value === 'string' && value.length > 0 && value.length <= 1024 && !/[\p{Cc}\\]/u.test(value)
|
|
35
|
+
&& value.split('/').every(part => part !== '' && part !== '.' && part !== '..')
|
|
36
|
+
}
|
|
37
|
+
|
|
21
38
|
// `@scope` of a scoped name, lowercased as scopes are compared; null for an
|
|
22
39
|
// unscoped one.
|
|
23
40
|
export function npmPackageScope(name) {
|
package/common/managed/routes.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Managed client pages only. API paths and E2E share hashes are not routes.
|
|
2
2
|
import { BUNDLE_TABS } from '../bundle-tabs.js'
|
|
3
3
|
import { MAX_FINDING_ID_LENGTH, isLinkableFindingId } from '../finding-id.js'
|
|
4
|
-
import { isNpmPackageName, isNpmPackageSpec } from './npm-packages.js'
|
|
4
|
+
import { isNpmPackageName, isNpmPackageSpec, isPatchHash, isPatchPath } from './npm-packages.js'
|
|
5
5
|
|
|
6
6
|
// How Compare reviews the changes, past its Overview: `code`, beside the
|
|
7
7
|
// tree of changed files. A link names the mode after the bundle or version
|
|
@@ -96,17 +96,26 @@ export function managedRoutePath(route) {
|
|
|
96
96
|
// Search, Compare or Treemap tab, `/npm/<name>[@<version or dist-tag>][/<tab>…]`. Code names
|
|
97
97
|
// its file by number and its lines in the fragment, as a bundle's Code
|
|
98
98
|
// does: `/code/<file>#L4`. Compare names the version compared with, then
|
|
99
|
-
// its mode past the Overview: `/compare/<version>/code` or `/diff`.
|
|
99
|
+
// its mode past the Overview: `/compare/<version>/code` or `/diff`. Review
|
|
100
|
+
// names the version reviewed against, and, of a version as a pnpm patch
|
|
101
|
+
// changes it, the patch (`patch`): its hash, and where it is read, a
|
|
102
|
+
// repository by its id, a commit and its file's path there,
|
|
103
|
+
// `?patch=<hash>&repo=<repo id>&commit=<commit>&path=<path>`.
|
|
100
104
|
export const NPM_TABS = new Set(['overview', 'code', 'search', 'compare', 'review', 'treemap'])
|
|
105
|
+
// A patch as the server reads one (server-managed/npm-patch.ts requestedPatch).
|
|
106
|
+
const isPatch = patch => isPatchHash(patch?.hash) && isRepoId(patch.repoId) && COMMIT.test(patch.commit ?? '') && isPatchPath(patch.path)
|
|
101
107
|
function npmPackagePath(route) {
|
|
102
108
|
if (route.packageName == null) return '/npm'
|
|
103
109
|
const tab = route.bundleTab ?? 'overview'
|
|
104
110
|
if (!isNpmPackageName(route.packageName) || (route.packageSpec != null && !isNpmPackageSpec(route.packageSpec)) || !NPM_TABS.has(tab)
|
|
105
111
|
|| (route.compareSpec != null && !isNpmPackageSpec(route.compareSpec))) return null
|
|
106
112
|
if (tab === 'review') {
|
|
107
|
-
if (route.reviewPackage != null && !isNpmPackageName(route.reviewPackage) || route.reviewSpec != null && !isNpmPackageSpec(route.reviewSpec)
|
|
113
|
+
if (route.reviewPackage != null && !isNpmPackageName(route.reviewPackage) || route.reviewSpec != null && !isNpmPackageSpec(route.reviewSpec)
|
|
114
|
+
|| route.patch != null && !isPatch(route.patch)) return null
|
|
108
115
|
const base = route.reviewPackage == null ? '' : `/${route.reviewPackage}${route.reviewSpec == null ? '' : `@${route.reviewSpec}`}`
|
|
109
|
-
|
|
116
|
+
const patch = route.patch == null ? ''
|
|
117
|
+
: `?patch=${route.patch.hash}&repo=${route.patch.repoId}&commit=${route.patch.commit}&path=${route.patch.path.split('/').map(encodeURIComponent).join('/')}`
|
|
118
|
+
return `/npm/${route.packageName}${route.packageSpec == null ? '' : `@${route.packageSpec}`}/review${base}${patch}`
|
|
110
119
|
}
|
|
111
120
|
const file = tab === 'code' && isLineNumber(route.file) ? `/${route.file}` : ''
|
|
112
121
|
const lines = file && isLineNumber(route.line)
|
|
@@ -115,15 +124,18 @@ function npmPackagePath(route) {
|
|
|
115
124
|
return `/npm/${route.packageName}${route.packageSpec == null ? '' : `@${route.packageSpec}`}${tab === 'overview' ? '' : `/${tab}`}${file}${compare}${lines}`
|
|
116
125
|
}
|
|
117
126
|
|
|
118
|
-
function parseNpmRoute(path, hash) {
|
|
127
|
+
function parseNpmRoute(path, hash, search) {
|
|
119
128
|
const review = /^\/npm\/((?:@[^/@]+\/)?[^/@]+)(?:@([^/]+))?\/review(?:\/((?:@[^/@]+\/)?[^/@]+)(?:@([^/]+))?)?$/u.exec(path)
|
|
120
129
|
if (review) {
|
|
121
130
|
if (!isNpmPackageName(review[1]) || review[2] != null && !isNpmPackageSpec(review[2])
|
|
122
131
|
|| review[3] != null && !isNpmPackageName(review[3]) || review[4] != null && !isNpmPackageSpec(review[4])) return null
|
|
123
132
|
// The full dependency form is canonical; a bare version is a shortcut.
|
|
124
133
|
const versionOnly = review[3] && /^\d+\.\d+\.\d+(?:[-+].*)?$/u.test(review[3]) && review[4] == null
|
|
134
|
+
const patch = search.has('patch')
|
|
135
|
+
? { hash: search.get('patch'), repoId: /^[1-9]\d*$/u.test(search.get('repo') ?? '') ? Number(search.get('repo')) : null, commit: search.get('commit'), path: search.get('path') } : null
|
|
136
|
+
if (patch && !isPatch(patch)) return null
|
|
125
137
|
return { view: 'npm', packageName: review[1], packageSpec: review[2] ?? null, bundleTab: 'review',
|
|
126
|
-
...(review[3] ? { reviewPackage: versionOnly ? review[1] : review[3], reviewSpec: versionOnly ? review[3] : review[4] ?? null } : {}) }
|
|
138
|
+
...(review[3] ? { reviewPackage: versionOnly ? review[1] : review[3], reviewSpec: versionOnly ? review[3] : review[4] ?? null } : {}), ...(patch ? { patch } : {}) }
|
|
127
139
|
}
|
|
128
140
|
const match = /^\/npm(?:\/((?:@[^/@]+\/)?[^/@]+)(?:@([^/]+))?(?:\/(code|search|compare|treemap)(?:\/([^/]+)(?:\/([a-z]+))?)?)?)?$/u.exec(path)
|
|
129
141
|
if (!match) return undefined
|
|
@@ -256,7 +268,7 @@ export function parseManagedRoute(url) {
|
|
|
256
268
|
if (bundleTab !== 'code' || bundle[5] || !/^[1-9]\d*$/u.test(bundle[4]) || !Number.isSafeInteger(file)) return null
|
|
257
269
|
return { ...route, file, ...codeLines(url.hash) }
|
|
258
270
|
}
|
|
259
|
-
const npm = parseNpmRoute(path, url.hash)
|
|
271
|
+
const npm = parseNpmRoute(path, url.hash, url.searchParams)
|
|
260
272
|
if (npm !== undefined) return npm
|
|
261
273
|
const code = parseRepositoryCodeRoute(url)
|
|
262
274
|
if (code !== undefined) return code
|
|
@@ -3,3 +3,4 @@
|
|
|
3
3
|
export type TeamAppMetadata = { appMode: true; appFindings: number } | { appMode: false; appReports?: number; findings?: number }
|
|
4
4
|
export function managedTeamAppMetadata(workspace: readonly { filename: string; data: unknown }[] & { links?: readonly (readonly string[])[] },
|
|
5
5
|
options?: { countFindings?: boolean }): TeamAppMetadata
|
|
6
|
+
export function managedTeamRevalidationConflicted(workspace: readonly { filename: string; data: unknown }[] & { links?: readonly (readonly string[])[] }): boolean
|
|
@@ -2,6 +2,7 @@ import { inheritReportMeta, isAppFinding, reportEntries } from '@preventive/repo
|
|
|
2
2
|
import { collectDuplicates } from '../../client/linked-findings.js'
|
|
3
3
|
import { hasRevalidateStamp } from '../finding-filters.js'
|
|
4
4
|
import { workspaceAppMetadata } from '../workspace-app.js'
|
|
5
|
+
import { mergeReportGroups } from '../workspace-groups.js'
|
|
5
6
|
|
|
6
7
|
// The same classification as local workspaces, using only the complete,
|
|
7
8
|
// server-filtered published workspace: the reports of one team response and
|
|
@@ -9,6 +10,21 @@ import { workspaceAppMetadata } from '../workspace-app.js'
|
|
|
9
10
|
// catalog, so the sidebar opens a team collapsed to its App findings at once.
|
|
10
11
|
// The server backfills finding IDs before building these envelopes.
|
|
11
12
|
export function managedTeamAppMetadata(workspace, options = {}) {
|
|
13
|
+
const { reports, duplicates } = viewReports(workspace)
|
|
14
|
+
return workspaceAppMetadata(reports, id => [...duplicates.get(id) ?? []], options)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Whether copies of one finding in the workspace disagree on what the
|
|
18
|
+
// revalidation pass said, which takes the App lens off a view of it
|
|
19
|
+
// (ui/view/render.js configureReportRevalidation). Asked of all its rows, so
|
|
20
|
+
// that a view of any of its reports holds none either.
|
|
21
|
+
export function managedTeamRevalidationConflicted(workspace) {
|
|
22
|
+
return mergeReportGroups(viewReports(workspace).reports).conflicts.size > 0
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// The workspace's reports as a view holds them (ui/view/ingest.js), with the
|
|
26
|
+
// duplicates its links name.
|
|
27
|
+
function viewReports(workspace) {
|
|
12
28
|
const duplicates = new Map(), reports = []
|
|
13
29
|
collectDuplicates(workspace.links ?? [], duplicates)
|
|
14
30
|
for (const { filename, data } of workspace) {
|
|
@@ -27,5 +43,5 @@ export function managedTeamAppMetadata(workspace, options = {}) {
|
|
|
27
43
|
}))
|
|
28
44
|
reports.push({ fileName: filename, groups })
|
|
29
45
|
}
|
|
30
|
-
return
|
|
46
|
+
return { reports, duplicates }
|
|
31
47
|
}
|