@codyswann/lisa 2.317.6 → 2.318.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-contents/gitignore +16 -0
- package/cdk/package-lisa/package.lisa.json +2 -2
- package/dist/configs/eslint/base.d.ts.map +1 -1
- package/dist/configs/eslint/base.js +5 -0
- package/dist/configs/eslint/base.js.map +1 -1
- package/dist/configs/jest/base.d.ts +12 -7
- package/dist/configs/jest/base.d.ts.map +1 -1
- package/dist/configs/jest/base.js +14 -9
- package/dist/configs/jest/base.js.map +1 -1
- package/dist/configs/vitest/base.d.ts +14 -8
- package/dist/configs/vitest/base.d.ts.map +1 -1
- package/dist/configs/vitest/base.js +18 -10
- package/dist/configs/vitest/base.js.map +1 -1
- package/dist/configs/worktrees.d.ts +39 -0
- package/dist/configs/worktrees.d.ts.map +1 -0
- package/dist/configs/worktrees.js +44 -0
- package/dist/configs/worktrees.js.map +1 -0
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +12 -6
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/expo/package-lisa/package.lisa.json +5 -5
- package/nestjs/package-lisa/package.lisa.json +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
- package/scripts/check-security-floors.mjs +285 -0
- package/typescript/copy-overwrite/eslint.ignore.config.json +1 -0
- package/typescript/package-lisa/package.lisa.json +6 -6
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Verify no force-pinned version floor permits a known-vulnerable release.
|
|
4
|
+
*
|
|
5
|
+
* A floor in a `force` section does not merely fail to protect when it goes
|
|
6
|
+
* stale — it OVERWRITES whatever the downstream project had. A project that
|
|
7
|
+
* pinned correctly gets moved back onto the vulnerable range by `lisa apply`,
|
|
8
|
+
* silently, and the only signal is a suspicious line in a diff nobody is
|
|
9
|
+
* looking for. That is how `brace-expansion >=5.0.7` survived: it was written
|
|
10
|
+
* to match GHSA-3jxr-9vmj-r5cp, whose first_patched_version genuinely is
|
|
11
|
+
* 5.0.7, and GHSA-mh99-v99m-4gvg later moved the floor to 5.0.8.
|
|
12
|
+
*
|
|
13
|
+
* The floors are therefore checked against the advisory database rather than
|
|
14
|
+
* against what npm happens to resolve. Resolution drifts as new advisories
|
|
15
|
+
* land; `first_patched_version` does not. Deriving floors from resolution is
|
|
16
|
+
* the specific mistake this exists to catch — it produced the same defect
|
|
17
|
+
* three times (fast-uri, js-yaml, and the three fixed in #2168).
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* check-security-floors.mjs [--strict] [--json]
|
|
21
|
+
*
|
|
22
|
+
* Exits non-zero only with --strict, so the job can report before it gates.
|
|
23
|
+
* @module scripts/check-security-floors
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { readFileSync } from "node:fs";
|
|
27
|
+
import { globSync } from "node:fs";
|
|
28
|
+
|
|
29
|
+
/** Sections whose entries are version constraints worth checking. */
|
|
30
|
+
const CONSTRAINT_SECTIONS = [
|
|
31
|
+
"overrides",
|
|
32
|
+
"resolutions",
|
|
33
|
+
"dependencies",
|
|
34
|
+
"devDependencies",
|
|
35
|
+
"peerDependencies",
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
/** Governance groups in a package.lisa.json. */
|
|
39
|
+
const GOVERNANCE_GROUPS = ["force", "defaults", "merge"];
|
|
40
|
+
|
|
41
|
+
/** Severities that gate. Medium and low advisories are ignored by this check. */
|
|
42
|
+
const GATING_SEVERITIES = new Set(["high", "critical"]);
|
|
43
|
+
|
|
44
|
+
const ADVISORY_ENDPOINT = "https://api.github.com/advisories";
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Every version constraint declared across the template manifests.
|
|
48
|
+
* @returns {Map<string, Array<{file: string, path: string, spec: string}>>} By package name.
|
|
49
|
+
*/
|
|
50
|
+
function collectFloors() {
|
|
51
|
+
const found = new Map();
|
|
52
|
+
const files = globSync("*/package-lisa/package.lisa.json").filter(
|
|
53
|
+
file => !file.includes("node_modules") && !file.includes(".worktrees")
|
|
54
|
+
);
|
|
55
|
+
for (const file of files) {
|
|
56
|
+
let manifest;
|
|
57
|
+
try {
|
|
58
|
+
manifest = JSON.parse(readFileSync(file, "utf8"));
|
|
59
|
+
} catch (error) {
|
|
60
|
+
throw new Error(`${file} is not valid JSON: ${error.message}`);
|
|
61
|
+
}
|
|
62
|
+
for (const group of GOVERNANCE_GROUPS) {
|
|
63
|
+
const block = manifest[group];
|
|
64
|
+
if (!block || typeof block !== "object") continue;
|
|
65
|
+
for (const section of CONSTRAINT_SECTIONS) {
|
|
66
|
+
const entries = block[section];
|
|
67
|
+
if (!entries || typeof entries !== "object") continue;
|
|
68
|
+
for (const [name, spec] of Object.entries(entries)) {
|
|
69
|
+
// `$name` self-references defer to the project's own pin and carry
|
|
70
|
+
// no floor of their own; a literal like "workspace:*" likewise.
|
|
71
|
+
if (typeof spec !== "string" || !/\d/.test(spec)) continue;
|
|
72
|
+
if (spec.startsWith("$")) continue;
|
|
73
|
+
if (!found.has(name)) found.set(name, []);
|
|
74
|
+
found.get(name).push({ file, path: `${group}.${section}`, spec });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return found;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Lowest version a spec permits, as a comparable tuple.
|
|
84
|
+
* @param {string} spec A range like ">=5.0.7", "^1.2.3", "~2.0.0".
|
|
85
|
+
* @returns {number[]|null} [major, minor, patch], or null if unparseable.
|
|
86
|
+
*/
|
|
87
|
+
export function lowestPermitted(spec) {
|
|
88
|
+
const match = /(\d+)\.(\d+)\.(\d+)/.exec(spec);
|
|
89
|
+
if (!match) return null;
|
|
90
|
+
return match.slice(1, 4).map(Number);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Compare two version tuples.
|
|
95
|
+
* @param {number[]} left First version.
|
|
96
|
+
* @param {number[]} right Second version.
|
|
97
|
+
* @returns {number} Negative, zero, or positive.
|
|
98
|
+
*/
|
|
99
|
+
function compare(left, right) {
|
|
100
|
+
for (let index = 0; index < 3; index += 1) {
|
|
101
|
+
if (left[index] !== right[index]) return left[index] - right[index];
|
|
102
|
+
}
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Whether a version falls inside an advisory's vulnerable range.
|
|
108
|
+
* @param {number[]} version Candidate version.
|
|
109
|
+
* @param {string} range e.g. ">= 4.0.0, < 5.0.8".
|
|
110
|
+
* @returns {boolean} True when the version is vulnerable.
|
|
111
|
+
*/
|
|
112
|
+
export function withinRange(version, range) {
|
|
113
|
+
if (!range) return false;
|
|
114
|
+
let inside = true;
|
|
115
|
+
for (const clause of range.split(",")) {
|
|
116
|
+
const match = /(>=|<=|<|>|=)\s*(\d+)\.(\d+)\.(\d+)/.exec(clause.trim());
|
|
117
|
+
if (!match) continue;
|
|
118
|
+
const bound = match.slice(2, 5).map(Number);
|
|
119
|
+
const delta = compare(version, bound);
|
|
120
|
+
switch (match[1]) {
|
|
121
|
+
case ">=":
|
|
122
|
+
inside &&= delta >= 0;
|
|
123
|
+
break;
|
|
124
|
+
case ">":
|
|
125
|
+
inside &&= delta > 0;
|
|
126
|
+
break;
|
|
127
|
+
case "<=":
|
|
128
|
+
inside &&= delta <= 0;
|
|
129
|
+
break;
|
|
130
|
+
case "<":
|
|
131
|
+
inside &&= delta < 0;
|
|
132
|
+
break;
|
|
133
|
+
default:
|
|
134
|
+
inside &&= delta === 0;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return inside;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Advisories affecting one package.
|
|
142
|
+
*
|
|
143
|
+
* Distinguishes "no advisories" from "could not ask", because conflating them
|
|
144
|
+
* is how a check like this goes quietly blind. The anonymous advisory limit is
|
|
145
|
+
* 60 requests/hour and these manifests declare roughly 200 packages, so an
|
|
146
|
+
* unauthenticated run answers for the first 60 and silently reports the other
|
|
147
|
+
* 140 as clean unless the difference is tracked.
|
|
148
|
+
* @param {string} name npm package name.
|
|
149
|
+
* @returns {Promise<{advisories: Array}|{error: string}>} Result or reason.
|
|
150
|
+
*/
|
|
151
|
+
async function advisoriesFor(name) {
|
|
152
|
+
const url = `${ADVISORY_ENDPOINT}?ecosystem=npm&affects=${encodeURIComponent(name)}&per_page=100`;
|
|
153
|
+
const headers = { accept: "application/vnd.github+json" };
|
|
154
|
+
// Authenticated calls get 5000/hour instead of 60. CI supplies the token via
|
|
155
|
+
// the standard GITHUB_TOKEN; locally, `gh auth token` covers it.
|
|
156
|
+
const token = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN;
|
|
157
|
+
if (token) headers.authorization = `Bearer ${token}`;
|
|
158
|
+
|
|
159
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
160
|
+
let response;
|
|
161
|
+
try {
|
|
162
|
+
response = await fetch(url, { headers });
|
|
163
|
+
} catch (error) {
|
|
164
|
+
return { error: `network: ${error.message}` };
|
|
165
|
+
}
|
|
166
|
+
if (response.ok) return { advisories: await response.json() };
|
|
167
|
+
|
|
168
|
+
const remaining = response.headers.get("x-ratelimit-remaining");
|
|
169
|
+
const limited =
|
|
170
|
+
response.status === 429 || (response.status === 403 && remaining === "0");
|
|
171
|
+
if (!limited) return { error: `HTTP ${response.status}` };
|
|
172
|
+
|
|
173
|
+
// Honour Retry-After when given, otherwise back off. A rate-limited run
|
|
174
|
+
// cannot demonstrate anything, so it is worth waiting rather than
|
|
175
|
+
// reporting a clean sheet that was never checked.
|
|
176
|
+
const retryAfter = Number(response.headers.get("retry-after"));
|
|
177
|
+
const waitSeconds =
|
|
178
|
+
Number.isFinite(retryAfter) && retryAfter > 0
|
|
179
|
+
? Math.min(retryAfter, 30)
|
|
180
|
+
: 2 ** attempt;
|
|
181
|
+
await new Promise(resolve => setTimeout(resolve, waitSeconds * 1000));
|
|
182
|
+
}
|
|
183
|
+
return { error: "rate limited" };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Audit every collected floor.
|
|
188
|
+
* @returns {Promise<{problems: Array, unreachable: string[], checked: number}>} Findings.
|
|
189
|
+
*/
|
|
190
|
+
async function audit() {
|
|
191
|
+
const floors = collectFloors();
|
|
192
|
+
const problems = [];
|
|
193
|
+
const unreachable = [];
|
|
194
|
+
for (const [name, sites] of floors) {
|
|
195
|
+
const result = await advisoriesFor(name);
|
|
196
|
+
if (result.error) {
|
|
197
|
+
unreachable.push({ name, reason: result.error });
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const { advisories } = result;
|
|
201
|
+
for (const advisory of advisories) {
|
|
202
|
+
if (!GATING_SEVERITIES.has(advisory.severity)) continue;
|
|
203
|
+
for (const vulnerability of advisory.vulnerabilities ?? []) {
|
|
204
|
+
if (vulnerability.package?.name !== name) continue;
|
|
205
|
+
for (const site of sites) {
|
|
206
|
+
const lowest = lowestPermitted(site.spec);
|
|
207
|
+
if (!lowest) continue;
|
|
208
|
+
if (!withinRange(lowest, vulnerability.vulnerable_version_range)) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
problems.push({
|
|
212
|
+
package: name,
|
|
213
|
+
...site,
|
|
214
|
+
advisory: advisory.ghsa_id,
|
|
215
|
+
severity: advisory.severity,
|
|
216
|
+
vulnerableRange: vulnerability.vulnerable_version_range,
|
|
217
|
+
patched: vulnerability.first_patched_version,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return { problems, unreachable, checked: floors.size };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function main() {
|
|
227
|
+
const strict = process.argv.includes("--strict");
|
|
228
|
+
const asJson = process.argv.includes("--json");
|
|
229
|
+
const { problems, unreachable, checked } = await audit();
|
|
230
|
+
|
|
231
|
+
if (asJson) {
|
|
232
|
+
console.log(JSON.stringify({ problems, unreachable, checked }, null, 2));
|
|
233
|
+
} else if (problems.length === 0) {
|
|
234
|
+
console.log(
|
|
235
|
+
`## Security floors\n\nNo force-pinned floor permits a high or critical vulnerable release. ${checked} package(s) checked.`
|
|
236
|
+
);
|
|
237
|
+
} else {
|
|
238
|
+
console.log("## Security floors\n");
|
|
239
|
+
console.log(
|
|
240
|
+
"These pins permit a release the advisory database marks vulnerable. Because they sit in governance sections, a stale floor **overwrites** a downstream project that pinned correctly.\n"
|
|
241
|
+
);
|
|
242
|
+
console.log("| package | pinned | permits | advisory | patched | site |");
|
|
243
|
+
console.log("|---|---|---|---|---|---|");
|
|
244
|
+
for (const problem of problems) {
|
|
245
|
+
console.log(
|
|
246
|
+
`| \`${problem.package}\` | \`${problem.spec}\` | ${problem.vulnerableRange} | ${problem.advisory} (${problem.severity}) | **${problem.patched}** | ${problem.file} ${problem.path} |`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
console.log(
|
|
250
|
+
"\nRaise each floor to the advisory's `first_patched_version`, then confirm the new value actually resolves on npm — a floor nothing satisfies is its own breakage."
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (unreachable.length > 0) {
|
|
255
|
+
const limited = unreachable.filter(entry =>
|
|
256
|
+
entry.reason.includes("rate limited")
|
|
257
|
+
);
|
|
258
|
+
console.log(
|
|
259
|
+
`\n> **Inconclusive for ${unreachable.length} of ${checked} package(s).** Their floors were NOT verified — this is not a clean result for them.`
|
|
260
|
+
);
|
|
261
|
+
if (limited.length > 0) {
|
|
262
|
+
console.log(
|
|
263
|
+
"> Cause is rate limiting. The anonymous advisory limit is 60 requests/hour; set `GITHUB_TOKEN` for 5000."
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
console.log(
|
|
267
|
+
`> Affected: ${unreachable.map(entry => entry.name).join(", ")}`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Deliberately exit 0 without --strict. An advisory landing overnight would
|
|
272
|
+
// otherwise fail a PR that changed nothing, and a check that cries wolf gets
|
|
273
|
+
// disabled rather than fixed.
|
|
274
|
+
//
|
|
275
|
+
// Under --strict an inconclusive run also fails: it proves nothing, and
|
|
276
|
+
// treating "could not check" as "checked and clean" is exactly the silent
|
|
277
|
+
// degradation this script exists to prevent.
|
|
278
|
+
if (strict && (problems.length > 0 || unreachable.length > 0)) {
|
|
279
|
+
process.exitCode = 1;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (import.meta.main) {
|
|
284
|
+
await main();
|
|
285
|
+
}
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
},
|
|
29
29
|
"resolutions": {
|
|
30
30
|
"@isaacs/brace-expansion": "^5.0.1",
|
|
31
|
-
"brace-expansion": ">=5.0.
|
|
32
|
-
"axios": ">=1.
|
|
31
|
+
"brace-expansion": ">=5.0.8",
|
|
32
|
+
"axios": ">=1.18.0",
|
|
33
33
|
"esbuild": ">=0.28.1",
|
|
34
34
|
"handlebars": ">=4.7.9",
|
|
35
35
|
"lodash": ">=4.18.1",
|
|
@@ -38,12 +38,12 @@
|
|
|
38
38
|
"vite": ">=8.0.16",
|
|
39
39
|
"ws": ">=8.21.0",
|
|
40
40
|
"form-data": ">=4.0.6",
|
|
41
|
-
"systeminformation": ">=5.
|
|
41
|
+
"systeminformation": ">=5.31.7"
|
|
42
42
|
},
|
|
43
43
|
"overrides": {
|
|
44
44
|
"@isaacs/brace-expansion": "^5.0.1",
|
|
45
|
-
"brace-expansion": ">=5.0.
|
|
46
|
-
"axios": ">=1.
|
|
45
|
+
"brace-expansion": ">=5.0.8",
|
|
46
|
+
"axios": ">=1.18.0",
|
|
47
47
|
"esbuild": ">=0.28.1",
|
|
48
48
|
"handlebars": ">=4.7.9",
|
|
49
49
|
"lodash": ">=4.18.1",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"vite": "^8.0.16",
|
|
53
53
|
"ws": ">=8.21.0",
|
|
54
54
|
"form-data": ">=4.0.6",
|
|
55
|
-
"systeminformation": ">=5.
|
|
55
|
+
"systeminformation": ">=5.31.7"
|
|
56
56
|
}
|
|
57
57
|
},
|
|
58
58
|
"defaults": {
|