@preventive/triage 1.0.0-alpha.5 → 1.0.0-alpha.50
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-APACHE +202 -0
- package/README.md +16 -0
- package/api/reap.ts +17 -0
- package/cli.js +6 -0
- package/client/dependency-paths.d.ts +3 -0
- package/client/dependency-paths.js +36 -0
- package/client/finding-link.js +305 -0
- package/client/linked-findings.d.ts +1 -0
- package/client/linked-findings.js +115 -0
- package/common/bundle-commit.d.ts +1 -0
- package/common/bundle-commit.js +4 -0
- package/common/bundle-compare-diff.d.ts +3 -0
- package/common/bundle-compare-diff.js +357 -0
- package/common/bundle-compare-inputs.d.ts +2 -0
- package/common/bundle-compare-inputs.js +65 -0
- package/common/bundle-compare-versions.js +172 -0
- package/common/bundle-comparison.d.ts +8 -0
- package/common/bundle-comparison.js +62 -0
- package/common/bundle-metadata.d.ts +15 -0
- package/common/bundle-metadata.js +226 -0
- package/common/bundle-pkg-of.d.ts +1 -0
- package/common/bundle-pkg-of.js +90 -0
- package/common/bundle-reasons.d.ts +2 -0
- package/common/bundle-reasons.js +21 -0
- package/common/bundle-source-links.d.ts +6 -0
- package/common/bundle-source-links.js +216 -0
- package/common/bundle-sourcemap.d.ts +10 -0
- package/common/bundle-sourcemap.js +100 -0
- package/common/bundle-sources.d.ts +7 -0
- package/common/bundle-sources.js +333 -0
- package/common/bundle-stats.js +41 -0
- package/common/bundle-tabs.js +1 -0
- package/common/code-language.js +65 -0
- package/common/default-scan-models.ts +31 -0
- package/common/dependency-reviews.d.ts +18 -0
- package/common/dependency-reviews.js +64 -0
- package/common/finding-duplicates.js +58 -0
- package/common/finding-filters.js +274 -0
- package/common/finding-id.js +47 -0
- package/common/github-issue-labels.d.ts +1 -0
- package/common/github-issue-labels.js +11 -0
- package/common/github-pr.ts +56 -0
- package/common/gzip.js +59 -0
- package/common/line-diff.d.ts +8 -0
- package/common/line-diff.js +57 -0
- package/common/linked-workspace-groups.js +63 -0
- package/common/managed/agent-tasks.ts +292 -0
- package/common/managed/comments.ts +34 -0
- package/common/managed/finding-detail.d.ts +2 -0
- package/common/managed/finding-detail.js +32 -0
- package/common/managed/github-agents.ts +114 -0
- package/common/managed/github-logins.ts +49 -0
- package/common/managed/npm-packages.d.ts +5 -0
- package/common/managed/npm-packages.js +34 -0
- package/common/managed/permissions.ts +31 -0
- package/common/managed/report-content.ts +72 -0
- package/common/managed/report-filter.ts +109 -0
- package/common/managed/repository-alias.ts +65 -0
- package/common/managed/roles.ts +35 -0
- package/common/managed/routes.d.ts +2 -0
- package/common/managed/routes.js +347 -0
- package/common/managed/scan-models.ts +6 -0
- package/common/managed/team-app.d.ts +5 -0
- package/common/managed/team-app.js +31 -0
- package/common/managed/triage.ts +83 -0
- package/common/managed/upload-seal.ts +126 -0
- package/common/pretty-print.d.ts +5 -0
- package/common/pretty-print.js +24 -0
- package/common/revalidation-conflicts.js +48 -0
- package/common/revalidation-input-groups.js +56 -0
- package/common/save-error-reason.ts +20 -7
- package/common/scan-server.ts +13 -0
- package/common/security-links.d.ts +5 -0
- package/common/security-links.js +33 -0
- package/common/server-info.ts +46 -0
- package/common/utf8.d.ts +3 -0
- package/common/utf8.js +45 -0
- package/common/workspace-app.js +39 -0
- package/common/workspace-groups.js +176 -0
- package/out/activity.js +4 -0
- package/out/brotli-fallback.js +3 -3
- package/out/chunk-2WDZPXVL.js +3 -0
- package/out/chunk-2XUEZHIP.js +28 -0
- package/out/chunk-2ZDZWLFY.js +1 -0
- package/out/chunk-4HI2MSAV.js +17 -0
- package/out/chunk-4XMP7YVH.js +36 -0
- package/out/chunk-6LNQTKAV.js +52 -0
- package/out/chunk-6S2U2UYG.js +232 -0
- package/out/chunk-75YLHBCM.js +3 -0
- package/out/chunk-7BKKYVWR.js +7 -0
- package/out/chunk-7C7DWL7I.js +1 -0
- package/out/chunk-7VAWINQF.js +6 -0
- package/out/chunk-C4Z6ZBXV.js +10 -0
- package/out/chunk-FC65TPUB.js +1 -0
- package/out/chunk-FKBXNUW4.js +1 -0
- package/out/chunk-GDPCLSOS.js +2 -0
- package/out/chunk-GO7FT4OD.js +1 -0
- package/out/chunk-HTBYHSPG.js +1 -0
- package/out/chunk-IYPY47MC.js +1 -0
- package/out/chunk-IZ7POWI3.js +18 -0
- package/out/chunk-JCIIBLKR.js +1 -0
- package/out/chunk-NRPC74AM.js +1 -0
- package/out/chunk-OOJM4CTU.js +1 -0
- package/out/chunk-PBZPYIG7.js +1 -0
- package/out/chunk-PNHBZGDD.js +1 -0
- package/out/chunk-PWFUOLZK.js +17 -0
- package/out/chunk-PXK7EU4V.js +1 -0
- package/out/chunk-QB4CBOX2.js +1 -0
- package/out/chunk-R3NZFXVR.js +1 -0
- package/out/chunk-R5O6FAW4.js +2 -0
- package/out/chunk-SXV2T5C4.js +80 -0
- package/out/chunk-TQJDYHVE.js +2 -0
- package/out/chunk-TUII34K2.js +1 -0
- package/out/chunk-UK7RGJT6.js +3 -0
- package/out/chunk-VFWMQ3LG.js +2 -0
- package/out/chunk-VH4XMZIW.js +2 -0
- package/out/chunk-WCPJDAYA.js +10 -0
- package/out/chunk-WFYLMF6Z.js +10 -0
- package/out/chunk-WYLQWYLA.js +1 -0
- package/out/client-managed-import.js +4 -0
- package/out/client-managed.js +33 -0
- package/out/client-sync.js +11 -17
- package/out/compare.js +35 -0
- package/out/dialogs.js +55 -0
- package/out/github-issue-authorized.html +3 -0
- package/out/github-issue-authorized.js +1 -0
- package/out/graph.js +17 -4
- package/out/index.html +90 -8
- package/out/prism.js +14 -10
- package/out/scan.js +2 -0
- package/out/stasis.svg +20 -0
- package/out/terminal.js +366 -39
- package/out/view.css +1 -1
- package/out/view.js +19 -144
- package/package.json +362 -59
- package/risk/actions/findings.js +161 -0
- package/risk/actions/resolve.js +219 -0
- package/risk/actions/scripts/commands.js +182 -0
- package/risk/actions/scripts/grade.js +113 -0
- package/risk/actions/scripts/packages.js +123 -0
- package/risk/actions/scripts/shell.js +169 -0
- package/risk/actions/uses.js +76 -0
- package/risk/actions/workflow.js +110 -0
- package/risk/files/characters.js +196 -0
- package/risk/files/hosts.js +205 -0
- package/risk/files/readability.js +177 -0
- package/risk/files/runs-code.js +615 -0
- package/risk/files/script.js +118 -0
- package/risk/files/stamps.js +47 -0
- package/risk/index.d.ts +254 -0
- package/risk/index.js +11 -0
- package/risk/npm/assess.js +285 -0
- package/risk/npm/combined.js +68 -0
- package/risk/npm/compare.js +257 -0
- package/risk/npm/dependencies.js +172 -0
- package/risk/npm/diff.js +79 -0
- package/risk/npm/downloads.js +84 -0
- package/risk/npm/files.js +445 -0
- package/risk/npm/held.js +63 -0
- package/risk/npm/install-scripts.js +34 -0
- package/risk/npm/malware.js +36 -0
- package/risk/npm/manifest.js +65 -0
- package/risk/npm/new-dependency.js +60 -0
- package/risk/npm/old-version.js +108 -0
- package/risk/npm/registry.js +388 -0
- package/risk/npm/release.js +85 -0
- package/risk/npm/score.js +59 -0
- package/risk/npm/specs.js +124 -0
- package/risk/npm/tarball.js +96 -0
- package/server-common/database-config.ts +16 -0
- package/server-common/initialize.ts +18 -0
- package/server-common/npm-advisories.ts +101 -0
- package/{server → server-common}/origin.ts +5 -5
- package/server-common/reap.ts +48 -0
- package/server-common/scan-config.ts +19 -0
- package/server-common/standalone.ts +29 -0
- package/server-common/storage-crypto.ts +182 -0
- package/server-common/storage-log.ts +38 -0
- package/server-common/vercel-blob.ts +103 -0
- package/server-e2e/app.ts +487 -0
- package/{server → server-e2e}/bus-receiver.ts +2 -1
- package/{server → server-e2e}/cli.js +9 -4
- package/{server → server-e2e}/config.ts +50 -42
- package/{server → server-e2e}/db-neon.ts +2 -2
- package/{server → server-e2e}/db-revision-sql.ts +1 -1
- package/{server → server-e2e}/db-stmt.ts +2 -2
- package/{server → server-e2e}/db.ts +7 -7
- package/{server → server-e2e}/http.ts +44 -13
- package/{server → server-e2e}/hub.ts +40 -9
- package/server-e2e/index.ts +17 -0
- package/server-e2e/lifecycle.ts +95 -0
- package/{server → server-e2e}/neon-driver.ts +2 -2
- package/{server → server-e2e}/npm-proxy.ts +12 -145
- package/{server → server-e2e}/objstore/blob-vercel.ts +10 -114
- package/{server → server-e2e}/objstore/blob.ts +1 -1
- package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
- package/{server → server-e2e}/objstore/handlers.ts +9 -8
- package/{server → server-e2e}/objstore/init.ts +19 -27
- package/{server → server-e2e}/objstore/reaper.ts +1 -1
- package/server-e2e/objstore/rest-deny.ts +28 -0
- package/server-e2e/objstore/rest-mint.ts +224 -0
- package/{server → server-e2e}/objstore/rest.ts +3 -3
- package/{server → server-e2e}/objstore/store-neon.ts +6 -10
- package/{server → server-e2e}/objstore/store.ts +7 -7
- package/{server → server-e2e}/objstore/tokens.ts +2 -2
- package/{server → server-e2e}/pubsub.ts +12 -9
- package/{server → server-e2e}/sign.ts +4 -4
- package/{server → server-e2e}/sse-server.ts +60 -33
- package/{server → server-e2e}/static.ts +31 -21
- package/{server → server-e2e}/sync-handlers.ts +64 -30
- package/{server → server-e2e}/ws-server.ts +20 -9
- package/server-managed/activity.ts +258 -0
- package/server-managed/agent-merges.ts +109 -0
- package/server-managed/app-agents.ts +21 -0
- package/server-managed/avatar-store.ts +13 -0
- package/server-managed/blob-store.ts +17 -0
- package/server-managed/brotli.ts +10 -0
- package/server-managed/bundle-advisories.ts +120 -0
- package/server-managed/bundle-advisory-inventory.ts +77 -0
- package/server-managed/bundle-build-diagnostics.d.ts +10 -0
- package/server-managed/bundle-build-diagnostics.js +23 -0
- package/server-managed/bundle-build-leases.ts +41 -0
- package/server-managed/bundle-build-worker.js +115 -0
- package/server-managed/bundle-build.ts +200 -0
- package/server-managed/bundle-cache.ts +211 -0
- package/server-managed/bundle-catalog.ts +29 -0
- package/server-managed/bundle-commits.ts +140 -0
- package/server-managed/bundle-store.ts +24 -0
- package/server-managed/bundle-summary-cache.ts +130 -0
- package/server-managed/bundle.ts +119 -0
- package/server-managed/cache-storage.ts +20 -0
- package/server-managed/cli.js +21 -0
- package/server-managed/combined.ts +47 -0
- package/server-managed/comments.ts +151 -0
- package/server-managed/commit-coauthors.ts +106 -0
- package/server-managed/commit-lockfiles.ts +73 -0
- package/server-managed/comparison.ts +41 -0
- package/server-managed/config.ts +164 -0
- package/server-managed/content-access.ts +15 -0
- package/server-managed/crypto.ts +25 -0
- package/server-managed/db-methods.ts +1923 -0
- package/server-managed/db-neon.ts +458 -0
- package/server-managed/db-schema.ts +238 -0
- package/server-managed/db-table-names.ts +22 -0
- package/server-managed/db.ts +142 -0
- package/server-managed/dependabot-access.ts +60 -0
- package/server-managed/dependabot-alerts.ts +185 -0
- package/server-managed/dependency-review-comments.ts +100 -0
- package/server-managed/dependency-reviews.ts +346 -0
- package/server-managed/github-accounts.ts +60 -0
- package/server-managed/github-actions.ts +71 -0
- package/server-managed/github-app.ts +415 -0
- package/server-managed/github-errors.ts +70 -0
- package/server-managed/github-graphql.ts +39 -0
- package/server-managed/github-issue-links.ts +140 -0
- package/server-managed/github-issue-oauth.ts +47 -0
- package/server-managed/github-issues.ts +152 -0
- package/server-managed/github-link-oauth.ts +69 -0
- package/server-managed/github-links.ts +204 -0
- package/server-managed/github-metadata.ts +213 -0
- package/server-managed/github-oauth.ts +238 -0
- package/server-managed/github-pulls.ts +227 -0
- package/server-managed/github-requests.ts +39 -0
- package/server-managed/github-users.ts +30 -0
- package/server-managed/http-response.ts +76 -0
- package/server-managed/http.ts +5329 -0
- package/server-managed/import-triage.ts +51 -0
- package/server-managed/index.ts +192 -0
- package/server-managed/kept-briefly.ts +37 -0
- package/server-managed/kept.ts +28 -0
- package/server-managed/link-reports.ts +162 -0
- package/server-managed/locked-packages.ts +129 -0
- package/server-managed/lockfile-scope.ts +49 -0
- package/server-managed/lru.ts +30 -0
- package/server-managed/managed-issues.ts +78 -0
- package/server-managed/management-catalog.ts +45 -0
- package/server-managed/management.ts +99 -0
- package/server-managed/npm-delta-worker.js +23 -0
- package/server-managed/npm-delta.ts +194 -0
- package/server-managed/npm-insights.ts +449 -0
- package/server-managed/npm-kept.ts +215 -0
- package/server-managed/npm-loads.ts +264 -0
- package/server-managed/npm-manifest.ts +115 -0
- package/server-managed/npm-packages.ts +454 -0
- package/server-managed/npm-provenance.ts +123 -0
- package/server-managed/npm-risk.ts +285 -0
- package/server-managed/object-storage-disk.ts +148 -0
- package/server-managed/object-storage-vercel.ts +67 -0
- package/server-managed/object-storage.ts +75 -0
- package/server-managed/package-entry-points.ts +51 -0
- package/server-managed/pretty-print.ts +434 -0
- package/server-managed/public-workspace.ts +238 -0
- package/server-managed/pull-comment-reviews.ts +161 -0
- package/server-managed/pull-dependencies.ts +348 -0
- package/server-managed/pull-dependency-reviews.ts +328 -0
- package/server-managed/pull-reviews.ts +205 -0
- package/server-managed/queue.ts +39 -0
- package/server-managed/release-commits.ts +84 -0
- package/server-managed/rename-diff-worker.js +26 -0
- package/server-managed/rename-diff.ts +279 -0
- package/server-managed/repo-path.ts +21 -0
- package/server-managed/report-migration.ts +35 -0
- package/server-managed/report-query.ts +10 -0
- package/server-managed/report-response.ts +16 -0
- package/server-managed/report-sources.ts +178 -0
- package/server-managed/report-store.ts +85 -0
- package/server-managed/repository-actions.ts +218 -0
- package/server-managed/repository-agents.ts +355 -0
- package/server-managed/repository-aliases.ts +91 -0
- package/server-managed/repository-browser.ts +294 -0
- package/server-managed/repository-changes.ts +284 -0
- package/server-managed/repository-code.ts +215 -0
- package/server-managed/repository-dependency-reviews.ts +165 -0
- package/server-managed/repository-discovery.ts +82 -0
- package/server-managed/repository-history.ts +432 -0
- package/server-managed/repository-issues.ts +195 -0
- package/server-managed/repository-policy.ts +27 -0
- package/server-managed/repository-pulls.ts +156 -0
- package/server-managed/repository-pulse.ts +460 -0
- package/server-managed/repository-risk-assess.ts +105 -0
- package/server-managed/repository-risk-audit.ts +79 -0
- package/server-managed/repository-risk-read.ts +170 -0
- package/server-managed/repository-risk.ts +491 -0
- package/server-managed/repository-squashes.ts +168 -0
- package/server-managed/request-timing.ts +63 -0
- package/server-managed/reviewed-dependencies.ts +150 -0
- package/server-managed/revisions.ts +35 -0
- package/server-managed/scan-downloads.ts +87 -0
- package/server-managed/session.ts +132 -0
- package/server-managed/slugs.ts +38 -0
- package/server-managed/solidity-entry-points.ts +71 -0
- package/server-managed/sql-postgres.ts +34 -0
- package/server-managed/sql.ts +88 -0
- package/server-managed/static.ts +28 -0
- package/server-managed/storage-command.ts +16 -0
- package/server-managed/storage-db.ts +183 -0
- package/server-managed/storage-encryption.ts +290 -0
- package/server-managed/storage-maintenance.ts +259 -0
- package/server-managed/storage-payload.ts +121 -0
- package/server-managed/storage-stores.ts +91 -0
- package/server-managed/storage.ts +82 -0
- package/server-managed/team-app.ts +134 -0
- package/server-managed/team-catalog.ts +7 -0
- package/server-managed/team-feed.ts +180 -0
- package/server-managed/team-npm-scopes.ts +106 -0
- package/server-managed/team-reports.ts +307 -0
- package/server-managed/triage-response.ts +16 -0
- package/server-managed/type-only-worker.js +19 -0
- package/server-managed/type-only.ts +105 -0
- package/server-managed/uploads.ts +106 -0
- package/server-managed/upstream-cache.ts +243 -0
- package/server-managed/view-as.ts +58 -0
- package/server-managed/workspace-shares.ts +169 -0
- package/server.ts +70 -0
- package/server/index.ts +0 -480
- package/server/lifecycle.ts +0 -208
- /package/{LICENSE → LICENSE-MIT} +0 -0
- /package/{server → server-e2e}/auth.ts +0 -0
- /package/{server → server-e2e}/config.example.json +0 -0
- /package/{server → server-e2e}/objstore/blob-fs.ts +0 -0
- /package/{server → server-e2e}/objstore/fs.ts +0 -0
- /package/{server → server-e2e}/objstore/sign.ts +0 -0
- /package/{server → server-e2e}/peer.ts +0 -0
- /package/{server → server-e2e}/sse-session.ts +0 -0
- /package/{server → server-e2e}/util.ts +0 -0
- /package/{server → server-e2e}/validation.ts +0 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { langForPath } from './code-language.js'
|
|
2
|
+
|
|
3
|
+
const BUNDLE_LANGUAGE_LABELS = Object.freeze({
|
|
4
|
+
javascript: 'JavaScript', jsx: 'JSX', typescript: 'TypeScript', tsx: 'TSX',
|
|
5
|
+
json: 'JSON', css: 'CSS', markup: 'HTML', yaml: 'YAML', bash: 'Shell',
|
|
6
|
+
markdown: 'Markdown', solidity: 'Solidity', php: 'PHP', rust: 'Rust',
|
|
7
|
+
ruby: 'Ruby', java: 'Java', cpp: 'C++', c: 'C', objectivec: 'Objective-C',
|
|
8
|
+
python: 'Python', go: 'Go', kotlin: 'Kotlin', swift: 'Swift', dart: 'Dart',
|
|
9
|
+
sql: 'SQL', lua: 'Lua', csharp: 'C#', scala: 'Scala', vue: 'Vue', svelte: 'Svelte',
|
|
10
|
+
})
|
|
11
|
+
const BUNDLE_EXTENSION_LANGUAGES = Object.freeze({
|
|
12
|
+
py: 'python', pyw: 'python', go: 'go', kt: 'kotlin', kts: 'kotlin', swift: 'swift',
|
|
13
|
+
dart: 'dart', sql: 'sql', lua: 'lua', cs: 'csharp', scala: 'scala', sc: 'scala',
|
|
14
|
+
vue: 'vue', svelte: 'svelte',
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
function bundleLanguageOf(path) {
|
|
18
|
+
const basename = typeof path === 'string' ? path.slice(path.lastIndexOf('/') + 1) : ''
|
|
19
|
+
const dot = basename.lastIndexOf('.')
|
|
20
|
+
const ext = dot > 0 ? basename.slice(dot + 1).trim().toLowerCase() : ''
|
|
21
|
+
const lang = langForPath(path) ?? BUNDLE_EXTENSION_LANGUAGES[ext]
|
|
22
|
+
if (lang) return { key: lang, label: BUNDLE_LANGUAGE_LABELS[lang] ?? lang }
|
|
23
|
+
return ext ? { key: `extension:${ext}`, label: `.${ext}` } : { key: 'other', label: 'Other' }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function bundleCodeStats(lineCounts, fileSizes) {
|
|
27
|
+
const languages = new Map()
|
|
28
|
+
let bytes = 0, lines = 0
|
|
29
|
+
for (const [path, count] of lineCounts) {
|
|
30
|
+
const language = bundleLanguageOf(path)
|
|
31
|
+
const stat = languages.get(language.key) ?? { ...language, files: 0, lines: 0, bytes: 0 }
|
|
32
|
+
stat.files++
|
|
33
|
+
stat.lines += count
|
|
34
|
+
stat.bytes += fileSizes.get(path) ?? 0
|
|
35
|
+
languages.set(language.key, stat)
|
|
36
|
+
lines += count
|
|
37
|
+
bytes += fileSizes.get(path) ?? 0
|
|
38
|
+
}
|
|
39
|
+
return { files: lineCounts.size, lines, bytes, languages: [...languages.values()]
|
|
40
|
+
.toSorted((a, b) => b.lines - a.lines || a.label.localeCompare(b.label)) }
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const BUNDLE_TABS = new Set(['overview', 'graph', 'treemap', 'compare', 'advisories', 'issues', 'code', 'search', 'terminal'])
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const EXT_TO_LANG = {
|
|
2
|
+
__proto__: null,
|
|
3
|
+
js: 'javascript', mjs: 'javascript', cjs: 'javascript',
|
|
4
|
+
jsx: 'jsx',
|
|
5
|
+
ts: 'typescript', mts: 'typescript', cts: 'typescript',
|
|
6
|
+
tsx: 'tsx',
|
|
7
|
+
json: 'json',
|
|
8
|
+
css: 'css',
|
|
9
|
+
html: 'markup', htm: 'markup', xml: 'markup', svg: 'markup',
|
|
10
|
+
yml: 'yaml', yaml: 'yaml',
|
|
11
|
+
sh: 'bash', bash: 'bash',
|
|
12
|
+
md: 'markdown', markdown: 'markdown',
|
|
13
|
+
sol: 'solidity',
|
|
14
|
+
php: 'php', phtml: 'php',
|
|
15
|
+
rs: 'rust',
|
|
16
|
+
rb: 'ruby',
|
|
17
|
+
py: 'python', pyw: 'python', pyi: 'python',
|
|
18
|
+
java: 'java',
|
|
19
|
+
// .h is claimed by C, C++ and Objective-C alike. The C++ grammar
|
|
20
|
+
// extends the C one, so it is the superset that colours a header
|
|
21
|
+
// from any of the three; the C grammar would leave a C++ header's
|
|
22
|
+
// class/template/namespace keywords plain.
|
|
23
|
+
cpp: 'cpp', cc: 'cpp', cxx: 'cpp', 'c++': 'cpp',
|
|
24
|
+
hpp: 'cpp', hh: 'cpp', hxx: 'cpp', 'h++': 'cpp', h: 'cpp',
|
|
25
|
+
c: 'c',
|
|
26
|
+
m: 'objectivec', mm: 'objectivec',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Stasis records loader formats as well as language names. Resolve those to
|
|
30
|
+
// the grammars we ship before consulting the filename.
|
|
31
|
+
const FORMAT_TO_LANG = {
|
|
32
|
+
__proto__: null,
|
|
33
|
+
module: 'javascript', commonjs: 'javascript',
|
|
34
|
+
'module-typescript': 'typescript', 'commonjs-typescript': 'typescript',
|
|
35
|
+
json: 'json', solidity: 'solidity', php: 'php', shell: 'bash', rust: 'rust',
|
|
36
|
+
java: 'java', objc: 'objectivec', objcpp: 'objectivec',
|
|
37
|
+
c: 'c', 'c-header': 'c', cpp: 'cpp', 'cpp-header': 'cpp',
|
|
38
|
+
ruby: 'ruby', podspec: 'ruby', podfile: 'ruby', fastlane: 'ruby',
|
|
39
|
+
'podfile-lock': 'yaml', xml: 'markup',
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Whether a text is a JSON object by its ends: `{` first and `}` last,
|
|
43
|
+
// whitespace aside. Read from both ends, never the whole of a large file.
|
|
44
|
+
function objectText(content) {
|
|
45
|
+
if (typeof content !== 'string') return false
|
|
46
|
+
let end = content.length - 1, start = 0
|
|
47
|
+
while (start < end && /\s/u.test(content[start])) start++
|
|
48
|
+
while (end > start && /\s/u.test(content[end])) end--
|
|
49
|
+
return start < end && content[start] === '{' && content[end] === '}'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// `content`, where the caller has it, settles what the name alone cannot: a
|
|
53
|
+
// `.map` file is a source map, JSON, when its text is an object.
|
|
54
|
+
export function langForPath(path, format, content) {
|
|
55
|
+
if (typeof format === 'string' && FORMAT_TO_LANG[format]) return FORMAT_TO_LANG[format]
|
|
56
|
+
if (typeof path !== 'string') return null
|
|
57
|
+
// Only the filename can contribute an extension. A dotted directory such
|
|
58
|
+
// as `.abc/edf` is a path segment, not an extension on `edf`.
|
|
59
|
+
const basename = path.slice(path.lastIndexOf('/') + 1)
|
|
60
|
+
const dot = basename.lastIndexOf('.')
|
|
61
|
+
if (dot <= 0) return null
|
|
62
|
+
const ext = basename.slice(dot + 1).toLowerCase()
|
|
63
|
+
if (ext === 'map') return objectText(content) ? 'json' : null
|
|
64
|
+
return EXT_TO_LANG[ext] ?? null
|
|
65
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Shared starting catalogue for the UI and managed server until scan-service
|
|
2
|
+
// discovery supplies supported models and effort limits. Use undated aliases.
|
|
3
|
+
export function defaultScanModels(provider: string | null = null) {
|
|
4
|
+
const ids = [
|
|
5
|
+
'anthropic/claude-opus-5.5',
|
|
6
|
+
'anthropic/claude-fable-5.1',
|
|
7
|
+
'anthropic/claude-sonnet-5.5',
|
|
8
|
+
'anthropic/claude-haiku-5.5',
|
|
9
|
+
'openai/gpt-6-astra',
|
|
10
|
+
'openai/gpt-6-astra-pro',
|
|
11
|
+
'openai/gpt-6.1-sol',
|
|
12
|
+
'openai/gpt-6.1-sol-pro',
|
|
13
|
+
'openai/gpt-6-luna',
|
|
14
|
+
'openai/gpt-6-luna-pro',
|
|
15
|
+
'moonshotai/kimi-k3',
|
|
16
|
+
'google/gemini-4-argon',
|
|
17
|
+
'google/gemma-4-31b-it',
|
|
18
|
+
'x-ai/grok-4.7',
|
|
19
|
+
'z-ai/glm-5.3',
|
|
20
|
+
'qwen/qwen3.8-max',
|
|
21
|
+
'deepseek/deepseek-v4.1-flash',
|
|
22
|
+
'deepseek/deepseek-v4-pro',
|
|
23
|
+
'mistralai/mistral-large-4-0',
|
|
24
|
+
]
|
|
25
|
+
// OpenRouter routes all listed providers. Direct-provider credentials only
|
|
26
|
+
// expose that provider's models; no selection leaves the catalogue intact.
|
|
27
|
+
const prefix = provider === 'moonshot' ? 'moonshotai' : provider
|
|
28
|
+
const models = ids.filter(id => !['anthropic', 'openai', 'moonshot'].includes(provider ?? '') || id.startsWith(`${prefix}/`))
|
|
29
|
+
.map(id => ({ id, efforts: ['low', 'medium', 'high', 'xhigh', 'max'] }))
|
|
30
|
+
return { models, defaultModel: models[0]!.id }
|
|
31
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type ReviewVerdict = 'accept' | 'accept-with-nits' | 'request-changes' | 'reject'
|
|
2
|
+
export interface ReviewSummary {
|
|
3
|
+
coverage: 'full' | 'partial' | null
|
|
4
|
+
checkedFiles: number | null
|
|
5
|
+
totalFiles: number | null
|
|
6
|
+
verdictLimit: ReviewVerdict
|
|
7
|
+
}
|
|
8
|
+
export const REVIEW_VERDICTS: Record<ReviewVerdict, string>
|
|
9
|
+
export function isReviewableFile(before: unknown, after: unknown): boolean
|
|
10
|
+
export function summarizeReview(entries: { file: string; status: ReviewVerdict | null }[], files: string[] | null, nonTextFiles?: string[] | null): ReviewSummary
|
|
11
|
+
export function allowedReviewVerdicts(entries: { file: string; status: ReviewVerdict | null }[], limit?: ReviewVerdict): ReviewVerdict[]
|
|
12
|
+
interface ReviewNpmVersion { version: string; private?: boolean; manifest?: Record<string, unknown> }
|
|
13
|
+
export function npmTakenDown(entry: { npm: ReviewNpmVersion }): boolean
|
|
14
|
+
export function npmReviewVerdictLimit(npm: ReviewNpmVersion | null | undefined,
|
|
15
|
+
advisoryList?: { versions: string[]; advisories: { id?: string; title?: string; severity?: string; cwe?: string[]; affected: number[] }[] } | null,
|
|
16
|
+
socket?: { alerts: { type: string }[] } | null, risk?: { findings: { id: string }[] } | null): ReviewVerdict
|
|
17
|
+
export function needsPackageJsonReview(entries: { file: string; status: ReviewVerdict | null }[], files: string[], nonTextFiles?: string[] | null): boolean
|
|
18
|
+
export function parseReviewNotes(comment: string): { text: string; bullet: boolean }[]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { npmMalwareAdvisory } from '../risk/npm/malware.js'
|
|
2
|
+
|
|
3
|
+
export const REVIEW_VERDICTS = {
|
|
4
|
+
accept: 'Accept',
|
|
5
|
+
'accept-with-nits': 'Accept with nits (non-blocking)',
|
|
6
|
+
'request-changes': 'Request changes',
|
|
7
|
+
reject: 'Reject',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Keep non-text files in the diff and its denominator. Verdicts for them are
|
|
11
|
+
// disabled for now, including when either side of a change is non-text.
|
|
12
|
+
export function isReviewableFile(before, after) {
|
|
13
|
+
return [before, after].every(content => content === undefined || typeof content === 'string')
|
|
14
|
+
&& [before, after].some(content => typeof content === 'string')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Publication saves all changed paths and their non-text subset. Null means
|
|
18
|
+
// an older snapshot without type evidence: it cannot certify full coverage.
|
|
19
|
+
export function summarizeReview(entries, files, nonTextFiles = []) {
|
|
20
|
+
const verdicts = Object.keys(REVIEW_VERDICTS)
|
|
21
|
+
const excluded = new Set(nonTextFiles ?? [])
|
|
22
|
+
const checks = entries.filter(entry => nonTextFiles !== null && !excluded.has(entry.file) && verdicts.includes(entry.status))
|
|
23
|
+
const verdictLimit = verdicts.findLast(verdict => checks.some(entry => entry.status === verdict)) ?? 'accept'
|
|
24
|
+
if (files === null) return { coverage: null, checkedFiles: null, totalFiles: null, verdictLimit }
|
|
25
|
+
const checked = new Set(checks.map(entry => entry.file))
|
|
26
|
+
const checkedFiles = files.filter(file => checked.has(file)).length
|
|
27
|
+
return { coverage: checkedFiles === files.length ? 'full' : 'partial', checkedFiles, totalFiles: files.length, verdictLimit }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function allowedReviewVerdicts(entries, limit = 'accept') {
|
|
31
|
+
const verdicts = Object.keys(REVIEW_VERDICTS)
|
|
32
|
+
return verdicts.slice(Math.max(verdicts.indexOf(limit), verdicts.indexOf(summarizeReview(entries, null).verdictLimit)))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// npm's security placeholder, identified by its provenance as well as its name.
|
|
36
|
+
export function npmTakenDown({ npm: { version, manifest = {} } }) {
|
|
37
|
+
const npms = manifest.description === 'security holding package' || manifest.github?.github?.toLowerCase() === 'npm/security-holder'
|
|
38
|
+
return npms && (manifest.publisher === 'npm' || /-security(?:\.\d+)?$/u.test(version))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Only findings affecting the version being reviewed constrain its verdict.
|
|
42
|
+
// Socket's aggregate scores also fall for other reasons; use explicit alerts.
|
|
43
|
+
// Alert identifiers: https://docs.socket.dev/docs/rule-conditions
|
|
44
|
+
export function npmReviewVerdictLimit(npm, advisoryList, socket, risk) {
|
|
45
|
+
if (!npm) return 'accept'
|
|
46
|
+
const index = advisoryList?.versions?.indexOf(npm.version) ?? -1
|
|
47
|
+
const advisories = index < 0 ? [] : advisoryList.advisories.filter(advisory => advisory.affected.includes(index))
|
|
48
|
+
const alerts = npm.private ? [] : socket?.alerts ?? []
|
|
49
|
+
if (risk?.findings?.some(finding => finding.id === 'malware') || npmTakenDown({ npm }) || alerts.some(alert => alert.type === 'malware')
|
|
50
|
+
|| advisories.some(advisory => npmMalwareAdvisory(advisory) || advisory.id?.startsWith('MAL-') || /^Malicious package\b/iu.test(advisory.title ?? ''))) return 'reject'
|
|
51
|
+
if (advisories.some(advisory => advisory.severity === 'critical') || alerts.some(alert => alert.type === 'criticalCVE')) return 'request-changes'
|
|
52
|
+
return 'accept'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function needsPackageJsonReview(entries, files, nonTextFiles = []) {
|
|
56
|
+
return summarizeReview(entries, files, nonTextFiles).coverage === 'partial'
|
|
57
|
+
&& (nonTextFiles === null || nonTextFiles.includes('package.json')
|
|
58
|
+
|| !entries.some(entry => entry.file === 'package.json' && Object.keys(REVIEW_VERDICTS).includes(entry.status)))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function parseReviewNotes(comment) {
|
|
62
|
+
return comment.replaceAll(/\r\n?/gu, '\n').split(/(?:^|\n)[\t ]*\*[\t ]+/u)
|
|
63
|
+
.map((text, index) => ({ text: text.trim(), bullet: index > 0 })).filter(note => note.text)
|
|
64
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { effectiveSeverity, revalidateKindOf } from '@preventive/report'
|
|
2
|
+
import { recordRevalidationCopies } from './revalidation-conflicts.js'
|
|
3
|
+
|
|
4
|
+
// Gap-fill a derived finding only within a mergeable row. Raw report copies
|
|
5
|
+
// remain intact so App and code modes can use different row partitions.
|
|
6
|
+
// Provenance stays with the first copy; corrected severity keeps per-report
|
|
7
|
+
// variants. Missing/null fields are gaps, never disagreements.
|
|
8
|
+
const KEEPS_ITS_OWN = new Set(['correctedSeverity', 'correctedSeverityReason', 'isApp', 'isUpstream', 'isSecurity', 'source'])
|
|
9
|
+
|
|
10
|
+
// Contradictory revalidate* answers disable App mode for this loaded set.
|
|
11
|
+
// Retain the first answer on the derived copy; the conflict dialog exposes all
|
|
12
|
+
// answers instead of presenting that arbitrary choice as the pass's verdict.
|
|
13
|
+
export function mergeDuplicateFields(survivor, dup) {
|
|
14
|
+
if (!survivor || !dup || survivor === dup) return false
|
|
15
|
+
let conflicted = false
|
|
16
|
+
for (const [key, value] of Object.entries(dup)) {
|
|
17
|
+
if (key.startsWith('_') || KEEPS_ITS_OWN.has(key)) continue
|
|
18
|
+
if (value === undefined || value === null) continue
|
|
19
|
+
// The stamp is compared as the app READS it, not as the file
|
|
20
|
+
// wrote it: the reader trims and case-folds, and answers "no
|
|
21
|
+
// stamp" for anything it doesn't recognise (@preventive/report/src/finding.js
|
|
22
|
+
// revalidateKindOf). So `confirmed` and ` Confirmed ` agree, and
|
|
23
|
+
// a value the app can't read is no answer at all — it neither
|
|
24
|
+
// blocks the other copy's real stamp from landing nor takes the
|
|
25
|
+
// layer off a whole workspace for a typo.
|
|
26
|
+
if (key === 'revalidate') {
|
|
27
|
+
const theirs = revalidateKindOf(dup)
|
|
28
|
+
if (!theirs) continue
|
|
29
|
+
const mine = revalidateKindOf(survivor)
|
|
30
|
+
if (!mine) survivor[key] = value
|
|
31
|
+
else if (mine !== theirs) conflicted = true
|
|
32
|
+
continue
|
|
33
|
+
}
|
|
34
|
+
const own = survivor[key]
|
|
35
|
+
if (own === undefined || own === null) { survivor[key] = value; continue }
|
|
36
|
+
// The pass's prose either side of the stamp, compared past the
|
|
37
|
+
// whitespace two writers can differ on for the same words.
|
|
38
|
+
if (key.startsWith('revalidate') && String(own).trim() !== String(value).trim()) conflicted = true
|
|
39
|
+
}
|
|
40
|
+
return conflicted
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// The row merger also retains the evidence behind a conflict. This must
|
|
44
|
+
// run for every duplicate, including agreeing copies and missing-field fills,
|
|
45
|
+
// so the eventual dialog can name the reports that actually supplied a value.
|
|
46
|
+
export function mergeReportDuplicateFields(survivor, dup, reportName, conflicts) {
|
|
47
|
+
if (!survivor || !dup || survivor === dup) return
|
|
48
|
+
if (survivor.correctedSeverity || dup.correctedSeverity) {
|
|
49
|
+
survivor._correctedByReport ??= {
|
|
50
|
+
[survivor._reportName ?? '']: { severity: effectiveSeverity(survivor), reason: survivor.correctedSeverityReason },
|
|
51
|
+
}
|
|
52
|
+
survivor._correctedByReport[reportName] = { severity: effectiveSeverity(dup), reason: dup.correctedSeverityReason }
|
|
53
|
+
}
|
|
54
|
+
const copies = recordRevalidationCopies(survivor, dup, reportName)
|
|
55
|
+
if (mergeDuplicateFields(survivor, dup) || conflicts.has(survivor.id)) {
|
|
56
|
+
conflicts.set(survivor.id, { finding: survivor, copies })
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// What the confidence range, the Confirmed outcome and a workspace's basic
|
|
2
|
+
// App view mean for a set of rows. Pure: the view passes its current lens
|
|
3
|
+
// (filters.js), and the managed server classifies team workspaces with it.
|
|
4
|
+
import { displayedSeverity, revalidateKindOf } from '@preventive/report'
|
|
5
|
+
|
|
6
|
+
// `tabKey` identifies an individual tab (= finding) across the view.
|
|
7
|
+
export function tabKey(f) { return f.id ?? String(f._id) }
|
|
8
|
+
|
|
9
|
+
// Does this row carry a judgement the pass made ABOUT a finding —
|
|
10
|
+
// any stamp but `revalidation`, which names the pass's own row and
|
|
11
|
+
// judges nothing. Raw for the same reason as the two above: it gates
|
|
12
|
+
// the switch, and a gate that stopped seeing the stamps the moment
|
|
13
|
+
// the layer came off would take the way back with it.
|
|
14
|
+
export function hasRevalidateStamp(f) {
|
|
15
|
+
const kind = revalidateKindOf(f)
|
|
16
|
+
return kind !== '' && kind !== 'revalidation'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// What the toolbar dropdown offers, in the order it lists them —
|
|
20
|
+
// answers to "did the pass leave this standing", running from yes to
|
|
21
|
+
// no. Fewer options than the field has values:
|
|
22
|
+
//
|
|
23
|
+
// * the `revalidation` row rides CONFIRMED — it is the pass itself,
|
|
24
|
+
// re-examining a finding it did not knock down, which is the same
|
|
25
|
+
// answer to that question;
|
|
26
|
+
// * `partial` rides it too. A partial confirmation is a yes to
|
|
27
|
+
// "does this still stand" — the pass narrowed the finding rather
|
|
28
|
+
// than knocking it down — and an option of its own would slice
|
|
29
|
+
// the standing findings in two for a distinction the reader wants
|
|
30
|
+
// the STAMP for, not a filter. It keeps its own stamp on the card.
|
|
31
|
+
// * `unknown` gets no option — a pass that couldn't tell hasn't
|
|
32
|
+
// answered it at all, so there is nothing to filter to. Those rows
|
|
33
|
+
// stay visible with no filter on, like every other row.
|
|
34
|
+
export const REVALIDATE_FILTERS = [
|
|
35
|
+
{ value: 'confirmed', label: 'Confirmed', kinds: ['confirmed', 'partial', 'revalidation'] },
|
|
36
|
+
{ value: 'unreachable', label: 'Unreachable', kinds: ['unreachable'] },
|
|
37
|
+
{ value: 'refuted', label: 'Refuted', kinds: ['refuted'] },
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
// The kinds one dropdown value covers, or null when the value names no
|
|
41
|
+
// option — filters.js reads that as "no filter" rather than hiding
|
|
42
|
+
// every finding behind a value it can't interpret.
|
|
43
|
+
export function revalidateFilterKinds(value) {
|
|
44
|
+
return REVALIDATE_FILTERS.find((o) => o.value === value)?.kinds ?? null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// How finely Confirmed is drawn, once it has taken the partial rows
|
|
48
|
+
// in. The toolbar cycles a chip through these inside the Confirmed row
|
|
49
|
+
// (revalidate-filter.js), because "did this survive" and "how
|
|
50
|
+
// completely" are one question asked twice, not two filters — and the
|
|
51
|
+
// second only has an answer once the first is Confirmed.
|
|
52
|
+
export const PARTIAL_MODES = ['', 'exclude', 'only']
|
|
53
|
+
|
|
54
|
+
// The kinds an outcome selection ACTUALLY matches, with that switch
|
|
55
|
+
// applied. All three are the same shape as every other filter here —
|
|
56
|
+
// a list of kinds, matched existentially over the group — so the chip
|
|
57
|
+
// narrows what Confirmed reaches rather than subtracting from it:
|
|
58
|
+
//
|
|
59
|
+
// '' everything the pass left standing: the row that IS the
|
|
60
|
+
// pass, the full confirmations, the partial ones;
|
|
61
|
+
// 'exclude' the full confirmations only. NOT "everything but the
|
|
62
|
+
// partials": a group is shown for carrying a `confirmed`
|
|
63
|
+
// row, not for lacking a `partial` one, so the pass row
|
|
64
|
+
// on its own no longer stands in for a verdict;
|
|
65
|
+
// 'only' the partial ones.
|
|
66
|
+
//
|
|
67
|
+
// It bites only on an option that took the partial rows in —
|
|
68
|
+
// Confirmed — so a mode left set from an earlier selection can't
|
|
69
|
+
// silently narrow Refuted or Unreachable, and callers can pass it
|
|
70
|
+
// through without checking which option is up.
|
|
71
|
+
export function activeRevalidateKinds(value, partialMode) {
|
|
72
|
+
const kinds = revalidateFilterKinds(value)
|
|
73
|
+
if (!kinds?.includes('partial')) return kinds
|
|
74
|
+
if (partialMode === 'only') return ['partial']
|
|
75
|
+
if (partialMode === 'exclude') return ['confirmed']
|
|
76
|
+
return kinds
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// A finding's place on the 0—10 confidence scale, or undefined when it
|
|
80
|
+
// has none. The one reader every confidence question goes through:
|
|
81
|
+
// what the range matches, what the auto-tune counts, and whether the
|
|
82
|
+
// control is offered at all (render.js hasAnyConfidence).
|
|
83
|
+
//
|
|
84
|
+
// Three ways to have a place:
|
|
85
|
+
//
|
|
86
|
+
// * a `confidence` the analyzer scored it with;
|
|
87
|
+
// * `critical: true` — the boolean, NOT `severity: 'critical'` —
|
|
88
|
+
// which stands in for a top score;
|
|
89
|
+
// * coming from another producer at all. An import carries no
|
|
90
|
+
// confidence because its producer doesn't emit one (Claude
|
|
91
|
+
// Security's parser reads none), not because anyone was unsure —
|
|
92
|
+
// there is no doubt here for a floor to act on. Reading it as 10
|
|
93
|
+
// keeps it on screen under any floor and out only under a cap
|
|
94
|
+
// that excludes the top: `8—10` and `2—10` show it, `0—5` and
|
|
95
|
+
// `7—9` don't. It also stops one imported report from taking the
|
|
96
|
+
// range away from a workspace: an unscored finding DISABLES the
|
|
97
|
+
// control for everyone (render.js), which left the range filtering
|
|
98
|
+
// nothing and every low-confidence row on screen.
|
|
99
|
+
//
|
|
100
|
+
// A row the pass knocked down reads as 0 instead, where that matters
|
|
101
|
+
// (voidsConfidence) — applied by the callers, since whether a finding
|
|
102
|
+
// has a place at all is about the finding, not about the pass.
|
|
103
|
+
export function confidenceOnScale(f) {
|
|
104
|
+
if (f.confidence !== undefined) return f.confidence
|
|
105
|
+
if (f.critical === true || f._source) return 10
|
|
106
|
+
return undefined
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Does this finding put itself on the scale, rather than ride the
|
|
110
|
+
// stand-in confidenceOnScale hands a row that carries no score? A
|
|
111
|
+
// number its producer wrote, or the `critical: true` that stands in
|
|
112
|
+
// for one.
|
|
113
|
+
const scoresItself = (f) => f.confidence !== undefined || f.critical === true
|
|
114
|
+
|
|
115
|
+
// Is the confidence range a live control over these rows — offered,
|
|
116
|
+
// and actually filtering? Two conditions, which the toolbar reads as
|
|
117
|
+
// one (render.js hasAnyConfidence):
|
|
118
|
+
//
|
|
119
|
+
// * every row has a place on the scale, or the first lift off 0
|
|
120
|
+
// would silently drop the ones that don't. A single analyzer
|
|
121
|
+
// finding with no confidence and no `critical: true` disables the
|
|
122
|
+
// control for the whole set;
|
|
123
|
+
// * something on screen puts ITSELF on that scale. A row riding the
|
|
124
|
+
// stand-in doesn't: a set of nothing but unscored imports is all
|
|
125
|
+
// 10s by definition, and a range over one value says nothing.
|
|
126
|
+
// There the control isn't disabled, it isn't offered at all — the
|
|
127
|
+
// toolbar drops the whole block when the outcome dropdown beside
|
|
128
|
+
// it has nothing to offer either.
|
|
129
|
+
//
|
|
130
|
+
// What answers this is the SCORE, not who wrote it. Asked as "not
|
|
131
|
+
// an import" it gave the same answer for every producer that emits
|
|
132
|
+
// no confidence — and the wrong one for DeepSec, which rates every
|
|
133
|
+
// finding it reports and whose words this app places on the scale
|
|
134
|
+
// itself (@preventive/report/src/parse-deepsec.js). A workspace of nothing but
|
|
135
|
+
// DeepSec reports is a real range over real numbers, and asking
|
|
136
|
+
// for a non-import took the slider, the confidence sort and the
|
|
137
|
+
// opening floor away from exactly the load whose producer had
|
|
138
|
+
// scored every row in it.
|
|
139
|
+
//
|
|
140
|
+
// Both halves are asked of the rows ON SCREEN by every caller, since
|
|
141
|
+
// this is about a control in front of a reader.
|
|
142
|
+
export function rangeApplies(groups) {
|
|
143
|
+
return groups.every((g) => g.every((f) => confidenceOnScale(f) !== undefined))
|
|
144
|
+
&& groups.some((g) => g.some(scoresItself))
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Does the confidence floor leave this group on screen? The rule
|
|
148
|
+
// matchesFilters applies (filters.js), with the upper bound at 10 so only the
|
|
149
|
+
// floor bites, hoisted to the group because that is the unit the list
|
|
150
|
+
// shows: a group is on screen when ANY of its rows clears the floor —
|
|
151
|
+
// an unscored row only at floor 0 unless it is flagged `critical`, and
|
|
152
|
+
// a row the pass knocked down reading as 0 whatever number it carries.
|
|
153
|
+
function showsAtConfidence(g, min, kindOf) {
|
|
154
|
+
return g.some((f) => {
|
|
155
|
+
const conf = ['refuted', 'unreachable'].includes(kindOf(f)) ? 0 : confidenceOnScale(f)
|
|
156
|
+
return conf === undefined ? min === 0 : conf >= min
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// The confidence floor a freshly-loaded set should OPEN on, tuned so
|
|
161
|
+
// the initial view fits ~25 groups. Step up 6 → 7 → 8 until the
|
|
162
|
+
// visible count is within budget; cap at 8 (the old static default).
|
|
163
|
+
// Nothing carrying a confidence at all means no floor — without that
|
|
164
|
+
// guard countAtMin(6) = 0 ≤ 25 lands it at 6, which then excludes
|
|
165
|
+
// every finding; 0 lets the filter no-op instead, and the toolbar
|
|
166
|
+
// hides the control anyway (see toolbarHtml in render.js).
|
|
167
|
+
//
|
|
168
|
+
// After picking the base, walk DOWN while each lower step surfaces no
|
|
169
|
+
// new groups — i.e. there's a "gap" in the confidence distribution
|
|
170
|
+
// below the chosen floor. Lowering for free puts the slider at the
|
|
171
|
+
// natural break: e.g. picked 8, nothing at 7 or 6 but some at 5 →
|
|
172
|
+
// settle at 6 (the lowest step revealing nothing new). Down to 0.
|
|
173
|
+
//
|
|
174
|
+
// Pure in its argument, and paired with defaultRevalidateFilter below:
|
|
175
|
+
// together they are "what does this set open on", asked by ingest.js
|
|
176
|
+
// on a first load and again by the App switch (events.js), which
|
|
177
|
+
// reshapes the set and so has to ask again rather than keep an answer
|
|
178
|
+
// that was about a different one.
|
|
179
|
+
export function defaultConfidenceFloor(groups, tabs) {
|
|
180
|
+
// App mode folds the source tabs a revalidation pass already answered
|
|
181
|
+
// under its App tab. Those hidden copies must not lower the opening floor
|
|
182
|
+
// for a workspace that otherwise has the same visible rows as its App
|
|
183
|
+
// report. Keep source-only rows intact; they are real rows in the App view.
|
|
184
|
+
const visible = groups.map(tabs).filter((g) => g.length > 0)
|
|
185
|
+
if (!visible.some((g) => g.some((f) => confidenceOnScale(f) !== undefined))) return 0
|
|
186
|
+
const countAtMin = (min) => visible.reduce((n, g) =>
|
|
187
|
+
n + (g.some((f) => (confidenceOnScale(f) ?? -1) >= min) ? 1 : 0), 0)
|
|
188
|
+
let base
|
|
189
|
+
if (countAtMin(6) <= 25) base = 6
|
|
190
|
+
else if (countAtMin(7) <= 25) base = 7
|
|
191
|
+
else base = 8
|
|
192
|
+
while (base > 0 && countAtMin(base - 1) === countAtMin(base)) base--
|
|
193
|
+
return base
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// The floor the default view will REALLY apply. The range is a
|
|
197
|
+
// whole-set control: one finding on screen with no confidence and no
|
|
198
|
+
// `critical: true` and render.js disables it and resets the bounds to
|
|
199
|
+
// 0—10 (hasAnyConfidence there), so the auto-tuned floor never bites
|
|
200
|
+
// and every row is on screen — an unscored finding is not hidden by a
|
|
201
|
+
// filter that isn't running. Measuring the comparison below against a
|
|
202
|
+
// floor the view is about to throw away would be measuring a screen
|
|
203
|
+
// nobody sees.
|
|
204
|
+
//
|
|
205
|
+
// `critical: true` rides the 10 bucket in place of a score
|
|
206
|
+
// (matchesFilters), so it doesn't block the range any more than a
|
|
207
|
+
// number does.
|
|
208
|
+
//
|
|
209
|
+
// render.js asks this of the on-screen bucket and we ask it of the
|
|
210
|
+
// whole set; at open time, before anything is triaged away, they are
|
|
211
|
+
// the same groups.
|
|
212
|
+
function effectiveFloor(groups, confMin) {
|
|
213
|
+
return rangeApplies(groups) ? confMin : 0
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Both the opening default and the dropdown lock use the same coverage:
|
|
217
|
+
// findings represented by Confirmed rows, plus findings explicitly ruled out.
|
|
218
|
+
// Read the original groups before `tabs` folds away their source members.
|
|
219
|
+
export function confirmedCoverage(groups, tabs, kindOf) {
|
|
220
|
+
// A workspace may carry an un-stamped source copy from report A while
|
|
221
|
+
// report B explicitly ruled out the same id. The ruled-out copy is
|
|
222
|
+
// removed before workspace rows are merged, so it cannot vouch for its
|
|
223
|
+
// source copy during the coverage check below. Remember those ids before
|
|
224
|
+
// that projection disappears: Confirmed may hide the source copy too,
|
|
225
|
+
// even when the App finding that caused the ruling has no
|
|
226
|
+
// `revalidateInputs` list of its own.
|
|
227
|
+
const covered = new Set(groups.ruledOutIds ?? [])
|
|
228
|
+
for (const raw of groups) {
|
|
229
|
+
for (const f of raw) if (['refuted', 'unreachable'].includes(kindOf(f))) covered.add(tabKey(f))
|
|
230
|
+
}
|
|
231
|
+
for (const raw of groups) {
|
|
232
|
+
const g = tabs(raw)
|
|
233
|
+
if (g.length === 0) continue
|
|
234
|
+
if (!g.some((f) => activeRevalidateKinds('confirmed', '').includes(
|
|
235
|
+
kindOf(f) || (f._source && !f._sourcePass ? 'revalidation' : '')))) continue
|
|
236
|
+
// A folded source copy is not a cost of Confirmed, but its id is still
|
|
237
|
+
// represented by the App row. Keep all raw members in the coverage set
|
|
238
|
+
// so a duplicate source-only row from another report is recognized as
|
|
239
|
+
// the same issue rather than making Confirmed look lossy.
|
|
240
|
+
for (const f of raw) {
|
|
241
|
+
covered.add(tabKey(f))
|
|
242
|
+
// App reports can carry the source findings they represent only as
|
|
243
|
+
// `revalidateInputs`, rather than as members of the same physical row.
|
|
244
|
+
// Those inputs are still present in the App row's visible result, so a
|
|
245
|
+
// duplicate source-only row from another report must not keep the
|
|
246
|
+
// workspace on Confidence.
|
|
247
|
+
if (f.isApp && Array.isArray(f.revalidateInputs)) {
|
|
248
|
+
for (const id of f.revalidateInputs) if (id) covered.add(id)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return covered
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function confirmedIsDefault(visible, confMin, covered, kindOf) {
|
|
256
|
+
const floor = effectiveFloor(visible, confMin)
|
|
257
|
+
const shown = visible.filter((g) => showsAtConfidence(g, floor, kindOf))
|
|
258
|
+
if (shown.length === 0) return false
|
|
259
|
+
const kinds = new Set(activeRevalidateKinds('confirmed', ''))
|
|
260
|
+
if (!visible.some((g) => g.some((f) => kinds.has(kindOf(f))))) return false
|
|
261
|
+
return shown.every((g) => g.every((f) => covered.has(tabKey(f))))
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// The same coverage test evaluates a workspace's basic App view without the
|
|
265
|
+
// reader's current lens: the view and the managed server pass the lens to use.
|
|
266
|
+
export function canLockConfirmed(groups, { tabs, kindOf, severityMode }) {
|
|
267
|
+
const visible = groups.map(tabs).filter((g) => g.length > 0)
|
|
268
|
+
const covered = confirmedCoverage(groups, tabs, kindOf)
|
|
269
|
+
if (!confirmedIsDefault(visible, defaultConfidenceFloor(groups, tabs), covered, kindOf)) return false
|
|
270
|
+
const floor = effectiveFloor(visible, 6)
|
|
271
|
+
return visible.every((g) => !showsAtConfidence(g, floor, kindOf)
|
|
272
|
+
|| !g.some((f) => displayedSeverity(f, severityMode) !== 'low')
|
|
273
|
+
|| g.every((f) => covered.has(tabKey(f))))
|
|
274
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Session-local finding ids (see `client/triage.js`'s SESSION_ID_RE:
|
|
2
|
+
// purely numeric `_id` fallbacks, handed out by an in-memory counter
|
|
3
|
+
// for findings whose id couldn't be derived) are re-assigned on every
|
|
4
|
+
// load, so a link built on one would point at an arbitrary other
|
|
5
|
+
// finding after a reload — including in the SENDER's own tab. Those are
|
|
6
|
+
// refused up front rather than silently mis-resolving. Everything else
|
|
7
|
+
// the app treats as persistent — the analyzer's uuids, the deterministic
|
|
8
|
+
// uuids `@preventive/report/src/finding-id.js` derives, and the codex importer's
|
|
9
|
+
// finding-URL ids — is linkable, which is why this is a
|
|
10
|
+
// "not session-local" test rather than a uuid-shape test.
|
|
11
|
+
const SESSION_ID_RE = /^\d+$/u
|
|
12
|
+
|
|
13
|
+
// Control characters can't appear in a uuid, a URL, or an OPFS filename
|
|
14
|
+
// the app will hand out, so their presence means a mangled / hand-crafted
|
|
15
|
+
// fragment. Rejecting them keeps a stray `\n` out of an `alert()` and out
|
|
16
|
+
// of the `[data-gid]` selector the reveal path builds. A scan rather than
|
|
17
|
+
// a character-class regex: matching control characters in a literal is a
|
|
18
|
+
// lint error (`no-control-regex`), and spelling the range out in code is
|
|
19
|
+
// clearer than the escaped equivalent anyway.
|
|
20
|
+
function hasControlChar(value) {
|
|
21
|
+
for (let i = 0; i < value.length; i++) {
|
|
22
|
+
const code = value.codePointAt(i)
|
|
23
|
+
if (code < 0x20 || code === 0x7f) return true
|
|
24
|
+
}
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Per-component cap. The longest legitimate value is a codex finding-URL
|
|
29
|
+
// id; report names and workspace ids no longer travel in the fragment at
|
|
30
|
+
// all. The cap exists so a hostile fragment can't push a megabyte of
|
|
31
|
+
// text through `decodeURIComponent` + the "couldn't find it" alert.
|
|
32
|
+
export const MAX_FINDING_ID_LENGTH = 512
|
|
33
|
+
|
|
34
|
+
function isUsablePart(value) {
|
|
35
|
+
return typeof value === 'string'
|
|
36
|
+
&& value.length > 0
|
|
37
|
+
&& value.length <= MAX_FINDING_ID_LENGTH
|
|
38
|
+
&& !hasControlChar(value)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Whether a finding id survives a reload, i.e. whether a link built on
|
|
42
|
+
// it means anything tomorrow. Callers use this to decide whether to
|
|
43
|
+
// OFFER a link at all (the per-finding Link button hides itself for a
|
|
44
|
+
// session-local id) rather than handing out one that silently rots.
|
|
45
|
+
export function isLinkableFindingId(id) {
|
|
46
|
+
return isUsablePart(id) && !SESSION_ID_RE.test(id)
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function newIssueLabels(isSecurity?: boolean, configured?: string): string[]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Shared by GitHub's prefilled form and managed API issue creation.
|
|
2
|
+
export function newIssueLabels(isSecurity = false, configured = '') {
|
|
3
|
+
const seen = new Set()
|
|
4
|
+
return ['deepview', ...(isSecurity ? ['security'] : []), ...configured.split(',')]
|
|
5
|
+
.map(label => label.trim()).filter(label => {
|
|
6
|
+
const key = label.toLowerCase()
|
|
7
|
+
if (!label || seen.has(key)) return false
|
|
8
|
+
seen.add(key)
|
|
9
|
+
return true
|
|
10
|
+
})
|
|
11
|
+
}
|