@preventive/triage 1.0.0-alpha.5 → 1.0.0-alpha.51
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 +3 -0
- package/common/managed/finding-detail.js +66 -0
- package/common/managed/github-agents.ts +114 -0
- package/common/managed/github-logins.ts +49 -0
- package/common/managed/npm-packages.d.ts +7 -0
- package/common/managed/npm-packages.js +51 -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 +359 -0
- package/common/managed/scan-models.ts +6 -0
- package/common/managed/team-app.d.ts +6 -0
- package/common/managed/team-app.js +47 -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-2K6UN3PC.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-6LNQTKAV.js +52 -0
- package/out/chunk-7BKKYVWR.js +7 -0
- package/out/chunk-7C7DWL7I.js +1 -0
- package/out/chunk-7JBSSRD7.js +3 -0
- package/out/chunk-7VAWINQF.js +6 -0
- package/out/chunk-C4Z6ZBXV.js +10 -0
- package/out/chunk-FMXWXDR4.js +36 -0
- package/out/chunk-GDPCLSOS.js +2 -0
- package/out/chunk-GO7FT4OD.js +1 -0
- package/out/chunk-HT2SSA3V.js +1 -0
- package/out/chunk-IPAH4LKF.js +232 -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-KOGNQDSJ.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-VDRXHIHN.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-WRAJVD35.js +1 -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 +364 -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 +23 -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 +76 -0
- package/server-managed/comparison.ts +52 -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 +468 -0
- package/server-managed/db-schema.ts +238 -0
- package/server-managed/db-table-names.ts +22 -0
- package/server-managed/db.ts +143 -0
- package/server-managed/dependabot-access.ts +60 -0
- package/server-managed/dependabot-alerts.ts +185 -0
- package/server-managed/dependency-review-comments.ts +98 -0
- package/server-managed/dependency-reviews.ts +364 -0
- package/server-managed/github-accounts.ts +60 -0
- package/server-managed/github-actions.ts +71 -0
- package/server-managed/github-app.ts +376 -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 +136 -0
- package/server-managed/github-users.ts +30 -0
- package/server-managed/http-response.ts +76 -0
- package/server-managed/http.ts +5503 -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/listed-activity.ts +39 -0
- package/server-managed/locked-packages.ts +152 -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 +225 -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-patch.ts +325 -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 +351 -0
- package/server-managed/pull-dependency-reviews.ts +334 -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 +448 -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 +166 -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 +212 -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 +149 -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 +319 -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,114 @@
|
|
|
1
|
+
// Agents by their own GitHub accounts, where a repository's activity names
|
|
2
|
+
// them: each its name, and how the app draws it, by its mark, as the Agents
|
|
3
|
+
// tab does (ui/view/provider-icons.js), or by its avatar, which
|
|
4
|
+
// /api/avatar/<avatar> serves; its accounts, each by the logins GitHub's REST and GraphQL APIs
|
|
5
|
+
// name it by and its numeric id where known, which GitHub never gives
|
|
6
|
+
// another; and the names its commits carry where GitHub names no account for
|
|
7
|
+
// them. Claude Code's commits are authored as Claude <noreply@anthropic.com>,
|
|
8
|
+
// which GitHub names `claude`; the Claude GitHub App's as `claude[bot]`.
|
|
9
|
+
// Codex's GitHub connector's are its own; Codex otherwise commits as whoever
|
|
10
|
+
// ran it, and those commits are theirs, with a
|
|
11
|
+
// `Co-authored-by: Codex <codex@openai.com>` trailer, an address GitHub
|
|
12
|
+
// names someone else's account for, so Codex is known by the address, not
|
|
13
|
+
// that account. Copilot's coding agent authors its commits as its account,
|
|
14
|
+
// Copilot; Copilot's code review reviews as another, its bot, which GitHub
|
|
15
|
+
// names Copilot as well where it is asked to review and in its threads, and
|
|
16
|
+
// `copilot-pull-request-reviewer` (`[bot]` to GitHub's REST API) on its
|
|
17
|
+
// reviews, the accounts' ids telling the two apart: a login may so be more
|
|
18
|
+
// than one account's. Dependabot, its GitHub App, as its bot,
|
|
19
|
+
// `dependabot` to GitHub's GraphQL API, `dependabot[bot]` to its REST API and
|
|
20
|
+
// in its address. GitHub's Agentic Workflows (gh-aw) post, and approve or
|
|
21
|
+
// review pull requests, as GitHub Actions' bot, `github-actions` (41898282,
|
|
22
|
+
// `[bot]` to the REST API), its mark the GitHub Actions App's avatar; other
|
|
23
|
+
// workflows the bot runs are as much its own, and the commits workflows make
|
|
24
|
+
// as the names they are commonly given, `github-actions` and `GitHub Action`,
|
|
25
|
+
// GitHub naming no account for their addresses. Neo, its GitHub App (3115942),
|
|
26
|
+
// as its account (274065190) and its App's bot (268952163), each known by
|
|
27
|
+
// its id alone, whatever GitHub names it, and drawn by its App's avatar,
|
|
28
|
+
// wherever either or its fingerprint names it. Cursor's agents commit as whoever ran them, with a
|
|
29
|
+
// `Co-authored-by: Cursor Agent <cursoragent@cursor.com>` trailer; the app
|
|
30
|
+
// has no mark of Cursor's, and draws it by its name's letter. Each by the
|
|
31
|
+
// addresses (`emails`) its commits name it by, as author or co-author.
|
|
32
|
+
//
|
|
33
|
+
// Those the Agents tab tells beside Claude and Codex (whose sessions and
|
|
34
|
+
// tasks it tells them by, common/managed/agent-tasks.ts), the server's app
|
|
35
|
+
// agents (server-managed/app-agents.ts APP_AGENTS), by their branches'
|
|
36
|
+
// prefix (`prefix`), the accounts their work is authored by, and the
|
|
37
|
+
// fingerprint (`fingerprint`) they leave as a line of their pull
|
|
38
|
+
// requests' bodies and their commits' messages, where they leave one:
|
|
39
|
+
// Neo's, on `neo/` branches; Copilot's coding agent's, on `copilot/`;
|
|
40
|
+
// Dependabot's, on `dependabot/<ecosystem>/…`. Their work may carry
|
|
41
|
+
// Claude's or Codex's marks too, those its tools.
|
|
42
|
+
export interface GithubAgentAccount { id?: number; logins: readonly string[] }
|
|
43
|
+
export interface GithubAgent { name: string; mark?: 'anthropic' | 'openai' | 'github-copilot'; avatar?: string; accounts: readonly GithubAgentAccount[]; names: readonly string[]
|
|
44
|
+
emails?: readonly string[]; prefix?: string; fingerprint?: string }
|
|
45
|
+
|
|
46
|
+
export const GITHUB_AGENTS = {
|
|
47
|
+
claude: { name: 'Claude', mark: 'anthropic', accounts: [{ id: 81847, logins: ['claude'] }, { logins: ['claude[bot]'] }], names: ['Claude'], emails: ['noreply@anthropic.com'] },
|
|
48
|
+
codex: { name: 'Codex', mark: 'openai', accounts: [{ id: 199175422, logins: ['chatgpt-codex-connector', 'chatgpt-codex-connector[bot]'] }], names: ['Codex'],
|
|
49
|
+
emails: ['codex@openai.com'] },
|
|
50
|
+
neo: { name: 'Neo', avatar: 'app-3115942', accounts: [{ id: 274065190, logins: [] }, { id: 268952163, logins: [] }], names: [], prefix: 'neo/',
|
|
51
|
+
fingerprint: '🤖 Generated with [Neo](' },
|
|
52
|
+
copilot: { name: 'Copilot', mark: 'github-copilot', accounts: [{ id: 198982749, logins: ['Copilot', 'copilot-swe-agent'] },
|
|
53
|
+
{ id: 175728472, logins: ['Copilot', 'copilot-pull-request-reviewer', 'copilot-pull-request-reviewer[bot]'] }], names: ['Copilot'], prefix: 'copilot/' },
|
|
54
|
+
dependabot: { name: 'Dependabot', avatar: 'app-29110', accounts: [{ id: 49699333, logins: ['dependabot', 'dependabot[bot]'] }], names: ['dependabot[bot]'],
|
|
55
|
+
prefix: 'dependabot/' },
|
|
56
|
+
actions: { name: 'GitHub Actions', avatar: 'app-15368', accounts: [{ id: 41898282, logins: ['github-actions', 'github-actions[bot]'] }],
|
|
57
|
+
names: ['github-actions[bot]', 'github-actions', 'GitHub Action'] },
|
|
58
|
+
cursor: { name: 'Cursor', accounts: [], names: ['Cursor Agent'], emails: ['cursoragent@cursor.com'] },
|
|
59
|
+
} as const satisfies Record<string, GithubAgent>
|
|
60
|
+
export type GithubAgentId = keyof typeof GITHUB_AGENTS
|
|
61
|
+
|
|
62
|
+
const agents = Object.entries(GITHUB_AGENTS) as [GithubAgentId, GithubAgent][]
|
|
63
|
+
// Each login's agent, and the ids of the accounts it names, undefined for
|
|
64
|
+
// one whose id the app doesn't know.
|
|
65
|
+
const byLogin = new Map<string, { agent: GithubAgentId; ids: (number | undefined)[] }>()
|
|
66
|
+
for (const [agent, { accounts }] of agents) {
|
|
67
|
+
for (const { id, logins } of accounts) {
|
|
68
|
+
for (const login of logins) byLogin.set(login.toLowerCase(), { agent, ids: [...byLogin.get(login.toLowerCase())?.ids ?? [], id] })
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// The accounts known by their ids alone, no login of theirs: each its agent.
|
|
72
|
+
const byId = new Map(agents.flatMap(([agent, { accounts }]) => accounts.flatMap(({ id, logins }) => id !== undefined && logins.length === 0 ? [[id, agent] as const] : [])))
|
|
73
|
+
const byName = new Map(agents.flatMap(([agent, { names }]) => names.map(name => [name.toLowerCase(), agent] as const)))
|
|
74
|
+
const byEmail = new Map(agents.flatMap(([agent, { emails = [] }]) => emails.map(email => [email, agent] as const)))
|
|
75
|
+
|
|
76
|
+
// The agent whose address `email` is, GitHub matching addresses regardless of
|
|
77
|
+
// case; null for anyone else's.
|
|
78
|
+
export const emailAgent = (email: unknown): GithubAgentId | null => typeof email === 'string' ? byEmail.get(email.trim().toLowerCase()) ?? null : null
|
|
79
|
+
|
|
80
|
+
// The agent an author is: by their login, GitHub matching logins regardless
|
|
81
|
+
// of case, their account's id the agent's where both are known, so another
|
|
82
|
+
// account GitHub names by the same login (a user's, the bot's GraphQL login
|
|
83
|
+
// may be) is not; an account known by its id alone, by that, whatever its
|
|
84
|
+
// login; for an author GitHub names no account for, by the name they carry.
|
|
85
|
+
// Null for anyone else: a human's account is theirs, whatever their commits
|
|
86
|
+
// name them.
|
|
87
|
+
export function githubAgent(login: string | null, account: number | null = null, name: string | null = null): GithubAgentId | null {
|
|
88
|
+
if (typeof login !== 'string') return typeof name === 'string' ? byName.get(name.trim().toLowerCase()) ?? null : null
|
|
89
|
+
const known = byLogin.get(login.toLowerCase())
|
|
90
|
+
if (known && (account === null || known.ids.includes(undefined) || known.ids.includes(account))) return known.agent
|
|
91
|
+
return account === null ? null : byId.get(account) ?? null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Whether the accounts `ids` a login is named by are all one agent's, as
|
|
95
|
+
// Copilot's coding agent's and its code review's, both named Copilot, are:
|
|
96
|
+
// the login is that agent's whichever it is, rather than no one's.
|
|
97
|
+
export function oneAgent(login: string, ids: Iterable<number>): boolean {
|
|
98
|
+
const theirs = new Set([...ids].map(id => githubAgent(login, id)))
|
|
99
|
+
return theirs.size === 1 && !theirs.has(null)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// The accounts agents work as, a person's to GitHub, as the app is told of
|
|
103
|
+
// them, each by its id and logins: the pull requests they open and the
|
|
104
|
+
// commits they push to a branch directly are agents' (the Pulse's,
|
|
105
|
+
// server-managed/repository-pulls.ts readPulls, repository-pulse.ts
|
|
106
|
+
// commitStats), whatever their branches and labels, the rest of what they do
|
|
107
|
+
// theirs. Christopher Turing's.
|
|
108
|
+
export const AGENT_OPERATED = [{ id: 188797125, logins: ['ChristopherTuring'] }] as const satisfies readonly GithubAgentAccount[]
|
|
109
|
+
|
|
110
|
+
// Whether an author is an account agents work as (AGENT_OPERATED): by its id
|
|
111
|
+
// where GitHub gives it, whatever it is renamed to, else by its login,
|
|
112
|
+
// regardless of case.
|
|
113
|
+
export const agentOperated = (login: string | null, id: number | null = null): boolean => AGENT_OPERATED.some(account => id === null
|
|
114
|
+
? typeof login === 'string' && account.logins.some(name => name.toLowerCase() === login.toLowerCase()) : account.id === id)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// The GitHub accounts an answer about a repository's activity names (its
|
|
2
|
+
// commits', issues', pull requests', agents' and pulse's): as an author, one
|
|
3
|
+
// who merged or a contributor, by login, and by the account's numeric id
|
|
4
|
+
// where the answer names it beside the login. The server resolves them to the
|
|
5
|
+
// app's users and agents (server-managed/github-users.ts), and the client
|
|
6
|
+
// keeps those it answered with in place of what it held for them
|
|
7
|
+
// (ui/view/github-users.js).
|
|
8
|
+
import { oneAgent } from './github-agents.ts'
|
|
9
|
+
|
|
10
|
+
// Where answers name a login, each with where the account's id is named
|
|
11
|
+
// beside it.
|
|
12
|
+
const LOGIN_KEYS = new Map([['author', 'authorId'], ['authorLogin', 'authorId'], ['mergedBy', 'mergedById'], ['login', 'id']])
|
|
13
|
+
const LOGIN = /^[A-Za-z\d][A-Za-z\d-]{0,38}(?:\[bot\])?$/u
|
|
14
|
+
const MAX_DEPTH = 8
|
|
15
|
+
const accountId = (value: unknown) => Number.isSafeInteger(value) && (value as number) > 0 ? value as number : null
|
|
16
|
+
|
|
17
|
+
// The logins `answer` names, each once, with the numeric id of the account
|
|
18
|
+
// named beside them (null where none is, or where those named are all one
|
|
19
|
+
// agent's, oneAgent); `ambiguous` where a login, GitHub matching logins
|
|
20
|
+
// regardless of case, is named beside more than one: by an account GitHub
|
|
21
|
+
// named by it once, as what was kept of the history says, and one it names by
|
|
22
|
+
// it now, which is no telling whose each is.
|
|
23
|
+
export function namedAccounts(answer: unknown): Map<string, number | 'ambiguous' | null> {
|
|
24
|
+
const named = new Map<string, Set<number>>()
|
|
25
|
+
collect(answer, named, 0)
|
|
26
|
+
const ids = new Map<string, Set<number>>()
|
|
27
|
+
for (const [login, theirs] of named) ids.set(login.toLowerCase(), new Set([...ids.get(login.toLowerCase()) ?? [], ...theirs]))
|
|
28
|
+
return new Map([...named.keys()].map((login): [string, number | 'ambiguous' | null] => {
|
|
29
|
+
const theirs = ids.get(login.toLowerCase())!
|
|
30
|
+
if (theirs.size <= 1) return [login, [...theirs][0] ?? null]
|
|
31
|
+
return [login, oneAgent(login, theirs) ? null : 'ambiguous']
|
|
32
|
+
}))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function collect(answer: unknown, named: Map<string, Set<number>>, depth: number): void {
|
|
36
|
+
if (depth > MAX_DEPTH || answer === null || typeof answer !== 'object') return
|
|
37
|
+
if (Array.isArray(answer)) {
|
|
38
|
+
for (const value of answer) collect(value, named, depth + 1)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
const fields = answer as Record<string, unknown>
|
|
42
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
43
|
+
const idKey = LOGIN_KEYS.get(key)
|
|
44
|
+
if (idKey && typeof value === 'string' && LOGIN.test(value)) {
|
|
45
|
+
const id = accountId(fields[idKey])
|
|
46
|
+
named.set(value, new Set([...named.get(value) ?? [], ...id === null ? [] : [id]]))
|
|
47
|
+
} else collect(value, named, depth + 1)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const MAX_TEAM_NPM_SCOPES: number
|
|
2
|
+
export function isNpmPackageName(value: unknown): value is string
|
|
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
|
|
6
|
+
export function npmPackageScope(name: string): string | null
|
|
7
|
+
export function normalizeNpmScope(value: unknown): string | null
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// npm package names, versions and scopes as the managed npm viewer takes
|
|
2
|
+
// them, and the pnpm patches its reviews read, shared by its routes, the
|
|
3
|
+
// server and the Teams page.
|
|
4
|
+
|
|
5
|
+
// Upstream's rule (@preventive/upstream src/args.js): a scope, then a name
|
|
6
|
+
// starting with none of `.`, `_` and `-`, 214 characters in all.
|
|
7
|
+
const PACKAGE_NAME = /^(?=.{1,214}$)(?:@[\w.-]+\/[\w-]|[\dA-Za-z])[\w.-]*$/u
|
|
8
|
+
// An exact version or a dist-tag, as the registry resolves `/<name>/<spec>`.
|
|
9
|
+
const PACKAGE_SPEC = /^(?=.{1,256}$)[\dA-Za-z][\w.+-]*$/u
|
|
10
|
+
// npm's rule for a scope: lowercase, and not starting with `.` or `_`.
|
|
11
|
+
const SCOPE = /^@[\da-z~-][\w.~-]*$/u
|
|
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
|
|
16
|
+
|
|
17
|
+
export function isNpmPackageName(value) {
|
|
18
|
+
return typeof value === 'string' && PACKAGE_NAME.test(value)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isNpmPackageSpec(value) {
|
|
22
|
+
return typeof value === 'string' && PACKAGE_SPEC.test(value)
|
|
23
|
+
}
|
|
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
|
+
|
|
38
|
+
// `@scope` of a scoped name, lowercased as scopes are compared; null for an
|
|
39
|
+
// unscoped one.
|
|
40
|
+
export function npmPackageScope(name) {
|
|
41
|
+
return name.startsWith('@') && name.includes('/') ? name.slice(0, name.indexOf('/')).toLowerCase() : null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// A scope as typed: `scope` or `@scope`, either case, surrounding space
|
|
45
|
+
// ignored. Null when it is not one npm could publish under.
|
|
46
|
+
export function normalizeNpmScope(value) {
|
|
47
|
+
if (typeof value !== 'string') return null
|
|
48
|
+
const trimmed = value.trim().toLowerCase()
|
|
49
|
+
const scope = trimmed.startsWith('@') ? trimmed : `@${trimmed}`
|
|
50
|
+
return scope.length <= 214 && SCOPE.test(scope) ? scope : null
|
|
51
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Per-membership visibility permissions for a team's users — shared by the
|
|
2
|
+
// managed server (which stores + validates them) and the admin client (the
|
|
3
|
+
// team-member checkboxes). Each is an independent opt-in, BOTH DEFAULT OFF:
|
|
4
|
+
// dependencies — scan findings in dependencies' own code; App impact stays visible
|
|
5
|
+
// security — security findings and published bundle advisories
|
|
6
|
+
// The managed server filters report responses and authorizes advisory lookups
|
|
7
|
+
// using these independent permissions.
|
|
8
|
+
export type VisibilityPermission = 'dependencies' | 'security'
|
|
9
|
+
|
|
10
|
+
export const VISIBILITY_PERMISSIONS: readonly VisibilityPermission[] = ['dependencies', 'security']
|
|
11
|
+
|
|
12
|
+
// Human labels for the admin checkboxes (keyed by permission).
|
|
13
|
+
export const VISIBILITY_PERMISSION_LABELS: Record<VisibilityPermission, string> = {
|
|
14
|
+
dependencies: 'Dependencies',
|
|
15
|
+
security: 'Security',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// A membership's resolved permission set; every key present (false = off).
|
|
19
|
+
export type TeamUserPermissions = Record<VisibilityPermission, boolean>
|
|
20
|
+
|
|
21
|
+
// All-off — the default for a fresh membership.
|
|
22
|
+
const DEFAULT_TEAM_USER_PERMISSIONS: TeamUserPermissions = { dependencies: false, security: false }
|
|
23
|
+
|
|
24
|
+
// Coerce arbitrary input (a request body) into a full permission set: a key is
|
|
25
|
+
// granted only when explicitly `true`; anything else (absent, non-bool) is off.
|
|
26
|
+
export function parseTeamUserPermissions(x: unknown): TeamUserPermissions {
|
|
27
|
+
const o = (x == null || typeof x !== 'object') ? {} : x as Record<string, unknown>
|
|
28
|
+
const out = { ...DEFAULT_TEAM_USER_PERMISSIONS }
|
|
29
|
+
for (const p of VISIBILITY_PERMISSIONS) out[p] = o[p] === true
|
|
30
|
+
return out
|
|
31
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { parseLinkedFindings } from '../../client/linked-findings.js'
|
|
2
|
+
import { dependencyDirectory, isDependencyFile } from '../../client/dependency-paths.js'
|
|
3
|
+
import { detectFormat, loadFindings, parseCodexCsvToScans, readReport, reportEntries, reportRepoGithub } from '@preventive/report'
|
|
4
|
+
import { commonFileDirectory } from './repository-alias.ts'
|
|
5
|
+
|
|
6
|
+
// A managed blob retains one server identity even when a CSV contains several
|
|
7
|
+
// scans. Keep all their findings (and upstream ids) under that identity. CSV
|
|
8
|
+
// repository columns describe findings, not an embedded report-level location.
|
|
9
|
+
export function readManagedReport(content: string, filename: string): ReturnType<typeof readReport> {
|
|
10
|
+
const links = content.trimStart().startsWith('[') ? parseLinkedFindings(content) : null
|
|
11
|
+
if (links) return { data: { source: 'links', findings: [], links: links.groups }, format: 'links', reason: null }
|
|
12
|
+
const parsed = readReport(content)
|
|
13
|
+
// Permission-filtered responses can be JSON under the original CSV filename.
|
|
14
|
+
if (parsed.data != null || detectFormat(content, filename) !== 'codex') return parsed
|
|
15
|
+
try {
|
|
16
|
+
const scans = parseCodexCsvToScans(content)
|
|
17
|
+
return {
|
|
18
|
+
data: { type: 'security', source: 'codex-security', findings: scans.flatMap((scan) => scan.data.findings) },
|
|
19
|
+
format: 'codex', reason: null,
|
|
20
|
+
}
|
|
21
|
+
} catch (err) {
|
|
22
|
+
return { data: null, format: null, reason: err instanceof Error ? err.message : 'Invalid Codex CSV' }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function loadManagedFindings(content: string, filename: string): ReturnType<typeof loadFindings> {
|
|
27
|
+
const parsed = readManagedReport(content, filename)
|
|
28
|
+
return parsed.data == null ? Promise.resolve(null) : loadFindings(JSON.stringify(parsed.data))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Source access follows individual visible members, not their shared finding
|
|
32
|
+
// IDs: another member with the same ID can cite files the viewer cannot see.
|
|
33
|
+
export function managedFindingSourcePaths(findings: unknown[]): Set<string> {
|
|
34
|
+
const paths = new Set<string>()
|
|
35
|
+
for (const finding of findings) {
|
|
36
|
+
if (!finding || typeof finding !== 'object') continue
|
|
37
|
+
const f = finding as { file?: unknown; evidence?: { file?: unknown }[] }
|
|
38
|
+
if (typeof f.file === 'string' && f.file) paths.add(f.file)
|
|
39
|
+
for (const evidence of Array.isArray(f.evidence) ? f.evidence : []) {
|
|
40
|
+
if (typeof evidence?.file === 'string' && evidence.file) paths.add(evidence.file)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return paths
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The report's own findings, in either entry shape, outside the dependency
|
|
47
|
+
// directory the local report view would choose: node_modules, else vendor,
|
|
48
|
+
// else dependencies.
|
|
49
|
+
function ownFindings(data: unknown) {
|
|
50
|
+
const objects = (reportEntries(data) ?? []).flat().filter((finding): finding is { file?: unknown } => finding != null && typeof finding === 'object')
|
|
51
|
+
const directory = dependencyDirectory([{ groups: [objects], tree: (data as { tree?: unknown } | null)?.tree }])
|
|
52
|
+
return { directory, own: objects.filter(finding => !isDependencyFile(finding.file, directory)) }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Findings name the repository of their own file. Like the local report view,
|
|
56
|
+
// a report without its own repository belongs to the one repository named by
|
|
57
|
+
// its own findings (Claude Security and Codex exports name one on every finding).
|
|
58
|
+
export function findingsRepository(data: unknown): string | null {
|
|
59
|
+
const repos = new Map<string, string>()
|
|
60
|
+
for (const finding of ownFindings(data).own) {
|
|
61
|
+
const github = reportRepoGithub(finding)
|
|
62
|
+
if (github) repos.set(github.toLowerCase(), github)
|
|
63
|
+
}
|
|
64
|
+
return repos.size === 1 ? [...repos.values()][0]! : null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// The directory shared by the files the report's own findings cite, for alias
|
|
68
|
+
// matching. Dependency paths, including cited evidence, cannot hide it.
|
|
69
|
+
export function ownFileDirectory(data: unknown): string {
|
|
70
|
+
const { directory, own } = ownFindings(data)
|
|
71
|
+
return commonFileDirectory([...managedFindingSourcePaths(own)].filter(path => !isDependencyFile(path, directory)))
|
|
72
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Apply report permissions before serving findings, triage, or source files.
|
|
2
|
+
// Security uses the same complete-row classification as the UI. Dependency
|
|
3
|
+
// access is per finding: App, own source, and the own-source organizations stay.
|
|
4
|
+
import { inheritReportMeta, isAppFinding, reportRepoGithub, stampSecurityGroups } from '@preventive/report'
|
|
5
|
+
import { readManagedReport } from './report-content.ts'
|
|
6
|
+
import { revalidationLinks, securityLinks } from '../security-links.js'
|
|
7
|
+
|
|
8
|
+
export interface ViewerPermissions {
|
|
9
|
+
dependencies: boolean
|
|
10
|
+
security: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type Finding = Record<string, unknown>
|
|
14
|
+
function object(value: unknown): Finding | null {
|
|
15
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Finding : null
|
|
16
|
+
}
|
|
17
|
+
function pathOf(finding: Finding): string {
|
|
18
|
+
return typeof finding['file'] === 'string' ? finding['file'].replaceAll('\\', '/') : ''
|
|
19
|
+
}
|
|
20
|
+
function isDependency(finding: Finding): boolean {
|
|
21
|
+
return /(?:^|\/)(?:node_modules|vendor|dependencies)(?:\/|$)/u.test(pathOf(finding))
|
|
22
|
+
}
|
|
23
|
+
function githubOrg(value: unknown): string | null {
|
|
24
|
+
return reportRepoGithub(value)?.split('/')[0]?.toLowerCase() ?? null
|
|
25
|
+
}
|
|
26
|
+
function npmScope(finding: Finding, fromPath = false): string | null {
|
|
27
|
+
let name = object(object(finding['package'])?.['npm'])?.['name']
|
|
28
|
+
if (typeof name !== 'string' && fromPath) {
|
|
29
|
+
const parts = pathOf(finding).split('/')
|
|
30
|
+
for (let i = parts.length - 3; i >= 0; i--) {
|
|
31
|
+
if (['node_modules', 'vendor', 'dependencies'].includes(parts[i]!)) {
|
|
32
|
+
name = `${parts[i + 1]}/${parts[i + 2]}`
|
|
33
|
+
break
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return typeof name === 'string' ? /^(@[^/\s]+)\/[^/\s]+$/u.exec(name)?.[1] ?? null : null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Parsed projections keep run inheritance and derived flags out of cached
|
|
41
|
+
// reports. A known positive security stamp may include linked findings outside
|
|
42
|
+
// this report; a negative stamp never overrides the intrinsic classifier.
|
|
43
|
+
export function projectFinding(value: unknown, report: Finding): Finding | null {
|
|
44
|
+
const original = object(value)
|
|
45
|
+
if (!original) return null
|
|
46
|
+
const finding = { ...original }
|
|
47
|
+
inheritReportMeta(finding, report)
|
|
48
|
+
finding['_source'] = finding['source'] ?? report['source'] ?? null
|
|
49
|
+
if (!('isApp' in finding)) finding['isApp'] = isAppFinding(finding, finding['_source'])
|
|
50
|
+
if (finding['isSecurity'] === true) finding['security'] = true
|
|
51
|
+
return finding
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Native JSON and supported text imports obey the same permission rules.
|
|
55
|
+
// A filtered text import is returned as JSON under its original filename, which
|
|
56
|
+
// readManagedReport already accepts. Unchanged inputs keep their original bytes.
|
|
57
|
+
export function filterReportContent(content: string, perms: ViewerPermissions, filename = '', repo?: unknown): string {
|
|
58
|
+
if (perms.dependencies && perms.security) return content
|
|
59
|
+
let data: unknown
|
|
60
|
+
try { data = JSON.parse(content) } catch {
|
|
61
|
+
const parsed = readManagedReport(content, filename)
|
|
62
|
+
if (parsed.data == null) {
|
|
63
|
+
if (/\.csv$/iu.test(filename)) throw new Error('Cannot filter unreadable CSV report')
|
|
64
|
+
return content
|
|
65
|
+
}
|
|
66
|
+
data = parsed.data
|
|
67
|
+
}
|
|
68
|
+
const filtered = filterReportData(data, perms, repo)
|
|
69
|
+
return filtered === data ? content : JSON.stringify(filtered)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// repo, when supplied, is the server's authoritative assignment (including an
|
|
73
|
+
// explicit unassigned value). Finding-specific repos still describe their source.
|
|
74
|
+
export function filterReportData(data: unknown, perms: ViewerPermissions, repo?: unknown, securityIds?: Set<string>): unknown {
|
|
75
|
+
if (perms.dependencies && perms.security) return data
|
|
76
|
+
const report = object(data)
|
|
77
|
+
if (!report) return data
|
|
78
|
+
const key = Array.isArray(report['findings']) ? 'findings' : Array.isArray(report['groups']) ? 'groups' : null
|
|
79
|
+
if (!key) return data
|
|
80
|
+
const entries = (report[key] as unknown[]).map(entry => ({
|
|
81
|
+
entry, members: (Array.isArray(entry) ? entry : [entry]).map(original => ({ original, finding: projectFinding(original, report) })),
|
|
82
|
+
}))
|
|
83
|
+
const groups = entries.map(({ members }) => members.flatMap(({ finding }) => finding ? [finding] : []))
|
|
84
|
+
if (!perms.security) stampSecurityGroups(groups, { linkedIds: securityLinks(revalidationLinks(groups.flat())).linkedIds })
|
|
85
|
+
const githubOrgs = new Set<string>(), npmScopes = new Set<string>()
|
|
86
|
+
const addOrgs = (finding: Finding) => {
|
|
87
|
+
const github = githubOrg(finding), npm = npmScope(finding)
|
|
88
|
+
if (github) githubOrgs.add(github)
|
|
89
|
+
if (npm) npmScopes.add(npm)
|
|
90
|
+
}
|
|
91
|
+
addOrgs({ ...report, repo: repo === undefined ? report['repo'] : repo })
|
|
92
|
+
for (const finding of groups.flat()) {
|
|
93
|
+
if (finding['isApp'] !== true && !isDependency(finding)) addOrgs(finding)
|
|
94
|
+
}
|
|
95
|
+
const kept: unknown[] = []
|
|
96
|
+
let changed = false
|
|
97
|
+
for (const { entry, members } of entries) {
|
|
98
|
+
// Classify the complete row before hiding dependency components.
|
|
99
|
+
if (!perms.security && members.some(({ finding }) => finding?.['isSecurity'] === true || securityIds?.has(String(finding?.['id'])))) { changed = true; continue }
|
|
100
|
+
const visible = members.filter(({ finding }) => !finding || perms.dependencies || finding['isApp'] === true || !isDependency(finding)
|
|
101
|
+
|| githubOrgs.has(githubOrg(finding) ?? '') || npmScopes.has(npmScope(finding, true) ?? ''))
|
|
102
|
+
if (visible.length === members.length) kept.push(entry)
|
|
103
|
+
else {
|
|
104
|
+
changed = true
|
|
105
|
+
if (visible.length > 0) kept.push(Array.isArray(entry) ? visible.map(({ original }) => original) : visible[0]!.original)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return changed ? { ...report, [key]: kept } : data
|
|
109
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export interface RepositoryAliasInput {
|
|
2
|
+
oldRepo: string
|
|
3
|
+
oldPath: string
|
|
4
|
+
repoId: number
|
|
5
|
+
newPath: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function containsPath(parent: string, path: string) {
|
|
9
|
+
return !parent || path === parent || path.startsWith(`${parent}/`)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function aliasLocation(alias: RepositoryAliasInput, directory: string) {
|
|
13
|
+
if (containsPath(alias.oldPath, directory)) {
|
|
14
|
+
const suffix = directory.slice(alias.oldPath.length).replace(/^\//u, '')
|
|
15
|
+
return { directory: [alias.newPath, suffix].filter(Boolean).join('/'), filePrefix: '' }
|
|
16
|
+
}
|
|
17
|
+
if (!containsPath(directory, alias.oldPath)) return null
|
|
18
|
+
const filePrefix = alias.oldPath.slice(directory.length).replace(/^\//u, '')
|
|
19
|
+
// This suffix is already in every file path. The destination must preserve
|
|
20
|
+
// it, so mapping needs only a new directory, never a file-path rewrite.
|
|
21
|
+
if (alias.newPath === filePrefix) return { directory: '', filePrefix }
|
|
22
|
+
if (!alias.newPath.endsWith(`/${filePrefix}`)) return null
|
|
23
|
+
return { directory: alias.newPath.slice(0, -filePrefix.length - 1), filePrefix }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Match directory boundaries, keeping any subdirectory below the alias. A
|
|
27
|
+
// shared file prefix can prove a more specific alias applies without changing
|
|
28
|
+
// those files. The most specific applicable source wins, once, never chained.
|
|
29
|
+
export function matchRepositoryAlias(github: string, directory: string, aliases: readonly RepositoryAliasInput[], filePrefix = '') {
|
|
30
|
+
let match: { repoId: number; directory: string } | null = null, specificity = -1
|
|
31
|
+
for (const alias of aliases) {
|
|
32
|
+
if (alias.oldRepo.toLowerCase() !== github.toLowerCase()) continue
|
|
33
|
+
const location = aliasLocation(alias, directory)
|
|
34
|
+
if (!location || !containsPath(location.filePrefix, filePrefix) || alias.oldPath.length <= specificity) continue
|
|
35
|
+
match = { repoId: alias.repoId, directory: location.directory }
|
|
36
|
+
specificity = alias.oldPath.length
|
|
37
|
+
}
|
|
38
|
+
return match
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Run before decoding a bundle's inventory: unrelated repos/directories and
|
|
42
|
+
// mappings without the required common suffix cannot benefit from its paths.
|
|
43
|
+
export function repositoryAliasNeedsFilePrefix(github: string, directory: string, aliases: readonly RepositoryAliasInput[]) {
|
|
44
|
+
return aliases.some(alias => alias.oldRepo.toLowerCase() === github.toLowerCase()
|
|
45
|
+
&& Boolean(aliasLocation(alias, directory)?.filePrefix))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Whole directory segments only; no source bodies, hashing, or path rewriting.
|
|
49
|
+
// A single file still proves its parent prefix. Stop as soon as none is shared.
|
|
50
|
+
export function commonFileDirectory(paths: Iterable<string>): string {
|
|
51
|
+
let prefix: string[] | undefined
|
|
52
|
+
for (const path of paths) {
|
|
53
|
+
const parts = path.split('/')
|
|
54
|
+
if (parts.some(part => !part || part === '.' || part === '..') || path.includes('\\')) return ''
|
|
55
|
+
parts.pop()
|
|
56
|
+
if (prefix === undefined) prefix = parts
|
|
57
|
+
else {
|
|
58
|
+
let length = 0
|
|
59
|
+
while (length < prefix.length && length < parts.length && prefix[length] === parts[length]) length++
|
|
60
|
+
prefix.length = length
|
|
61
|
+
}
|
|
62
|
+
if (prefix.length === 0) return ''
|
|
63
|
+
}
|
|
64
|
+
return prefix?.join('/') ?? ''
|
|
65
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Managed-mode user roles, shared by the managed server (which stores +
|
|
2
|
+
// validates them) and the admin client (the role picker). A linear privilege
|
|
3
|
+
// ladder, highest first:
|
|
4
|
+
// admin — full access incl. managing other users' roles
|
|
5
|
+
// manage — owned/team content management
|
|
6
|
+
// triage — can triage findings
|
|
7
|
+
// view — read-only
|
|
8
|
+
// none — no access
|
|
9
|
+
// Admins manage workspace access. Managers handle owned uploads and content
|
|
10
|
+
// in their teams; repository changes also require repository/path access. View
|
|
11
|
+
// is the floor for reading content, triage for annotating accessible reports.
|
|
12
|
+
// None cannot read or mutate managed data, regardless of ownership or teams.
|
|
13
|
+
export type Role = 'admin' | 'manage' | 'triage' | 'view' | 'none'
|
|
14
|
+
|
|
15
|
+
export const ROLES: readonly Role[] = ['admin', 'manage', 'triage', 'view', 'none']
|
|
16
|
+
|
|
17
|
+
export function isRole(x: unknown): x is Role {
|
|
18
|
+
return typeof x === 'string' && (ROLES as readonly string[]).includes(x)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// True iff `role` sits at or above `min` in the privilege ladder. ROLES is
|
|
22
|
+
// ordered highest-first, so a lower index is more privileged; an unknown role
|
|
23
|
+
// (index -1) is never "at least" anything. e.g. roleAtLeast(role, 'view') is
|
|
24
|
+
// "has at least read access" (everything except 'none').
|
|
25
|
+
export function roleAtLeast(role: Role, min: Role): boolean {
|
|
26
|
+
const r = ROLES.indexOf(role)
|
|
27
|
+
return r !== -1 && r <= ROLES.indexOf(min)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Who reads connected repositories' code (the code viewer): a team member,
|
|
31
|
+
// view or above, the server reading only what their team grants reach and
|
|
32
|
+
// their own GitHub account may read. A public link reads none.
|
|
33
|
+
export function canReadRepositoryCode(session: { role: Role; publicShare?: boolean } | null | undefined): boolean {
|
|
34
|
+
return session != null && !session.publicShare && roleAtLeast(session.role, 'view')
|
|
35
|
+
}
|