@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,205 @@
|
|
|
1
|
+
// The hosts a version's code names, and the services malicious packages send
|
|
2
|
+
// what they take to, fetch what they run from or look the machine up with,
|
|
3
|
+
// for a reviewer: the hosts a version's scripts name that the version
|
|
4
|
+
// compared with's named in none of theirs, and the services a file names,
|
|
5
|
+
// under a tag for how much they call for (HOST_TAGS: Exfiltration, Remote,
|
|
6
|
+
// IP lookup), as a file's stamps say (stamps.js).
|
|
7
|
+
//
|
|
8
|
+
// A quick read, not a parse, as runs-code.js reads: a host is a URL's in a
|
|
9
|
+
// script's strings, regular expressions and template literals
|
|
10
|
+
// (JS_STRING_OR_COMMENT), its comments aside, where a link to docs is no host
|
|
11
|
+
// the code calls; an exfiltration host is told in any text but prose and a
|
|
12
|
+
// source map, wherever it is written. A host built as the code runs
|
|
13
|
+
// (`'https://' + name`, `${host}`) goes unseen, so a version without these
|
|
14
|
+
// labels is not one that sends nothing.
|
|
15
|
+
import { NPM_PROSE, SOURCE_MAP } from './readability.js'
|
|
16
|
+
import { JS_STRING_OR_COMMENT, NPM_MAX_FOUND, lineOf, npmIsScript } from './script.js'
|
|
17
|
+
|
|
18
|
+
// How many hosts a file is told naming at most, the first it names.
|
|
19
|
+
const NPM_MAX_HOSTS = 1000
|
|
20
|
+
|
|
21
|
+
// A URL's host: a name whose last label is letters (an internationalized
|
|
22
|
+
// one's too, as written or as punycode), or an IPv4 address, after its
|
|
23
|
+
// scheme, as a regular expression may leave its `s` optional (`https?://`),
|
|
24
|
+
// and any user, its case aside; whole, not the start of one built as the code
|
|
25
|
+
// runs (`api.service.com.${tld}`).
|
|
26
|
+
const LABEL = String.raw`[\p{L}\p{N}](?:[\p{L}\p{N}-]*[\p{L}\p{N}])?`
|
|
27
|
+
const URL_HOST = new RegExp(String.raw`\b(?:https?|wss?|ftp)\??:\/\/(?:[^\s/@'"\x60]*@)?(${LABEL}(?:\.${LABEL})+)(?![\p{L}\p{N}-]|\.[\p{L}\p{N}_$-])`, 'giu')
|
|
28
|
+
const NAMED = /\.(?:\p{L}{2,}|xn--[a-z0-9-]+)$/u
|
|
29
|
+
const IPV4 = /^(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)$/u
|
|
30
|
+
// Hosts no code calls out to: this machine, the names kept for examples and
|
|
31
|
+
// tests (RFC 2606, RFC 6761) and local networks, and W3C's, whose URLs name
|
|
32
|
+
// XML namespaces.
|
|
33
|
+
const LOCAL = /^(?:localhost|.+\.(?:localhost|local|test|invalid|example|internal)|(?:.+\.)?example\.(?:com|net|org)|127(?:\.\d+){3}|0\.0\.0\.0|(?:.+\.)?w3\.org)$/u
|
|
34
|
+
|
|
35
|
+
// What a regular expression escapes in a URL, its slashes and dots
|
|
36
|
+
// (`/https:\/\/api\.example\.com/`), as a string may its slashes
|
|
37
|
+
// (`"https:\/\/api.example.com"`, as JSON can be written), read unescaped.
|
|
38
|
+
const ESCAPED = /\\([/.])/gu
|
|
39
|
+
|
|
40
|
+
// The pages github.com shows of a repository that a person reads, past its
|
|
41
|
+
// owner and name, as links and error messages name them: not its Actions,
|
|
42
|
+
// whose runs' artifacts and logs are downloads of what its workflows made.
|
|
43
|
+
const GITHUB_PAGES = new Set(['issues', 'pull', 'pulls', 'blob', 'tree', 'wiki', 'discussions', 'commit', 'commits', 'compare', 'blame',
|
|
44
|
+
'security', 'projects', 'labels', 'milestone', 'milestones', 'releases', 'tags', 'graphs', 'network', 'stargazers'])
|
|
45
|
+
// The paths past github.com that name no repository but a page of GitHub's
|
|
46
|
+
// own (`github.com/sponsors/someone`, `github.com/orgs/someone/people`).
|
|
47
|
+
const GITHUB_OWN = new Set(['sponsors', 'orgs', 'apps', 'marketplace', 'features', 'topics', 'collections', 'settings', 'login', 'search', 'explore', 'about'])
|
|
48
|
+
// How a github.com URL (`url`, as written from its scheme on, the punctuation
|
|
49
|
+
// a sentence ends it with aside) is named among a file's hosts, read as fetch
|
|
50
|
+
// reads it (WHATWG URL: a port, a trailing dot, backslashes as slashes, `..`
|
|
51
|
+
// resolved): `github.com` for its home or a person's, a page of GitHub's own
|
|
52
|
+
// (GITHUB_OWN), a page of a repository a person reads (GITHUB_PAGES), a
|
|
53
|
+
// release's page (`releases/tag/v1`, `releases/latest`) but not its assets'
|
|
54
|
+
// (`releases/latest/download/…`), a file's page but not the file raw
|
|
55
|
+
// (`blob/…?raw=true`, any `raw` asked), and a page but not its content in another format, as
|
|
56
|
+
// GitHub serves one by a suffix to its path past a file's (`pull/1.patch`,
|
|
57
|
+
// `commit/….diff`, `commits/main.atom`). Any other (a repository's own URL,
|
|
58
|
+
// which git clones, `raw/…`, `archive/…`, `tarball`, `repo.git`, a path GitHub
|
|
59
|
+
// may add, one read otherwise) leads to whatever a repository holds: named by
|
|
60
|
+
// it (`github.com/owner/repo`, in lower case, as GitHub's names are any), so
|
|
61
|
+
// that it is never taken for one of github.com's links to code and docs
|
|
62
|
+
// (compare.js ALLOWED_HOSTS).
|
|
63
|
+
const FILE_PATHS = new Set(['blob', 'tree', 'blame', 'wiki'])
|
|
64
|
+
const FORMAT = /\.(?:patch|diff|atom)$/iu
|
|
65
|
+
const GIT = /\.git$/iu
|
|
66
|
+
const decoded = part => { try { return decodeURIComponent(part) } catch { return part } }
|
|
67
|
+
function githubNamed(url) {
|
|
68
|
+
let parsed = null
|
|
69
|
+
try { parsed = new URL(url.replace(/^(\w+)\?:/u, '$1:').replace(/[)\]}>.,;:!]+$/u, '')) } catch {}
|
|
70
|
+
if (!parsed) return 'github.com/'
|
|
71
|
+
// Its path and query decoded, as GitHub routes them (`pull/1%2Epatch`,
|
|
72
|
+
// `?raw=%31`).
|
|
73
|
+
const segments = parsed.pathname.split('/').slice(1).map(decoded)
|
|
74
|
+
if (segments.length > 1 && segments.at(-1) === '') segments.pop()
|
|
75
|
+
const [owner, repo = '', section, ...rest] = segments
|
|
76
|
+
const repositoryPage = owner !== '' && repo !== '' && !GIT.test(repo) && GITHUB_PAGES.has(section)
|
|
77
|
+
&& !(section === 'releases' && rest.includes('download')) && !(section === 'blob' && parsed.searchParams.has('raw'))
|
|
78
|
+
&& !(!FILE_PATHS.has(section) && FORMAT.test(segments.at(-1)))
|
|
79
|
+
const page = parsed.hostname.replace(/\.$/u, '') === 'github.com' && (segments.length === 1 || GITHUB_OWN.has(owner) || repositoryPage)
|
|
80
|
+
return page ? 'github.com' : `github.com/${owner}/${repo.replace(GIT, '')}`.toLowerCase()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// The hosts a script's strings name, each with the line it is first named on,
|
|
84
|
+
// in the order named, NPM_MAX_HOSTS at most; null for none, or for a file
|
|
85
|
+
// that is no script, by its name or as node runs it (`script`, npmIsScript).
|
|
86
|
+
export function npmHosts(path, text, script = false) {
|
|
87
|
+
if (!npmIsScript(path, text, script) || !(text.includes('://') || text.includes(':\\/\\/'))) return null
|
|
88
|
+
const hosts = new Map(), line = lineOf(text)
|
|
89
|
+
for (const { 1: token, index } of text.matchAll(JS_STRING_OR_COMMENT)) {
|
|
90
|
+
if (hosts.size >= NPM_MAX_HOSTS) break
|
|
91
|
+
// Read unescaped, its offsets as near: on the same line but in a template
|
|
92
|
+
// across lines; a regular expression without its closing slash and flags,
|
|
93
|
+
// as a string's quote ends its URL (`/https:\/\/github.com\/a\/b\/pull\/1.patch/i`).
|
|
94
|
+
const body = token?.startsWith('/') ? token.replace(/\/[a-z]*$/u, '') : token
|
|
95
|
+
const string = body?.includes('\\') ? body.replaceAll(ESCAPED, '$1') : body
|
|
96
|
+
if (!string?.includes('://')) continue
|
|
97
|
+
for (const { 1: name, index: at } of string.matchAll(URL_HOST)) {
|
|
98
|
+
const host = name.toLowerCase()
|
|
99
|
+
const named = host === 'github.com' ? githubNamed(/^[^\s'"\u0060]*/u.exec(string.slice(at))[0]) : host
|
|
100
|
+
if (!hosts.has(named) && (NAMED.test(host) || IPV4.test(host)) && !LOCAL.test(host)) hosts.set(named, line(index + at))
|
|
101
|
+
if (hosts.size >= NPM_MAX_HOSTS) break
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return hosts.size > 0 ? hosts : null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// A host as it is written, under another or not (`m.webhook.site`), then a
|
|
108
|
+
// path on it where only that path is the service (`/api/webhooks`), on its
|
|
109
|
+
// own: not part of a longer name (`notwebhook.site`, `webhook.sites`), a
|
|
110
|
+
// name under another domain (`webhook.site.evil.example`, `webhook.site.рф`),
|
|
111
|
+
// nor another path (`/install.sh`, `/api/webhooks.json`), though a sentence
|
|
112
|
+
// may end after it. Its name first, so a text is searched for that alone.
|
|
113
|
+
const NAME_CHAR = String.raw`[\p{L}\p{N}_-]`
|
|
114
|
+
const host = (name, path = '') => new RegExp(String.raw`(?<!${NAME_CHAR})${name}${path}(?!\.?${NAME_CHAR})`, 'giu')
|
|
115
|
+
// One so like a file's name or code (`transfer.sh`, `file.io`) only as a
|
|
116
|
+
// URL's host: after the `//` that starts its authority (or `\/\/` as JSON can
|
|
117
|
+
// write it), where the URL starts, its scheme as URL_HOST takes one after the
|
|
118
|
+
// text's start, a blank, a quote, `(`, `=`, `,`, `<` or `>`, or without one
|
|
119
|
+
// after a quote, `(`, `=` or `,` (`"//file.io/x"`): not a URL's path, whatever
|
|
120
|
+
// it holds (`https://example.com/a//transfer.sh`, `/http://transfer.sh`); and
|
|
121
|
+
// after any user, not a user itself (`https://transfer.sh@example.com`).
|
|
122
|
+
const inUrl = name => new RegExp(String.raw`(?<=(?:(?:^|[\s'"\x60(=,<>])(?:https?|wss?|ftp)\??:|['"\x60(=,])\\?\/\\?\/(?:[^\s/@'"\x60]*@)?(?:${NAME_CHAR}+\.)*)${name}(?!\.?${NAME_CHAR}|(?::[^\s/@'"\x60]*)?@)`, 'giu')
|
|
123
|
+
// The tags the services go under, each with its severity (stamps.js
|
|
124
|
+
// SEVERITIES), as a change between versions raises it (npmHostSeverity):
|
|
125
|
+
// Exfiltration, `critical` (red), where nothing but catching what is sent
|
|
126
|
+
// has a use for it in a package's code; Remote, `high` (orange), where it
|
|
127
|
+
// could be fine; and IP lookup, `moderate` (yellow), where it usually is.
|
|
128
|
+
export const HOST_TAGS = new Map([['Exfiltration', 'critical'], ['Remote', 'high'], ['IP lookup', 'moderate']])
|
|
129
|
+
const [CATCHER, REMOTE, IP_LOOKUP] = [...HOST_TAGS.keys()].map(tag => ({ tag }))
|
|
130
|
+
// Services malicious packages send what they take to, fetch what they run
|
|
131
|
+
// from, or look the machine up with, each under its tag. Exfiltration's:
|
|
132
|
+
// request catchers, and out-of-band testing hosts (interactsh's, Burp
|
|
133
|
+
// Collaborator's, Canarytokens', DNS logs). Remote's: bot and webhook APIs,
|
|
134
|
+
// paste sites and file drops, tunnels, Pipedream's own API (its endpoints
|
|
135
|
+
// are `pipedream.net`), and the install script of Bun (`bun.sh/install`, now
|
|
136
|
+
// `bun.com/install`), which a worm ran its payload with. And IP lookups.
|
|
137
|
+
export const EXFILTRATION = new Map([
|
|
138
|
+
['webhook-site', { ...CATCHER, name: 'webhook.site', pattern: host(String.raw`webhook\.site`) }],
|
|
139
|
+
['pipedream', { ...CATCHER, name: 'Pipedream', pattern: host(String.raw`pipedream\.net`) }],
|
|
140
|
+
['requestbin', { ...CATCHER, name: 'RequestBin', pattern: host(String.raw`(?:requestbin\.(?:net|com)|requestb\.in)`) }],
|
|
141
|
+
['requestcatcher', { ...CATCHER, name: 'Request Catcher', pattern: host(String.raw`requestcatcher\.com`) }],
|
|
142
|
+
['requestrepo', { ...CATCHER, name: 'requestrepo', pattern: host(String.raw`requestrepo\.com`) }],
|
|
143
|
+
['beeceptor', { ...CATCHER, name: 'Beeceptor', pattern: host(String.raw`beeceptor\.com`) }],
|
|
144
|
+
['oast', { ...CATCHER, name: 'OAST', pattern: host(String.raw`(?:interact\.sh|interactsh\.com|oast\.(?:pro|live|site|online|fun|me)|oastify\.com|burpcollaborator\.net)`) }],
|
|
145
|
+
['canarytokens', { ...CATCHER, name: 'Canarytokens', pattern: host(String.raw`canarytokens\.(?:com|org)`) }],
|
|
146
|
+
['dnslog', { ...CATCHER, name: 'DNSLog', pattern: host(String.raw`dnslog\.cn`) }],
|
|
147
|
+
['ceye', { ...CATCHER, name: 'CEYE', pattern: host(String.raw`ceye\.io`) }],
|
|
148
|
+
['telegram', { ...REMOTE, name: 'Telegram', pattern: host(String.raw`api\.telegram\.org`) }],
|
|
149
|
+
['discord', { ...REMOTE, name: 'Discord webhook', pattern: host(String.raw`discord(?:app)?\.com`, String.raw`/api(?:/v\d+)?/webhooks`) }],
|
|
150
|
+
['slack', { ...REMOTE, name: 'Slack webhook', pattern: host(String.raw`hooks\.slack\.com`, '/(?:services|workflows|triggers)') }],
|
|
151
|
+
['pastebin', { ...REMOTE, name: 'Pastebin', pattern: host(String.raw`pastebin\.com`) }],
|
|
152
|
+
['paste-ee', { ...REMOTE, name: 'paste.ee', pattern: host(String.raw`paste\.ee`) }],
|
|
153
|
+
['transfer-sh', { ...REMOTE, name: 'transfer.sh', pattern: inUrl(String.raw`transfer\.sh`) }],
|
|
154
|
+
['file-io', { ...REMOTE, name: 'file.io', pattern: inUrl(String.raw`file\.io`) }],
|
|
155
|
+
['0x0', { ...REMOTE, name: '0x0.st', pattern: host(String.raw`0x0\.st`) }],
|
|
156
|
+
['ngrok', { ...REMOTE, name: 'ngrok', pattern: host(String.raw`ngrok(?:-free)?\.(?:io|app|dev)`) }],
|
|
157
|
+
['localtunnel', { ...REMOTE, name: 'localtunnel', pattern: host(String.raw`loca\.lt`) }],
|
|
158
|
+
['cloudflare-tunnel', { ...REMOTE, name: 'Cloudflare Tunnel', pattern: host(String.raw`trycloudflare\.com`) }],
|
|
159
|
+
['serveo', { ...REMOTE, name: 'Serveo', pattern: host(String.raw`serveo\.net`) }],
|
|
160
|
+
['pipedream-api', { ...REMOTE, name: 'Pipedream API', pattern: host(String.raw`pipedream\.com`) }],
|
|
161
|
+
['bun', { ...REMOTE, name: 'Bun install', pattern: host(String.raw`bun\.(?:sh|com)`, '/install') }],
|
|
162
|
+
['ipinfo', { ...IP_LOOKUP, name: 'ipinfo', pattern: host(String.raw`ipinfo\.io`) }],
|
|
163
|
+
['ipify', { ...IP_LOOKUP, name: 'ipify', pattern: host(String.raw`ipify\.org`) }],
|
|
164
|
+
['ifconfig-me', { ...IP_LOOKUP, name: 'ifconfig.me', pattern: host(String.raw`ifconfig\.me`) }],
|
|
165
|
+
['icanhazip', { ...IP_LOOKUP, name: 'icanhazip', pattern: host(String.raw`icanhazip\.com`) }],
|
|
166
|
+
['ip-api', { ...IP_LOOKUP, name: 'ip-api', pattern: host(String.raw`ip-api\.com`) }],
|
|
167
|
+
['ipapi', { ...IP_LOOKUP, name: 'ipapi', pattern: host(String.raw`ipapi\.com?`) }],
|
|
168
|
+
['checkip', { ...IP_LOOKUP, name: 'checkip', pattern: host(String.raw`checkip\.amazonaws\.com`) }],
|
|
169
|
+
])
|
|
170
|
+
// Any of them, as a text may say it: most say none, told at once.
|
|
171
|
+
const SAYS_ANY = new RegExp([...EXFILTRATION.values()].map(({ pattern }) => pattern.source).join('|'), 'iu')
|
|
172
|
+
|
|
173
|
+
// The services a file names, each with the lines it is named on, the first
|
|
174
|
+
// NPM_MAX_FOUND, in EXFILTRATION's order; null for none, or for prose or a
|
|
175
|
+
// source map by its name that node doesn't run as a script (`script`).
|
|
176
|
+
export function npmExfiltration(path, text, script = false) {
|
|
177
|
+
if (typeof text !== 'string' || (!script && (NPM_PROSE.test(path) || SOURCE_MAP.test(path))) || !SAYS_ANY.test(text)) return null
|
|
178
|
+
const found = new Map(), line = lineOf(text)
|
|
179
|
+
for (const [service, { pattern }] of EXFILTRATION) {
|
|
180
|
+
const lines = new Set()
|
|
181
|
+
for (const { index } of text.matchAll(pattern)) if (lines.add(line(index)).size >= NPM_MAX_FOUND) break
|
|
182
|
+
if (lines.size > 0) found.set(service, [...lines])
|
|
183
|
+
}
|
|
184
|
+
return found.size > 0 ? found : null
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// The services of `found` under `tag` (HOST_TAGS), in their order; null for
|
|
188
|
+
// none.
|
|
189
|
+
export function npmHostsTagged(found, tag) {
|
|
190
|
+
const tagged = found && new Map([...found].filter(([service]) => EXFILTRATION.get(service).tag === tag))
|
|
191
|
+
return tagged?.size > 0 ? tagged : null
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// The services a file names (`found`) by their tag, in HOST_TAGS' order:
|
|
195
|
+
// `[tag, services]` each.
|
|
196
|
+
export const npmHostTags = found => [...HOST_TAGS.keys()].map(tag => [tag, npmHostsTagged(found, tag)]).filter(([, services]) => services)
|
|
197
|
+
|
|
198
|
+
// How severe a tag's `services` are: its own severity, or with `was` (null for
|
|
199
|
+
// a file that held none), where one of them the file didn't name, a level
|
|
200
|
+
// higher, never past `critical`, as a change between versions calls for more.
|
|
201
|
+
export function npmHostSeverity(tag, services, was) {
|
|
202
|
+
const levels = [...HOST_TAGS.values()]
|
|
203
|
+
const at = levels.indexOf(HOST_TAGS.get(tag))
|
|
204
|
+
return levels[was !== undefined && [...services.keys()].some(service => !was?.has(service)) ? Math.max(0, at - 1) : at]
|
|
205
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// How a file's text reads, for a reviewer: its category (binary, a source
|
|
2
|
+
// map, code with unexpectedly long lines or an inline map, minified, else
|
|
3
|
+
// ASCII or UTF-8), its control and invisible characters, its longest lines,
|
|
4
|
+
// an obfuscator's fingerprint, a large code file, and a file become
|
|
5
|
+
// unreadable between two versions.
|
|
6
|
+
import { CONTROLS, npmTextEncoding } from './characters.js'
|
|
7
|
+
import { MINIFIED_AVERAGE, minifiable, minifiedAverage } from './script.js'
|
|
8
|
+
|
|
9
|
+
// Lines longer than anyone writes by hand.
|
|
10
|
+
export const NPM_LONG_LINE = 1000
|
|
11
|
+
// Prose, which wraps where it is read: its long lines are paragraphs. By its
|
|
12
|
+
// extension (text, Markdown and MDX, and the markup GitHub renders a README
|
|
13
|
+
// from: `README.org`, `README.rdoc`), or its name alone (`LICENSE-MIT`,
|
|
14
|
+
// `CHANGES`; a dot in it only a version's, `LICENSE-LGPL2.1`,
|
|
15
|
+
// `CHANGES.2.0`; a prose extension after it is one), not code named alike
|
|
16
|
+
// (`lib/history.js`, `dist/notice.min.js`), which can be minified as any.
|
|
17
|
+
export const NPM_PROSE = /(?:\.(?:md|markdown|mdown|mkdn|mdx|txt|rst|adoc|asciidoc|asc|textile|rdoc|org|creole|mediawiki|wiki|pod)|(?:^|\/)(?:licen[cs]e|copying|notice|authors|contributors|readme|changelog|changes|history)(?:-[^/.]*)?(?:\.\d[^/.]*)*)$/iu
|
|
18
|
+
const SOURCE_MAP_COMMENT = /^\s*(?:\/\/|\/\*)[#@] sourceMappingURL=/u
|
|
19
|
+
// A line that is all one, the map in it as a data: URL, base64 or
|
|
20
|
+
// percent-encoded: not code that writes one.
|
|
21
|
+
const INLINE_SOURCE_MAP = /^\s*(?:\/\/|\/\*)[#@] sourceMappingURL=data:[^\s,]*,[\w+/=%.~-]*\s*(?:\*\/)?\s*$/u
|
|
22
|
+
export const SOURCE_MAP = /\.map$/iu
|
|
23
|
+
const MINIFIED_NAME = /\.min\.[^/.]+$/iu
|
|
24
|
+
// What javascript-obfuscator writes, and next to nothing else: its names,
|
|
25
|
+
// `_0x` and four to six hex digits, OBFUSCATED_NAMES of them or more, or the
|
|
26
|
+
// loop it turns its array of strings with, `while(!![])`, spaced or not.
|
|
27
|
+
const OBFUSCATED_NAME = /(?<![\p{ID_Continue}$])_0x[0-9a-f]{4,6}(?![\p{ID_Continue}$])/gu
|
|
28
|
+
export const OBFUSCATED_NAMES = 20
|
|
29
|
+
const OBFUSCATED_LOOP = /while\s*\(\s*!!\[\]\s*\)/u
|
|
30
|
+
// Names counted before a fingerprint's count stops, reading as that many or
|
|
31
|
+
// more: each is kept to tell it from the others, and a file can hold millions.
|
|
32
|
+
export const OBFUSCATED_COUNTED = 1000
|
|
33
|
+
// A text's obfuscator fingerprint, `{ names, loop }`: how many such names it
|
|
34
|
+
// has, up to OBFUSCATED_COUNTED, and whether it has the loop; null where it
|
|
35
|
+
// has neither enough names nor the loop.
|
|
36
|
+
export function npmObfuscation(text) {
|
|
37
|
+
const names = new Set()
|
|
38
|
+
for (const [name] of text.matchAll(OBFUSCATED_NAME)) {
|
|
39
|
+
names.add(name)
|
|
40
|
+
if (names.size === OBFUSCATED_COUNTED) break
|
|
41
|
+
}
|
|
42
|
+
const loop = OBFUSCATED_LOOP.test(text)
|
|
43
|
+
return names.size >= OBFUSCATED_NAMES || loop ? { names: names.size, loop } : null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// A code file this large is mostly a bundle, worth a look where a version
|
|
47
|
+
// adds it: JavaScript or TypeScript, not a declaration file, which holds no
|
|
48
|
+
// code that runs, or any file node runs as a script (`script`, npm/files.js
|
|
49
|
+
// npmLoadedScripts).
|
|
50
|
+
export const NPM_LARGE_CODE = 256 * 1024
|
|
51
|
+
const CODE = /\.(?:[cm]?[jt]s|[jt]sx)$/iu
|
|
52
|
+
const DECLARATION = /\.d\.[cm]?ts$/iu
|
|
53
|
+
export const npmLargeCode = (path, bytes, script = false) => typeof bytes === 'number' && bytes >= NPM_LARGE_CODE && (script || (CODE.test(path) && !DECLARATION.test(path)))
|
|
54
|
+
|
|
55
|
+
// A code file that isn't text (npmFileReadability's `binary`: not UTF-8, or
|
|
56
|
+
// holding a NUL), by its path and how it reads, a declaration file too, or
|
|
57
|
+
// any file node runs as a script (`script`, npm/files.js npmLoadedScripts):
|
|
58
|
+
// no one can read it in review, yet what reads it by its byte order mark can
|
|
59
|
+
// run it, or type with it, as UTF-16 (a browser's classic script, Windows
|
|
60
|
+
// Script Host, TypeScript), and node runs it decoded as UTF-8 whatever it
|
|
61
|
+
// holds past its code (`//` and a byte no UTF-8 has).
|
|
62
|
+
export const npmBinaryCode = (path, readability, script = false) => readability?.category === 'binary' && (script || CODE.test(path))
|
|
63
|
+
|
|
64
|
+
// A file with a line over UNREADABLE_LINE characters, by how it reads
|
|
65
|
+
// (`now`, npmFileReadability's), `path` its path: an inline source map's line
|
|
66
|
+
// counts for none, and prose and source maps, whose long lines the Long lines
|
|
67
|
+
// tag passes over, never are, but where node runs it as a script (`script`),
|
|
68
|
+
// nor is a file that isn't text. Then one that had no line over SHORT_LINES
|
|
69
|
+
// characters (`was`) and now has one over UNREADABLE_LINE, by how it reads on
|
|
70
|
+
// each side.
|
|
71
|
+
const SHORT_LINES = 200
|
|
72
|
+
const UNREADABLE_LINE = 2000
|
|
73
|
+
export const npmUnreadable = (path, now, script = false) => Boolean(now) && now.category !== 'binary' && (script || (!NPM_PROSE.test(path) && !SOURCE_MAP.test(path)))
|
|
74
|
+
&& now.longest > UNREADABLE_LINE
|
|
75
|
+
export const npmBecameUnreadable = (path, was, now, script = false) => Boolean(was) && was.category !== 'binary' && was.longest <= SHORT_LINES && npmUnreadable(path, now, script)
|
|
76
|
+
|
|
77
|
+
// How a file reads, as its `category`, the first that holds (READABILITY):
|
|
78
|
+
// binary (no text), map (a source map), long (code with some lines
|
|
79
|
+
// longer than anyone writes), inline-map (code with its source map in it, minified or
|
|
80
|
+
// not), minified (code mostly on long lines, or named .min., or minified
|
|
81
|
+
// into shorter lines: see MINIFIED_AVERAGE), else utf8 or ascii; and
|
|
82
|
+
// whatever it is, the control, bidirectional or invisible characters a text
|
|
83
|
+
// holds (`controls`), found in one pass, which count as a label of their own
|
|
84
|
+
// beside it each, control and invisible (npmReadabilityLabels). Prose is
|
|
85
|
+
// readable whatever its lines' lengths, and an inline source map's line
|
|
86
|
+
// counts for none, nor for how much of the code is on long lines. With
|
|
87
|
+
// npmTextEncoding's `kind` and `controls`, its `longest` line's length and
|
|
88
|
+
// `longLines`, its code's lines' `average` length where it is minified into
|
|
89
|
+
// short ones (strings and comments aside, see MINIFIED_AVERAGE; 0 otherwise),
|
|
90
|
+
// how long its inline map is (`inlineMap`, 0 for none), and its obfuscator
|
|
91
|
+
// fingerprint (`obfuscation`, npmObfuscation's; null for none, and for prose
|
|
92
|
+
// or a source map). One node runs as a script whatever its name (`script`,
|
|
93
|
+
// script.js npmIsScript) is read as code: no prose nor source map.
|
|
94
|
+
export function npmFileReadability(path, text, script = false) {
|
|
95
|
+
const encoding = npmTextEncoding(text)
|
|
96
|
+
if (encoding.kind === 'binary') return { ...encoding, category: 'binary', longest: 0, longLines: 0, average: 0, inlineMap: 0, obfuscation: null }
|
|
97
|
+
let inlineMap = 0, longChars = 0, longLines = 0, longest = 0
|
|
98
|
+
for (let at = 0; at <= text.length;) {
|
|
99
|
+
const next = text.indexOf('\n', at)
|
|
100
|
+
const end = next === -1 ? text.length : next
|
|
101
|
+
const length = end - at - (text[end - 1] === '\r' ? 1 : 0)
|
|
102
|
+
if (SOURCE_MAP_COMMENT.test(text.slice(at, at + 64)) && INLINE_SOURCE_MAP.test(text.slice(at, end))) inlineMap += length
|
|
103
|
+
else {
|
|
104
|
+
if (length > NPM_LONG_LINE) {
|
|
105
|
+
longLines++
|
|
106
|
+
longChars += length
|
|
107
|
+
}
|
|
108
|
+
longest = Math.max(longest, length)
|
|
109
|
+
}
|
|
110
|
+
at = end + 1
|
|
111
|
+
}
|
|
112
|
+
// A source map holds the names and sources of the code it maps, and prose
|
|
113
|
+
// its examples: neither runs.
|
|
114
|
+
const map = !script && SOURCE_MAP.test(path), prose = !script && NPM_PROSE.test(path)
|
|
115
|
+
const obfuscation = prose || map ? null : npmObfuscation(text)
|
|
116
|
+
const read = { ...encoding, longest, longLines, average: 0, inlineMap, obfuscation }
|
|
117
|
+
if (map) return { ...read, category: 'map' }
|
|
118
|
+
if (prose) return { ...read, category: inlineMap > 0 ? 'inline-map' : encoding.kind }
|
|
119
|
+
if (longLines === 0) {
|
|
120
|
+
// None of its lines longer than MINIFIED_AVERAGE, none of its code's can
|
|
121
|
+
// average more: told without reading its code.
|
|
122
|
+
const language = minifiable(path, script)
|
|
123
|
+
const average = language !== null && longest > MINIFIED_AVERAGE ? minifiedAverage(text, language) : 0
|
|
124
|
+
return { ...read, average, category: inlineMap > 0 ? 'inline-map' : average > 0 ? 'minified' : encoding.kind }
|
|
125
|
+
}
|
|
126
|
+
if (longChars / (text.length - inlineMap) < .5 && !MINIFIED_NAME.test(path)) return { ...read, category: 'long' }
|
|
127
|
+
return { ...read, category: inlineMap > 0 ? 'inline-map' : 'minified' }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// The category a file's tag shows: for one with long lines, how its text is
|
|
131
|
+
// encoded.
|
|
132
|
+
export const npmTaggedCategory = readability => readability.category === 'long' ? readability.kind : readability.category
|
|
133
|
+
|
|
134
|
+
// What a file is labelled, in its tags and in the Overview's warning and
|
|
135
|
+
// chips, each finding it: its control characters, its invisible ones
|
|
136
|
+
// (characterLabels) and its long lines, where it holds them, beside its category (how its text is encoded, for one whose long
|
|
137
|
+
// lines are unexpected), but for ASCII or UTF-8, which a file that can't be
|
|
138
|
+
// read as it is counts under as none of them. A minified file's long lines
|
|
139
|
+
// are labelled too, as its inline map's are; prose's and a source map's
|
|
140
|
+
// never are.
|
|
141
|
+
export function npmReadabilityLabels(readability) {
|
|
142
|
+
const flags = [...characterLabels(readability.controls),
|
|
143
|
+
...readability.longLines > 0 && ['long', 'minified', 'inline-map'].includes(readability.category) ? ['long'] : []]
|
|
144
|
+
const category = npmTaggedCategory(readability)
|
|
145
|
+
return READABILITY.get(category).mark || flags.length === 0 ? [...flags, category] : flags
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// How a label is marked on a file: long lines as a warning where it is
|
|
149
|
+
// otherwise readable, as they are unexpected there, and as a notice in
|
|
150
|
+
// minified code, as minified code is; any other as its category is.
|
|
151
|
+
export const npmLabelMark = (readability, label) => label === 'long' && readability.category !== 'long' ? 'notice' : READABILITY.get(label).mark
|
|
152
|
+
|
|
153
|
+
// Each category: its name, its tag in a list of files or a file's head (and,
|
|
154
|
+
// `short`, in a file tree, where that is too long), and how it is marked,
|
|
155
|
+
// `warn` for what to look into, `notice` for what can be hard to review by
|
|
156
|
+
// reading it either way.
|
|
157
|
+
export const READABILITY = new Map([
|
|
158
|
+
['binary', { name: 'Binary', tag: 'Binary', mark: 'warn' }],
|
|
159
|
+
['controls', { name: 'Control chars', tag: 'Controls', mark: 'warn' }],
|
|
160
|
+
['invisible', { name: 'Invisible chars', tag: 'Invisible', mark: 'warn' }],
|
|
161
|
+
['long', { name: 'Long lines', tag: 'Long lines', mark: 'warn' }],
|
|
162
|
+
['minified', { name: 'Minified', tag: 'Minified', short: 'Min', mark: 'notice' }],
|
|
163
|
+
['map', { name: 'Source maps', tag: 'Map', mark: 'notice' }],
|
|
164
|
+
['inline-map', { name: 'Inline source maps', tag: 'Inline map', mark: 'notice' }],
|
|
165
|
+
['utf8', { name: 'UTF-8', tag: 'UTF-8', mark: null }],
|
|
166
|
+
['ascii', { name: 'ASCII', tag: 'ASCII', mark: null }],
|
|
167
|
+
])
|
|
168
|
+
|
|
169
|
+
// The characters a text holds of a label's kind, `controls` (C0, DEL, C1
|
|
170
|
+
// and bidirectional controls) or `invisible`, `[point, times]`, the most
|
|
171
|
+
// found first: those npmTextEncoding found in one pass, told apart by code
|
|
172
|
+
// point. The labels a text's characters take, of those two.
|
|
173
|
+
const CONTROL = new RegExp(CONTROLS.source, 'u')
|
|
174
|
+
export const charactersOf = (controls, label) => [...controls ?? []]
|
|
175
|
+
.filter(([point]) => CONTROL.test(String.fromCodePoint(point)) === (label === 'controls')).toSorted((a, b) => b[1] - a[1] || a[0] - b[0])
|
|
176
|
+
const characterLabels = controls => controls ? ['controls', 'invisible'].filter(label => charactersOf(controls, label).length > 0) : []
|
|
177
|
+
|