@codyswann/lisa 2.325.5 → 2.326.0
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/all/copy-overwrite/scripts/lisa-floor-collisions.mjs +188 -0
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +3 -0
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file Fail when a security override can be collapsed into a weaker floor.
|
|
4
|
+
*
|
|
5
|
+
* npm and bun both let an override be written as `$name`, meaning "use whatever
|
|
6
|
+
* the root package pins this to". bun does not merely permit that form — it
|
|
7
|
+
* REWRITES to it, on every `bun install`, whenever an override names a package
|
|
8
|
+
* the project also depends on directly. The override stops carrying its own
|
|
9
|
+
* floor and adopts the dependency line's.
|
|
10
|
+
*
|
|
11
|
+
* That is fine when the two agree. It is a silent security regression when the
|
|
12
|
+
* dependency line is lower, which is the normal case: an override exists
|
|
13
|
+
* *because* the direct range was too permissive. Observed in
|
|
14
|
+
* gunnertech/frontend on bun 1.3.11, from a clean tree:
|
|
15
|
+
*
|
|
16
|
+
* "postcss": ">=8.5.18" -> "$postcss" with devDependencies ^8.5.0
|
|
17
|
+
* "prettier": "3.8.3" -> "$prettier" with devDependencies ^3.3.3
|
|
18
|
+
*
|
|
19
|
+
* postcss 8.5.0 is inside the range GHSA patches at 8.5.18. The rewrite reads
|
|
20
|
+
* as a tidy-up in the diff, recurs on every install, and defeats any attempt to
|
|
21
|
+
* restore the floor by hand.
|
|
22
|
+
*
|
|
23
|
+
* This check is deliberately OFFLINE and deterministic. It asks one structural
|
|
24
|
+
* question — can this override be collapsed into something weaker? — and needs
|
|
25
|
+
* no advisory lookup to answer it, so it runs in every project's CI in
|
|
26
|
+
* milliseconds and cannot go quiet when a network call fails. Whether a given
|
|
27
|
+
* floor is high enough for current advisories is a separate question, answered
|
|
28
|
+
* by check-security-floors.mjs against the advisory database.
|
|
29
|
+
*
|
|
30
|
+
* The fix it asks for is always the same: raise the dependency line to the
|
|
31
|
+
* floor. Removing the override is the wrong move — it is what carries the floor
|
|
32
|
+
* to transitive copies, which is the case it was written for.
|
|
33
|
+
*
|
|
34
|
+
* Usage:
|
|
35
|
+
* lisa-floor-collisions.mjs [--manifest package.json] [--json]
|
|
36
|
+
*
|
|
37
|
+
* Exit 1 when any override would collapse to a weaker range.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import { readFileSync } from "node:fs";
|
|
41
|
+
|
|
42
|
+
const OVERRIDE_SECTIONS = ["overrides", "resolutions"];
|
|
43
|
+
const DEPENDENCY_SECTIONS = [
|
|
44
|
+
"dependencies",
|
|
45
|
+
"devDependencies",
|
|
46
|
+
"optionalDependencies",
|
|
47
|
+
"peerDependencies",
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Lowest version a range permits, as a comparable tuple.
|
|
52
|
+
*
|
|
53
|
+
* Deliberately crude: it reads the first version in the spec, which is the
|
|
54
|
+
* lower bound for every form used in practice (`>=1.2.3`, `^1.2.3`, `~1.2.3`,
|
|
55
|
+
* `1.2.3`, `>=1.2.3 <2`). A spec carrying no version — `*`, `workspace:*`,
|
|
56
|
+
* `latest` — has no floor to compare and returns null.
|
|
57
|
+
* @param {string} spec A version range.
|
|
58
|
+
* @returns {number[]|null} [major, minor, patch], or null when there is none.
|
|
59
|
+
*/
|
|
60
|
+
export function lowestPermitted(spec) {
|
|
61
|
+
const match = /(\d+)\.(\d+)\.(\d+)/.exec(spec ?? "");
|
|
62
|
+
return match ? match.slice(1, 4).map(Number) : null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Compare two version tuples.
|
|
67
|
+
* @param {number[]} left First version.
|
|
68
|
+
* @param {number[]} right Second version.
|
|
69
|
+
* @returns {number} Negative, zero, or positive.
|
|
70
|
+
*/
|
|
71
|
+
function compare(left, right) {
|
|
72
|
+
for (let index = 0; index < 3; index += 1) {
|
|
73
|
+
if (left[index] !== right[index]) return left[index] - right[index];
|
|
74
|
+
}
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Every direct dependency range, flattened across sections.
|
|
80
|
+
*
|
|
81
|
+
* Later sections do not overwrite earlier ones: the FIRST declaration wins, and
|
|
82
|
+
* a package declared twice is a different problem this check does not own.
|
|
83
|
+
* @param {object} manifest A parsed package.json.
|
|
84
|
+
* @returns {Map<string, {spec: string, section: string}>} By package name.
|
|
85
|
+
*/
|
|
86
|
+
export function directDependencies(manifest) {
|
|
87
|
+
const found = new Map();
|
|
88
|
+
for (const section of DEPENDENCY_SECTIONS) {
|
|
89
|
+
for (const [name, spec] of Object.entries(manifest[section] ?? {})) {
|
|
90
|
+
if (typeof spec === "string" && !found.has(name)) {
|
|
91
|
+
found.set(name, { spec, section });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return found;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Overrides that a package manager could collapse into a weaker floor.
|
|
100
|
+
*
|
|
101
|
+
* Only INTACT overrides are judged. A `$name` override is skipped, and the
|
|
102
|
+
* reason is that the evidence is already gone: once collapsed, the manifest no
|
|
103
|
+
* longer records what the floor used to be, so there is nothing to compare the
|
|
104
|
+
* dependency line against. Reporting every collapsed override instead would
|
|
105
|
+
* fire on the correct end state — an override collapsed onto a dependency that
|
|
106
|
+
* already carries the floor is exactly what a fixed project looks like after
|
|
107
|
+
* its next install — and a check that fails on the fix is one that gets
|
|
108
|
+
* skipped rather than heeded.
|
|
109
|
+
*
|
|
110
|
+
* That leaves a genuine gap for a project whose floor was lost before this
|
|
111
|
+
* check ever ran. Nothing offline can recover it; the advisory-database check
|
|
112
|
+
* is what covers that case, by asking whether the surviving range is high
|
|
113
|
+
* enough rather than whether it changed.
|
|
114
|
+
* @param {object} manifest A parsed package.json.
|
|
115
|
+
* @returns {Array<{name: string, override: string, dependency: string,
|
|
116
|
+
* section: string, from: string}>} Collisions found.
|
|
117
|
+
*/
|
|
118
|
+
export function collisions(manifest) {
|
|
119
|
+
const direct = directDependencies(manifest);
|
|
120
|
+
const found = [];
|
|
121
|
+
|
|
122
|
+
for (const from of OVERRIDE_SECTIONS) {
|
|
123
|
+
for (const [name, spec] of Object.entries(manifest[from] ?? {})) {
|
|
124
|
+
if (typeof spec !== "string" || spec.startsWith("$")) continue;
|
|
125
|
+
const dependency = direct.get(name);
|
|
126
|
+
if (!dependency) continue;
|
|
127
|
+
|
|
128
|
+
const floor = lowestPermitted(spec);
|
|
129
|
+
const target = lowestPermitted(dependency.spec);
|
|
130
|
+
if (!floor || !target) continue;
|
|
131
|
+
if (compare(target, floor) >= 0) continue;
|
|
132
|
+
|
|
133
|
+
found.push({
|
|
134
|
+
name,
|
|
135
|
+
override: spec,
|
|
136
|
+
dependency: dependency.spec,
|
|
137
|
+
section: dependency.section,
|
|
138
|
+
from,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return found;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function main() {
|
|
146
|
+
const argv = process.argv.slice(2);
|
|
147
|
+
const asJson = argv.includes("--json");
|
|
148
|
+
const manifestIndex = argv.indexOf("--manifest");
|
|
149
|
+
const manifestPath =
|
|
150
|
+
manifestIndex >= 0 ? argv[manifestIndex + 1] : "package.json";
|
|
151
|
+
|
|
152
|
+
let manifest;
|
|
153
|
+
try {
|
|
154
|
+
manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
155
|
+
} catch (error) {
|
|
156
|
+
process.stderr.write(`${manifestPath} is not readable: ${error.message}\n`);
|
|
157
|
+
process.exit(2);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const found = collisions(manifest);
|
|
161
|
+
|
|
162
|
+
if (asJson) {
|
|
163
|
+
console.log(JSON.stringify({ manifest: manifestPath, found }, null, 2));
|
|
164
|
+
} else if (found.length === 0) {
|
|
165
|
+
console.log(
|
|
166
|
+
"## Floor collisions\n\nNo override can be collapsed into a weaker range."
|
|
167
|
+
);
|
|
168
|
+
} else {
|
|
169
|
+
console.log("## Floor collisions\n");
|
|
170
|
+
console.log(
|
|
171
|
+
"Each override below names a package this project also depends on directly, whose range permits a LOWER version. `bun install` rewrites such an override to `$name`, at which point the floor becomes the dependency line and the protection is gone.\n"
|
|
172
|
+
);
|
|
173
|
+
console.log("| package | override | direct dependency |");
|
|
174
|
+
console.log("|---|---|---|");
|
|
175
|
+
for (const row of found) {
|
|
176
|
+
console.log(
|
|
177
|
+
`| \`${row.name}\` | \`${row.override}\` (${row.from}) | \`${row.dependency}\` (${row.section}) |`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
console.log(
|
|
181
|
+
"\nRaise the direct dependency to the floor. Do not delete the override — it is what carries the floor to transitive copies, which is the case it was written for."
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (found.length > 0) process.exitCode = 1;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (import.meta.main) main();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upstream-evidence-manifest.d.ts","sourceRoot":"","sources":["../../src/core/upstream-evidence-manifest.ts"],"names":[],"mappings":"AACA,iFAAiF;AACjF,eAAO,MAAM,0BAA0B,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"upstream-evidence-manifest.d.ts","sourceRoot":"","sources":["../../src/core/upstream-evidence-manifest.ts"],"names":[],"mappings":"AACA,iFAAiF;AACjF,eAAO,MAAM,0BAA0B,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAkoEpE,CAAC;AAEL,4EAA4E;AAC5E,eAAO,MAAM,yBAAyB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAwlNjE,CAAC;AAEL,sFAAsF;AACtF,eAAO,MAAM,uBAAuB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAk6I/D,CAAC"}
|
|
@@ -4,6 +4,7 @@ export const UPSTREAM_EVIDENCE_MANIFEST = Object.freeze({
|
|
|
4
4
|
"all/copy-contents/.gitattributes": "9d3831007e681186a3673e1037ef3fc82980cab2fc04e27c0868e562912c9e9f",
|
|
5
5
|
"all/copy-contents/gitignore": "2dbf7fd2f2020fc7824c432342002d9ca3ebb57b2872e761b14e942b23479a11",
|
|
6
6
|
"all/copy-overwrite/scripts/lisa-enforcement-fallback.sh": "9cab71562d01421d57c6719318a4d03a6249a45c1aea434db3ff26e3065eeb66",
|
|
7
|
+
"all/copy-overwrite/scripts/lisa-floor-collisions.mjs": "a612f172282d13ba9318fe5e03689d7c6ff42a0122d0c9d5d39051e1967b93dc",
|
|
7
8
|
"all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh": "aa95a84f9f9224914947a09b858535aa8bd052199ff64cb53768cf78fb69b687",
|
|
8
9
|
"all/copy-overwrite/scripts/lisa-hooks/block-shell-json-parsing.sh": "3b5f074f3ab5708030af507eea962389f3b067c5dbdc76580bd9e44d3ac6c603",
|
|
9
10
|
"all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh": "062c69eea85157f1e941ec3626d8912aa9f182af6ccdbe19687588a6074db5ce",
|
|
@@ -1238,6 +1239,7 @@ export const UPSTREAM_SURFACE_MANIFEST = Object.freeze({
|
|
|
1238
1239
|
"all/copy-contents/.gitattributes": true,
|
|
1239
1240
|
"all/copy-contents/gitignore": true,
|
|
1240
1241
|
"all/copy-overwrite/scripts/lisa-enforcement-fallback.sh": true,
|
|
1242
|
+
"all/copy-overwrite/scripts/lisa-floor-collisions.mjs": true,
|
|
1241
1243
|
"all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh": true,
|
|
1242
1244
|
"all/copy-overwrite/scripts/lisa-hooks/block-shell-json-parsing.sh": true,
|
|
1243
1245
|
"all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh": true,
|
|
@@ -7262,6 +7264,7 @@ export const UPSTREAM_SURFACE_MANIFEST = Object.freeze({
|
|
|
7262
7264
|
"tests/unit/scripts/detect-stale-workflow-inputs-helpers.ts": true,
|
|
7263
7265
|
"tests/unit/scripts/detect-stale-workflow-inputs.test.ts": true,
|
|
7264
7266
|
"tests/unit/scripts/e2e-coverage.test.ts": true,
|
|
7267
|
+
"tests/unit/scripts/floor-collisions.test.ts": true,
|
|
7265
7268
|
"tests/unit/scripts/generate-agy-plugin-artifacts.test.ts": true,
|
|
7266
7269
|
"tests/unit/scripts/generate-copilot-plugin-artifacts.test.ts": true,
|
|
7267
7270
|
"tests/unit/scripts/generate-cursor-plugin-artifacts.artifacts.test.ts": true,
|