@exodus/stasis 1.0.0-beta.6 → 1.0.0-beta.8
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/README.md +26 -24
- package/bin/stasis.js +3 -2
- package/package.json +6 -5
- package/src/audit-corrections.js +45 -21
- package/src/audit.js +21 -4
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ Lockfile can attest the bundle in a readable form, but that's not required - bun
|
|
|
27
27
|
| Modes: `add`, `replace`, `frozen`, `ignore` | Modes: `load`, `add`, `replace`, `frozen`, `ignore` |
|
|
28
28
|
|
|
29
29
|
The main difference between lockfiles and bundles is that lockfiles contain integrities, and bundles contain full content.\
|
|
30
|
-
See [file formats](https://github.com/
|
|
30
|
+
See [file formats](https://github.com/PreventiveMeasures/stasis/blob/main/doc/file-formats.md).
|
|
31
31
|
|
|
32
32
|
Both can be run in full scope (default) or just in `node_modules` scope.
|
|
33
33
|
|
|
@@ -58,38 +58,40 @@ _Lockfiles (npm/pnpm/etc) not mentioned: they are like the "tarball" column, but
|
|
|
58
58
|
| `stasis run --lock=add --bundle=add --mock app.js` | build without the app's side effects (network, fs writes) |
|
|
59
59
|
| `stasis run --lock=add --child-process app.js` | also attest modules loaded in forked child processes (e.g. Metro transform workers) |
|
|
60
60
|
| `stasis run --bundle=add --fs=sync app.js` | build a bundle that also captures sync `fs.readFileSync`/`readdirSync` reads |
|
|
61
|
-
| `stasis run --lock=add --import=./instrument.mjs app.js` |
|
|
62
|
-
| `stasis bundle src/index.js` | build a bundle statically, without executing it
|
|
63
|
-
| `stasis bundle --add src/worker.js` | merge more entries
|
|
64
|
-
| `stasis
|
|
65
|
-
| `stasis
|
|
66
|
-
| `stasis bundle --
|
|
67
|
-
| `stasis bundle --
|
|
68
|
-
| `stasis
|
|
69
|
-
| `stasis bundle --
|
|
70
|
-
| `stasis
|
|
71
|
-
| `stasis bundle --typescript src/index.ts` | statically bundle TS sources that import by output name, the way tsc resolves: a `./x.js` (or `.jsx`/`.mjs`/`.cjs`) specifier with no such file on disk lands on its on-disk `./x.ts` or `./x.tsx` (`.tsx`/`.mts`/`.cts`) source, and an extensionless `./x` probes `./x.ts`, `./x.tsx` and `./x/index.ts(x)`; the same substitution applies to package `main`/`exports`/`imports` targets and bare subpaths, and an existing `.js` always wins over its `.ts` twin. None of this applies from or into `node_modules`, by real path: an installed package is used as published, while a monorepo's workspace packages, linked in through `node_modules`, really live outside it and resolve like the project's own files. On without the flag when every entry is TypeScript and their relative imports of JS outputs (`./x.js`) are each only on disk as the TS source (`./x.ts`) -- tsc's convention; a project of Node-compatible `.ts` files importing `./x.ts` keeps Node's resolution. Otherwise an import that fails but would resolve under it names the file and `--typescript` in the error. Combines with `--jsx`/`--conditions`/`--mainFields`/`--metro` (not `--metro-resolver`, which can't substitute) |
|
|
72
|
-
| `stasis bundle --typescript --tsconfig=tsconfig.build.json src/index.ts` | same, honoring tsconfig `compilerOptions.paths` aliases (e.g. `"@/*": ["./src/*"]`, resolved against `baseUrl`, `extends` followed) for bare specifiers nothing else resolves, an extensionless target completing as tsc's does (`.ts`, `.tsx`, then `.js`, `.jsx`, then the directory's `index`); without `--tsconfig`, the `tsconfig.json` beside the importing file's `package.json` applies when present: in a monorepo (a `pnpm-workspace.yaml` or `package.json` `workspaces` at or above the cwd) the nearest named `package.json` at or above the file, else the project's one (the nearest named `package.json` at or above the cwd); never for a file in `node_modules`, and no lookup walks through one |
|
|
73
|
-
| `stasis github-bundle --github=owner/name [--sha=<commit>] [--tag=<name>] [--directory=path] [--package-manager=pnpm] [src/index.js]` | statically bundle a GitHub repo at a commit, or the one `--tag` names (the default branch's head without either), as `stasis bundle` bundles a clone of it once installed: the tree is fetched (with `GITHUB_TOKEN` where set) and held to its git tree id, and the dependencies are laid out in memory from the lockfile alone (`pnpm`, `yarn1`, `npm`, or `soldeer` for `.sol` entries, a Soldeer git dependency on GitHub fetched as the repo is; without `--package-manager`, the one whose lockfile installs the directory, where only one's does), with nothing installed and no package script run; where no entries are given, they are the JS files the directory's `package.json` names (`main`, `exports`, `bin`), resolved as the build resolves (`--conditions`, `--mainFields`, `--metro --platforms`, `--typescript`), or for Soldeer its `.sol` files directly in it, under `contracts/`, and under its source directory (`foundry.toml`'s `src`, else `src/`), but tests, scripts, mocks and dependency or build directories; its paths, and the directory its `repo` names, are the directory's, or for a JS bundle, the innermost package's at or above it that holds every file it bundles (the project root's, where one is outside it, such as a sibling workspace package or the root's `node_modules`); written to `owner-name.<commit's first 7>.stasis.code.br` by default, `owner-name.<directory, its / made ->.<commit's first 7>.stasis.code.br` with `--directory` (each character outside `[A-Za-z0-9._-]` made `_`, and a directory too deep to fit in 255 characters cut, with a hash of it); takes `stasis bundle`'s options for the entries and `--lockfile` (as a library: `buildGitHubBundle` in `@exodus/stasis/vfs-bundle`, and `suggestedEntries` for the entries it would take, of a GitHub repo or a Vfs) |
|
|
74
|
-
| `stasis github-bundle --github=owner/name --generate=prisma packages/api/src/index.ts` | same, with the Prisma Client each project's `prisma generate` would write first: for each project the lockfile installs with a `prisma-client` generator in its schema (the one its Prisma config names as a string literal or `path.join` of them, else `schema.prisma` or `prisma/schema.prisma`), as the Prisma it installs (7.4.0 to 7.10.0: its `prisma`, or beside Prisma 8, the one its `@prisma/prisma7` runs) writes it, byte for byte, where the generator's `output` says, the bundle then carrying it as a source like any other. Nothing of the repo runs: the schema, the config's `schema` path, and the `tsconfig.json` and `package.json` the generator infers its module format and import extensions from are read as data, its installed `prisma` only for its version (and, for an edge runtime's client, the query compiler it ships). The client is generated by the optional `@prisma/client-generator-ts` 7.10.0 dependency and rewritten as the version installed writes it; a project it can't generate for is skipped with a warning |
|
|
75
|
-
| `stasis build --output=out.js app.stasis.code.br [entry]` | rebuild a runnable JS bundle with esbuild, following the bundle's recorded import graph exactly (entry optional when the bundle has one) |
|
|
61
|
+
| `stasis run --lock=add --import=./instrument.mjs app.js` | also load a preload module; its own graph is runner infrastructure, not attested |
|
|
62
|
+
| `stasis bundle src/index.js` | build a bundle statically, without executing it |
|
|
63
|
+
| `stasis bundle --add src/worker.js` | merge more entries into the existing bundle |
|
|
64
|
+
| `stasis bundle --conditions=react-native,browser app.js` | statically bundle with extra `exports`/`imports` resolution conditions |
|
|
65
|
+
| `stasis bundle --mainFields=react-native,browser,main app.js` | statically bundle honoring legacy package `mainFields` |
|
|
66
|
+
| `stasis bundle --metro --platforms=ios,android app.js` | statically bundle the way Metro resolves, all platforms at once |
|
|
67
|
+
| `stasis bundle --typescript src/index.ts` | statically bundle TS sources, resolving `./x.js` imports to `./x.ts` as tsc does |
|
|
68
|
+
| `stasis add src assets` | add files to the project's bundles as-is, with no dependency resolution |
|
|
69
|
+
| `stasis github-bundle --github=owner/name --sha=<commit> src/index.js` | statically bundle a GitHub repo at a commit, with nothing installed |
|
|
70
|
+
| `stasis build --output=out.js app.stasis.code.br [entry]` | rebuild a runnable JS bundle with esbuild, following the bundle's recorded import graph exactly |
|
|
76
71
|
| `stasis build --output=out.js stasis.lock.json [entry]` | same, reading + verifying sources from disk against the lockfile |
|
|
77
72
|
| `stasis extract app.stasis.code.br` | unpack a bundle back to sources + a `stasis.lock.json` |
|
|
78
73
|
| `stasis diff --stat a.lock.json b.stasis.code.br` | summarize module/file differences between two lockfiles/bundles |
|
|
79
74
|
| `stasis prune` | trim `node_modules` to the lockfile, verifying the rest |
|
|
80
|
-
| `stasis audit stasis.lock.json` | report
|
|
81
|
-
| `stasis audit app.stasis.code.br` |
|
|
82
|
-
| `stasis audit --why app.stasis.code.br` | same, with the cross-module import paths that pull each flagged package in (`run: a -> b -> c`), prefixed by the consumer that imports each chain at the top level; a chain is skipped when its full tail is already listed as its own chain |
|
|
83
|
-
| `stasis audit --why-deep app.stasis.code.br` | like `--why`, but keep those longer chains too (every path, shared tails collapsed to `a -> b -> ... -> d`) |
|
|
84
|
-
| `stasis audit --why-full app.stasis.code.br` | like `--why`, but spell every chain out with no `...` collapse (combine with `--why-deep` for the complete raw listing) |
|
|
85
|
-
| `stasis audit --reason=run app.stasis.code.br` | show only advisories related to one consumer (`run`); with `--why`, keep only that consumer's chains |
|
|
86
|
-
| `GITHUB_TOKEN=... stasis audit --repo-advisories stasis.lock.json` | also ask each dependency's GitHub repository for the advisories its maintainers published there, which npm's database has only once GitHub reviews them; needs a GitHub token in `GITHUB_TOKEN`. Each package's repository is the one its artifact records (its `package.json`'s, see `repo` in [file formats](https://github.com/ExodusOSS/stasis/blob/main/doc/file-formats.md)), else it is looked up in npm's registry, Packagist or crates.io and cached for a month in the user cache directory (`$XDG_CACHE_HOME/stasis`, `~/.cache/stasis`, `~/Library/Caches/stasis` on macOS, `%LOCALAPPDATA%\stasis\Cache` on Windows) |
|
|
75
|
+
| `stasis audit stasis.lock.json` | report advisories for a lockfile's or bundle's dependencies |
|
|
76
|
+
| `stasis audit --why app.stasis.code.br` | same, with the import chains that pull each flagged package in |
|
|
87
77
|
| `stasis sbom --format=spdx stasis.lock.json` | export an SPDX SBOM for a lockfile or bundle |
|
|
88
78
|
| `stasis sbom --format=cyclonedx app.stasis.code.br` | export a CycloneDX SBOM for a lockfile or bundle |
|
|
89
79
|
|
|
80
|
+
Each command's options are documented in [doc/](https://github.com/PreventiveMeasures/stasis/tree/main/doc):
|
|
81
|
+
[`bundle`](https://github.com/PreventiveMeasures/stasis/blob/main/doc/bundle.md) (and `add`),
|
|
82
|
+
[`github-bundle`](https://github.com/PreventiveMeasures/stasis/blob/main/doc/github-bundle.md),
|
|
83
|
+
[`build`](https://github.com/PreventiveMeasures/stasis/blob/main/doc/build.md),
|
|
84
|
+
[`extract`](https://github.com/PreventiveMeasures/stasis/blob/main/doc/extract.md),
|
|
85
|
+
[`diff`](https://github.com/PreventiveMeasures/stasis/blob/main/doc/diff.md),
|
|
86
|
+
[`prune`](https://github.com/PreventiveMeasures/stasis/blob/main/doc/prune.md),
|
|
87
|
+
[`audit`](https://github.com/PreventiveMeasures/stasis/blob/main/doc/audit.md),
|
|
88
|
+
[`sbom`](https://github.com/PreventiveMeasures/stasis/blob/main/doc/sbom.md).
|
|
89
|
+
`stasis bundle` also bundles Solidity, PHP, Bash and Rust sources, see [file formats](https://github.com/PreventiveMeasures/stasis/blob/main/doc/file-formats.md#source-language-bundles-solidity--php--bash--rust).
|
|
90
|
+
|
|
90
91
|
## Runtime
|
|
91
92
|
|
|
92
|
-
The zero-dependency [`@exodus/stasis-core`](../stasis-core) CLI provides `run`, `
|
|
93
|
+
The zero-dependency [`@exodus/stasis-core`](../stasis-core) CLI provides the `run`, `add`, `extract` and `prune` commands only.\
|
|
94
|
+
The bundler plugins live in [`@exodus/stasis-plugins`](../stasis-plugins).
|
|
93
95
|
|
|
94
96
|
## License
|
|
95
97
|
|
package/bin/stasis.js
CHANGED
|
@@ -115,8 +115,9 @@ function usage(prefix = '') {
|
|
|
115
115
|
--reason=consumer shows only advisories related to that consumer, and with --why only its chains;
|
|
116
116
|
--repo-advisories also asks each package's GitHub repository for the advisories its maintainers
|
|
117
117
|
published there, before GitHub reviews them into the databases above; it needs a token in
|
|
118
|
-
GITHUB_TOKEN, and asks the repository each package's artifact records, else
|
|
119
|
-
|
|
118
|
+
GITHUB_TOKEN, and asks the repository each package's artifact records, else the one the
|
|
119
|
+
package.json a bundle carries of an npm package names, else looks one up and caches it for a
|
|
120
|
+
month in the user cache dir, e.g. ~/.cache/stasis)
|
|
120
121
|
stasis sbom --format=(spdx|cyclonedx) [--output=(path|-)] path/to/(lockfile|bundle) ...
|
|
121
122
|
(streams to stdout by default; --output=- is explicit stdout)
|
|
122
123
|
`.trim())
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/stasis",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.8",
|
|
4
4
|
"description": "File-level source bundles and lockfiles",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"./lockfile": "./src/lockfile.js",
|
|
20
20
|
"./prune": "./src/prune.js",
|
|
21
21
|
"./sbom": "./src/sbom.js",
|
|
22
|
+
"./audit-corrections": "./src/audit-corrections.js",
|
|
22
23
|
"./vfs-bundle": "./src/vfs-bundle.js",
|
|
23
24
|
"./logo.svg": "./logo.svg"
|
|
24
25
|
},
|
|
@@ -93,12 +94,12 @@
|
|
|
93
94
|
"homepage": "https://github.com/PreventiveMeasures/stasis#readme",
|
|
94
95
|
"dependencies": {
|
|
95
96
|
"@exodus/bytes": "^1.16.0",
|
|
96
|
-
"@exodus/stasis-core": "1.0.0-beta.
|
|
97
|
-
"@exodus/stasis-plugins": "1.0.0-beta.
|
|
97
|
+
"@exodus/stasis-core": "1.0.0-beta.8",
|
|
98
|
+
"@exodus/stasis-plugins": "1.0.0-beta.8",
|
|
98
99
|
"@preventive/archive": "1.0.0-beta.4",
|
|
99
|
-
"@preventive/deptree": "1.0.0-alpha.
|
|
100
|
+
"@preventive/deptree": "1.0.0-alpha.9",
|
|
100
101
|
"@preventive/lockfile": "1.0.0-alpha.7",
|
|
101
|
-
"@preventive/upstream": "1.0.0-alpha.
|
|
102
|
+
"@preventive/upstream": "1.0.0-alpha.11",
|
|
102
103
|
"@preventive/vfs": "1.0.0-beta.4",
|
|
103
104
|
"oxc-parser": "^0.152.0"
|
|
104
105
|
},
|
package/src/audit-corrections.js
CHANGED
|
@@ -6,38 +6,62 @@ import { satisfies, valid } from '@preventive/upstream/semver.js'
|
|
|
6
6
|
// package pulled in only through corrected files is skipped by the audit (see
|
|
7
7
|
// collectPackagesFromFile) -- none of its real code ships, so no advisory applies.
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
9
|
+
// Exported as `@exodus/stasis/audit-corrections`, as plain data, so a tool that
|
|
10
|
+
// lays out or reads packages without stasis -- a tree @preventive/deptree builds,
|
|
11
|
+
// a server reading bundles -- can apply the same corrections without its own copy.
|
|
12
|
+
// Each entry names a package of `ecosystem`, one audit-irrelevant `file` (relative
|
|
13
|
+
// to the module dir), `range` -- the semver range VERIFIED to match the rationale --
|
|
14
|
+
// and `integrity`, the sha512 of every distinct copy of `file` the versions in
|
|
15
|
+
// `range` publish, as a stasis lockfile records a file: a holder of the file's bytes
|
|
16
|
+
// can tell they are one of the copies verified. The correction never applies outside
|
|
17
|
+
// the range: a release could change the file, so the range is widened only after
|
|
18
|
+
// re-checking the file in the new versions, adding any new copy's integrity. Frozen,
|
|
19
|
+
// as the audit reads these very objects.
|
|
20
|
+
export const CORRECTIONS = Object.freeze([
|
|
14
21
|
{
|
|
15
22
|
// ws's browser build is a noop stub -- `module.exports = function () { throw
|
|
16
23
|
// new Error('ws does not work in the browser...') }` -- with none of the
|
|
17
|
-
// WebSocket implementation in it.
|
|
18
|
-
// (latest at the time of writing)
|
|
24
|
+
// WebSocket implementation in it. browser.js ships from 6.0.0 on; every copy
|
|
25
|
+
// through 8.22.0 (latest at the time of writing) is one of these two, which
|
|
26
|
+
// differ only in a space; re-check browser.js before widening.
|
|
27
|
+
ecosystem: 'npm',
|
|
19
28
|
name: 'ws',
|
|
20
|
-
|
|
21
|
-
range: '<=8.
|
|
29
|
+
file: 'browser.js',
|
|
30
|
+
range: '<=8.22.0',
|
|
31
|
+
integrity: [
|
|
32
|
+
'sha512-98uEc4/THUpHCA7AQwBKLHHmxV6leyGmPkDAYZsoBiigxjKL8//TIb/oUKDUJLc+s/pwdGA9LR0F9e7nZHfXUQ==', // `function ()`: 6.0.0 - 6.1.1, 7.3.1 - 8.22.0
|
|
33
|
+
'sha512-F7vx6oiX9vsFoxNPTudk20Hjp7IzJePAJCYaSoX9FMJ2JaGVcFx4Bkw5W7+I3a1DsonitnosbpuG1qAMsUwH7w==', // `function()`: 6.1.2 - 7.3.0
|
|
34
|
+
],
|
|
22
35
|
},
|
|
23
36
|
{
|
|
24
37
|
// node-fetch's browser build re-exports the environment's native fetch
|
|
25
38
|
// (`module.exports = globalObject.fetch`, plus Headers/Request/Response) --
|
|
26
39
|
// none of the node-fetch implementation, where its advisories live, is in it.
|
|
27
|
-
// browser.js ships only through 2.7.0 (3.x dropped it);
|
|
40
|
+
// browser.js ships only from 2.0.0 through 2.7.0 (3.x dropped it); every copy
|
|
41
|
+
// in the range is one of these, which differ in how they find the global.
|
|
42
|
+
ecosystem: 'npm',
|
|
28
43
|
name: 'node-fetch',
|
|
29
|
-
|
|
44
|
+
file: 'browser.js',
|
|
30
45
|
range: '<=2.7.0',
|
|
46
|
+
integrity: [
|
|
47
|
+
'sha512-uCZB0X6rUVIf5lZU2NN9/Z1Fd6cPCLg4M9eduYxOgSP3QZNwi0/xSa/s6AUFtvLz4y4MK02SJihOIhDoFIzdiA==', // 2.0.0 - 2.1.1
|
|
48
|
+
'sha512-toQ4RDdyjsAxTaxnaLs2AE56rXws+jMZV896U8pP0MSuKRiq8ZGKkOTMsvyCJoAJTbrgoQpr89H2n8+Z1ALcYA==', // 2.1.2
|
|
49
|
+
'sha512-LOd+I1R3xYSCHFN9oj3xeN3wa7fYYx5XQKTiKkvWn4dSYVgHI2jsXZp0mJt5KlQg3ECXXh5UtpEW7kGu1dG/aQ==', // 2.2.0 - 2.2.1
|
|
50
|
+
'sha512-2yo4Ysl+g09ocycwEBcN7IkSvM/KPpO9CW+1FI+0QfzvWJfaQepo+WEqn1/o5Og1weM14flcdv23hqZtxFXFUw==', // 2.3.0 - 2.6.0
|
|
51
|
+
'sha512-WgHaZJVwFovVrKVySaKzxzXssn5kY8y3x8NJThodDRL9nwUxV7Q1z/RRzuLZ2lNkxabHLpG/l+vu7KQ2tGbP8g==', // 2.6.1 - 2.6.7
|
|
52
|
+
'sha512-84sQFcu3uzgEsJOjIhz6tE8vInV1STCA4sX5EI9NK3Tt8n/Kb1serbKMz+YkdOAuCspHRiSUW2qV01z0wOgIXg==', // 2.6.8
|
|
53
|
+
'sha512-SkBfLO20d2Ngr6nhxgh/Pt6Yqe6oW8EB99KSPq0lrW0se8jIiRF3YpOe7hHbfNWxuNCsKoZZT98uCJI9OJAa/Q==', // 2.6.9 - 2.7.0
|
|
54
|
+
],
|
|
31
55
|
},
|
|
32
|
-
]
|
|
56
|
+
].map((correction) => Object.freeze({ ...correction, integrity: Object.freeze(correction.integrity) })))
|
|
33
57
|
|
|
34
58
|
// Is `rel` (a file path relative to the module dir) audit-irrelevant for
|
|
35
|
-
// `name@version`? Unknown or unparsable versions are never
|
|
36
|
-
// closed: the package stays audited).
|
|
37
|
-
function isCorrectedFile(name, version, rel) {
|
|
38
|
-
for (const
|
|
39
|
-
if (
|
|
40
|
-
if (valid(version) && satisfies(version, range)) return true
|
|
59
|
+
// `name@version` of `ecosystem`? Unknown or unparsable versions are never
|
|
60
|
+
// corrected (fail closed: the package stays audited).
|
|
61
|
+
function isCorrectedFile(name, version, rel, ecosystem) {
|
|
62
|
+
for (const correction of CORRECTIONS) {
|
|
63
|
+
if (correction.ecosystem !== ecosystem || correction.name !== name || correction.file !== rel) continue
|
|
64
|
+
if (valid(version) && satisfies(version, correction.range)) return true
|
|
41
65
|
}
|
|
42
66
|
return false
|
|
43
67
|
}
|
|
@@ -65,8 +89,8 @@ const isManifest = (rel, ecosystem) => MANIFESTS[ecosystem]?.has(rel.slice(rel.l
|
|
|
65
89
|
// Is `rel` evidence that `name@version`'s REAL code is present, of a dependency of
|
|
66
90
|
// `ecosystem` (npm by default)? This is the one rule every audit surface shares --
|
|
67
91
|
// package presence, the reason column, and the --why chain graph all count a file
|
|
68
|
-
// (or an edge targeting it) only when it passes. Manifests never do;
|
|
69
|
-
// files
|
|
92
|
+
// (or an edge targeting it) only when it passes. Manifests never do; CORRECTIONS'
|
|
93
|
+
// files don't within their verified range.
|
|
70
94
|
export function isEvidenceFile(name, version, rel, ecosystem = 'npm') {
|
|
71
|
-
return !isManifest(rel, ecosystem) &&
|
|
95
|
+
return !isManifest(rel, ecosystem) && !isCorrectedFile(name, version, rel, ecosystem)
|
|
72
96
|
}
|
package/src/audit.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { packageRepo } from '@exodus/stasis-core/bundle-util'
|
|
1
2
|
import { dependencyEcosystem, moduleFileKey, sameGithub } from '@exodus/stasis-core/util'
|
|
2
3
|
import { advisories } from '@preventive/upstream/advisories.js'
|
|
3
4
|
import { compareVersions, valid } from '@preventive/upstream/semver.js'
|
|
4
5
|
import { isEvidenceFile } from './audit-corrections.js'
|
|
5
|
-
import { parseFile } from './parse.js'
|
|
6
|
+
import { parseFile, parseFileWithKind } from './parse.js'
|
|
6
7
|
import { collectWhy, invertReason } from './why.js'
|
|
7
8
|
|
|
8
9
|
// Where advisories() asks for each ecosystem's advisories: npm's registry; OSV for crates (those
|
|
@@ -57,21 +58,37 @@ function unaudited({ ecosystem, version }) {
|
|
|
57
58
|
// why.js apply the same evidence rule per file/edge there.
|
|
58
59
|
//
|
|
59
60
|
// A package's `github` is the repository its artifact records for it (its package.json's), where
|
|
60
|
-
// one does; a
|
|
61
|
+
// one does; else, for an npm package a bundle carries the package.json of (one built before builds
|
|
62
|
+
// recorded `repo`), the one that names, read by the rule builds record it by. A GitHub repo is its
|
|
63
|
+
// own, and has none.
|
|
61
64
|
export function collectPackagesFromFile(file) {
|
|
62
65
|
const out = []
|
|
63
|
-
|
|
66
|
+
const { kind, artifact } = parseFileWithKind(file)
|
|
67
|
+
for (const [dir, { name, version, ecosystem: tagged, repo, files }] of artifact.modules) {
|
|
64
68
|
const ecosystem = dependencyEcosystem(dir, tagged)
|
|
65
69
|
// First-party code is never sent to a public registry (leaks names, adds noise).
|
|
66
70
|
if (ecosystem === undefined) continue
|
|
67
71
|
if (!name || !version) continue
|
|
68
72
|
if (!Object.keys(files).some((rel) => isEvidenceFile(name, version, rel, ecosystem))) continue
|
|
69
|
-
|
|
73
|
+
let github = ecosystem === 'github' ? undefined : repo?.github
|
|
74
|
+
if (github === undefined && ecosystem === 'npm' && kind === 'bundle') github = manifestRepo(files['package.json'])
|
|
70
75
|
out.push({ ecosystem, name, version: versionOf(ecosystem, version), ...(github === undefined ? {} : { github }) })
|
|
71
76
|
}
|
|
72
77
|
return out
|
|
73
78
|
}
|
|
74
79
|
|
|
80
|
+
// The GitHub repository a bundled package.json's text names (packageRepo), or undefined where it
|
|
81
|
+
// names none or doesn't parse. Read as Node reads one, past a byte order mark (packageJSONText),
|
|
82
|
+
// which a bundle keeps in the text. A lockfile holds the file's digest alone, never asked here.
|
|
83
|
+
function manifestRepo(text) {
|
|
84
|
+
if (typeof text !== 'string') return undefined
|
|
85
|
+
try {
|
|
86
|
+
return packageRepo(JSON.parse(text.replace(/^\uFEFF/u, '')))?.github
|
|
87
|
+
} catch {
|
|
88
|
+
return undefined
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
75
92
|
// Versions in semver's order where both are semver's, else as numbers in text compare.
|
|
76
93
|
const byNumbers = new Intl.Collator('en', { numeric: true }).compare
|
|
77
94
|
const byVersion = (a, b) => (valid(a) && valid(b) ? compareVersions(a, b) : byNumbers(a, b))
|