mirror of
https://github.com/Heretek-AI/GDevelop-Heretek.git
synced 2026-10-12 02:02:42 +00:00
* ci: fix fallow SARIF, upstream-sync token dead end, guard coverage; refresh vuln docs
Four independent defects found while reviewing the open Dependabot queue.
fallow.yml
- `--format` and `--output` are the SAME option (`--output` is a documented
alias of `--format`), so `--format sarif --output fallow.sarif` aborted with
"the argument '--format <FORMAT>' cannot be used multiple times" and never
wrote the file. The following `upload-sarif` step then failed with
"Path does not exist: fallow.sarif", reddening `Fallow full scan` on master
and on every PR. Use `--output-file` for the path.
- Pin the CLI to 3.27.0. A bare `npx fallow` resolves the `latest` dist-tag at
run time and was observed returning 3.3.0 on one run and 3.27.0 on another,
so the full scan could silently disagree with the `audit` job (which pins
3.27.0 through the action's `version:` input).
upstream-sync.yml
- Org policy forces a read-only GITHUB_TOKEN and no SYNC_PAT secret exists, so
`gh pr create` can never succeed ("Resource not accessible by integration")
and the run failed with exit 1 every day. The branch is already pushed by
that point, so degrade to a warning plus a ready-to-click compare URL in the
step summary instead of failing.
check-lockfiles.js
- `newIDE/visual-tests/package-lock.json` existed on disk but was absent from
the guard's list (10 lockfiles, 9 guarded). Added.
dependabot.yml
- Drop the `ignore:` block pinning electron to 32.x. Both manifests are now on
the current release line (newIDE/electron-app 44.4.3, GDJS/Runtime/Electron
44.3.0), so the block only suppressed legitimate updates.
- `exclude-patterns` on the newIDE/app weekly group: for 0.x packages a
"minor" bump is breaking by semver convention, so the group silently carried
migrations like three 0.160->0.186 and spine-pixi-v7 4.2->4.3 alongside real
patch bumps. One such group PR grew into a 19-package migration.
- Raise newIDE/app's `open-pull-requests-limit` 5 -> 10; 6 PRs are already
open there, so new ones were being suppressed at the source.
KNOWN_VULNS.md
- Rewritten against the live API and the manifests. The previous revision
claimed the fork stays on electron 32.x with 47 advisories (it is on 44.4.3),
and that all non-electron vulnerabilities were remediated (the API reports
49 open alerts: 22 high, 25 medium, 2 low, across 18 packages).
- Documented two guard gaps: the `tar` floor (`< 6.2.1`) is bypassed by the
guard's same-major rule while advisories cover `<= 7.5.17`, and
newIDE/visual-tests was unguarded.
- Recorded the four no-patch advisories as accepted risk.
DEPENDABOT_PR_REMEDIATION_PLAN.md
- Full review of the 32 open PRs: 6 root causes, all reproduced locally, with
fixes verified before being proposed.
* fix(deps): repair three lockfiles already desynced on master; gate lockfile sync in CI
`npm ci` hard-fails when package.json and package-lock.json disagree, but CI
only installed the directories some later job happened to need. Three lockfiles
had rotted on master without anything noticing:
GDJS 16 missing packages (@pixi/* 7.4.3)
newIDE/electron-app/app 6 missing + 3 invalid (electron 44.4.3, @electron/get)
SharedLibs/TileMapHelper 36 missing + 2 invalid (@webassemblyjs/*, terser)
Each only failed the moment someone tried to install in that directory — which
is how the GDJS one stayed invisible while PR #121 was being reviewed for an
unrelated reason.
Regenerated all three with `npm install --package-lock-only`. All ten manifests
now install clean under `npm ci --dry-run`:
GDevelop.js GDJS GDJS/tests newIDE/app newIDE/electron-app
newIDE/electron-app/app newIDE/web-app newIDE/visual-tests
SharedLibs/TileMapHelper SharedLibs/ThreeAddons -> rc=0, 0 sync errors
Added a `lockfile-sync` matrix job over all ten directories so drift fails on
the PR that causes it. `--ignore-scripts` keeps postinstall hooks
(patch-package, import-resources) out of it — the job only asserts that the
lockfile resolves.
`node scripts/security/check-lockfiles.js` still exits 0.
* fix(deps): bump adm-zip 0.6.0 -> 0.6.1, closing 2 high advisories
`[email protected]` is vulnerable to GHSA-7q85-xj36-vmfc (high, patched in 0.6.1).
It is a *direct* dependency declared as `^0.6.0` in both manifests, so 0.6.1 was
already permitted — the lockfiles were simply pinned at the vulnerable 0.6.0 and
never re-resolved.
Verified the overrides already present in both manifests (`^0.6.0`) are harmless
here: a fresh resolution with or without them yields 0.6.1. Raising the override
to `^0.6.1` is NOT the fix — npm rejects it with EOVERRIDE because the override
would conflict with the direct dependency. Regenerating is the correct fix.
newIDE/app/package-lock.json adm-zip 0.6.0 -> 0.6.1
newIDE/electron-app/package-lock.json adm-zip 0.6.0 -> 0.6.1
Closes 2 of the 22 open high-severity alerts. The remaining `adm-zip` medium
advisory (GHSA-vwc7-r8mq-g2x9) has no patch and stays documented as accepted
risk in KNOWN_VULNS.md.
All ten manifests verified: `npm ci --dry-run` rc=0, 0 sync errors, and
`node scripts/security/check-lockfiles.js` exits 0.
* docs: record execution status for the PR remediation plan
Queue 32 -> 25 open; 8 merged. Adds the evidence used to justify each merge,
the repo fixes landed in PR #124, the three master lockfiles found already
desynced (GDJS, newIDE/electron-app/app, SharedLibs/TileMapHelper) and the
adm-zip 0.6.0 -> 0.6.1 bump that closed 2 high advisories.
Also records the rebase mechanics blocker: PRs 18/26/37/39/40/104 do not move
on '@dependabot rebase' because they were edited by hand; only #40 reports it.
* Merge master into chore/ci-remediation
Master advanced 10 merges (our own dependency PRs), so the lockfiles this
branch regenerated were stale against it. The merge reverted
newIDE/electron-app/package-lock.json to master's pre-regeneration copy;
regenerated it again (adm-zip stays 0.6.1).
All ten manifests verified in the merged state: npm ci --dry-run rc=0,
0 sync errors, guard exits 0.
* ci: run lockfile-sync as one job, not a 10-way matrix
Each matrix entry was a dry-run resolve taking a few seconds, so the 10-way
matrix bought ~30s of wall clock at the cost of 10 runners. The queue
contention that produced was starving the slower build-libgd and Jest jobs,
which dominate the workflow anyway.
Now a single job loops the same ten directories, continues past a failure so
one run reports every drifted manifest, and exits 1 at the end. Verified both
paths locally: on a good tree it prints "All lockfiles in sync." and exits 0;
with one drifted manifest it prints the per-directory ❌ with the npm
Missing:/Invalid: lines, keeps checking the rest, and exits 1.
192 lines
9.1 KiB
JavaScript
192 lines
9.1 KiB
JavaScript
#!/usr/bin/env node
|
|
/*
|
|
* Regression guard: asserts that no manifest in the repo pins a known-malicious
|
|
* or critically-vulnerable version of a curated list of npm packages.
|
|
*
|
|
* Add new advisories to VULNERABLE_VERSIONS / VULNERABLE_RANGES below as they
|
|
* come in. The script exits 0 when everything is clean and 1 when at least one
|
|
* manifest has a bad version — wire it into CI so a regression reintroducing
|
|
* an old package version fails the build.
|
|
*
|
|
* Usage:
|
|
* node scripts/security/check-lockfiles.js
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const repoRoot = path.join(__dirname, '..', '..');
|
|
|
|
// (package, [list of "pinned version" strings considered vulnerable])
|
|
// Exact-match: we compare the "version" field in lockfileVersion 2/3 packages,
|
|
// or the "version" field in lockfileVersion 1 dependencies tree.
|
|
const VULNERABLE_VERSIONS = {
|
|
fsevents: ['1.2.4'], // GHSA-xv2f-5jw4-v95m (malware)
|
|
'socket.io-parser': ['3.3.0', '3.2.0'], // ReDoS / prototype pollution
|
|
'xmlhttprequest-ssl': ['1.6.2'], // prototype pollution
|
|
'set-value': ['3.0.0', '3.0.1'], // prototype pollution
|
|
'mixin-deep': ['1.3.1'], // prototype pollution
|
|
growl: ['1.10.0', '1.10.3'], // command injection (1.10.5 fixed)
|
|
};
|
|
|
|
// (package, [list of "X.Y.Z" lower-bound comparisons]).
|
|
// Currently only "< M.m.p" form is supported.
|
|
const VULNERABLE_RANGES = {
|
|
// Critical / malware / high
|
|
minimist: ['< 1.2.6'], // GHSA-xvch-5gv4-984h
|
|
'gh-pages': ['< 5.0.0'], // GHSA-8mmm-9v2q-x3f9
|
|
lodash: ['< 4.17.21'], // command injection + prototype pollution
|
|
'brace-expansion': ['< 2.0.2'], // ReDoS in 1.x / 2.0.1
|
|
tar: ['< 6.2.1'], // various CVEs
|
|
shelljs: ['< 0.9.0'], // privilege management
|
|
'js-yaml': ['< 4.3.1'], // safeLoad removal in 3.x, quadratic DoS
|
|
'follow-redirects': ['< 1.15.0'], // GHSA-74fj-2j2h-c42q
|
|
async: ['< 3.0.0'], // prototype pollution in 2.x
|
|
minimatch: ['< 5.1.0'], // ReDoS in 3.x
|
|
'shell-quote': ['< 1.8.4'], // GHSA-w7jw-789q-3m8p
|
|
handlebars: ['< 4.7.9'], // GHSA-2699-9764-gw49
|
|
ejs: ['< 3.1.10'], // GHSA-x2rg-2cr9-2m93
|
|
// High-volume / medium-severity
|
|
axios: ['< 1.7.0'], // prototype pollution, SSRF, etc.
|
|
'electron-updater': ['< 6.6.0'], // cross-origin redirect leaks
|
|
'extract-zip': ['< 2.0.0'], // unvalidated symlink path traversal
|
|
'form-data': ['< 4.0.6'], // CRLF injection in <4.0.6
|
|
'@xmldom/xmldom': ['< 0.9.0'], // prototype pollution
|
|
undici: ['< 6.0.0'], // GHSA-cxrh-jh5x-9p9g etc.
|
|
'fast-uri': ['< 3.1.5'], // ReDoS
|
|
bodyparser: ['< 1.20.3'], // DoS
|
|
braces: ['< 3.0.3'], // ReDoS in 2.x
|
|
picomatch: ['< 3.0.0'], // ReDoS in 2.x
|
|
cookie: ['< 0.7.0'], // GHSA-pxg6-pf52-xh8x
|
|
qs: ['< 6.15.3'], // GHSA-q8mj-m7cp-5q26
|
|
tmp: ['< 0.2.7'], // GHSA-52f5-j9jc-3726
|
|
ws: ['< 7.5.10'], // GHSA-3h5v-q93c-6h6q
|
|
diff: ['< 5.2.0'], // ReDoS in <5
|
|
'http-proxy': ['< 1.18.1'], // GHSA-6x8p-c9mf-4wrg
|
|
debug: ['< 2.6.9'], // ReDoS in <2.6.9
|
|
chownr: ['< 1.1.4'], // TOCTOU
|
|
ini: ['< 1.3.8'], // ReDoS in <1.3.8
|
|
decodeuricomponent: ['< 0.2.2'], // GHSA-w7cr-mhqq-3fmw
|
|
esbuild: ['< 0.25.0'], // GHSA-67mh-4wv8-vw99 (dev-only)
|
|
'adm-zip': ['< 0.6.0'], // GHSA-4gg4-h6c6-9cw9
|
|
'ip-address': ['< 10.5.0'], // GHSA-3k65-mwh4-6c84
|
|
'node-forge': ['< 1.3.1'], // CVE-2022-24771 etc
|
|
rollup: ['< 2.79.2'], // GHSA-gcx4-mw6x-p8f3
|
|
piscina: ['< 4.8.0'], // CVE-2024-52804
|
|
// npm audit failures surfaced during CI integration (2026-08-20)
|
|
'node-fetch': ['< 2.6.7'], // GHSA-r683-j2x4-v87g (header leak)
|
|
'@grpc/grpc-js': ['< 1.14.4'], // GHSA-99f4-grh7-6pcq etc.
|
|
protobufjs: ['< 7.6.5'], // GHSA-xq3m-2v4x-88gg etc.
|
|
'websocket-driver': ['< 0.7.5'], // GHSA-mp7j-qc5w-4988 etc.
|
|
semver: ['< 7.5.2'], // GHSA-c2qf-rxjj-qqgw (ReDoS)
|
|
uuid: ['< 11.1.1'], // GHSA-w5hq-g745-h8pq (buffer bounds)
|
|
yaml: ['< 2.8.3'], // GHSA-48c2-rrv3-qjmp (stack overflow)
|
|
'@protobufjs/utf8': ['< 1.1.2'], // GHSA-q6x5-8v7m-xcrf (overlong UTF-8)
|
|
};
|
|
|
|
const lockfiles = [
|
|
'GDevelop.js/package-lock.json',
|
|
'GDJS/package-lock.json',
|
|
'GDJS/tests/package-lock.json',
|
|
'newIDE/app/package-lock.json',
|
|
'newIDE/electron-app/package-lock.json',
|
|
'newIDE/electron-app/app/package-lock.json',
|
|
'newIDE/web-app/package-lock.json',
|
|
'newIDE/visual-tests/package-lock.json',
|
|
'SharedLibs/TileMapHelper/package-lock.json',
|
|
'SharedLibs/ThreeAddons/package-lock.json',
|
|
];
|
|
|
|
function readLockfile(relPath) {
|
|
const abs = path.join(repoRoot, relPath);
|
|
if (!fs.existsSync(abs)) return null;
|
|
return JSON.parse(fs.readFileSync(abs, 'utf8'));
|
|
}
|
|
|
|
function collectPinnedVersions(lockfileJson) {
|
|
const pinned = new Map(); // package -> Set<version>
|
|
if (lockfileJson.packages) {
|
|
for (const [pkgPath, info] of Object.entries(lockfileJson.packages)) {
|
|
if (!pkgPath || !pkgPath.startsWith('node_modules/')) continue;
|
|
// Take the segment after the final "node_modules/" so that nested
|
|
// installs (e.g. "node_modules/@scope/a/node_modules/b") are keyed
|
|
// by the inner package "b", not the path-prefixed string.
|
|
const pkgName = pkgPath.split('node_modules/').pop();
|
|
if (!info || !info.version) continue;
|
|
if (!pinned.has(pkgName)) pinned.set(pkgName, new Set());
|
|
pinned.get(pkgName).add(info.version);
|
|
}
|
|
} else if (lockfileJson.dependencies) {
|
|
// lockfileVersion 1: walk the dependency tree, keying each entry by
|
|
// its own package name (not the parent's path) so that nested
|
|
// transitive deps with the same name aggregate into one bucket.
|
|
const walk = (deps) => {
|
|
for (const [name, info] of Object.entries(deps || {})) {
|
|
if (info.version) {
|
|
if (!pinned.has(name)) pinned.set(name, new Set());
|
|
pinned.get(name).add(info.version);
|
|
}
|
|
if (info.dependencies) walk(info.dependencies);
|
|
}
|
|
};
|
|
walk(lockfileJson.dependencies);
|
|
}
|
|
return pinned;
|
|
}
|
|
|
|
function rangeMatches(version, range) {
|
|
// Tiny semver "< M.m.p" matcher. Treats pre-release tags (e.g. 1.2.3-beta.1)
|
|
// as equal to 1.2.3 — good enough for our advisory-floor use case.
|
|
//
|
|
// Security advisories apply to a specific major-version line, so a range
|
|
// "< 2.8.3" should NOT match versions in the 1.x line (different package,
|
|
// different advisory list). We require same-major before comparing.
|
|
const m = range.match(/^<\s*(\d+)\.(\d+)\.(\d+)$/);
|
|
if (!m) return false;
|
|
const vParts = version.split('-')[0].split('.').map(Number);
|
|
if (vParts.length !== 3 || vParts.some(Number.isNaN)) return false;
|
|
const [vMaj, vMin, vPat] = vParts;
|
|
const [tMaj, tMin, tPat] = [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
if (vMaj !== tMaj) return false;
|
|
if (vMin !== tMin) return vMin < tMin;
|
|
return vPat < tPat;
|
|
}
|
|
|
|
const failures = [];
|
|
for (const relPath of lockfiles) {
|
|
const json = readLockfile(relPath);
|
|
if (!json) {
|
|
failures.push(`${relPath}: lockfile missing`);
|
|
continue;
|
|
}
|
|
const pinned = collectPinnedVersions(json);
|
|
|
|
for (const [pkg, badVersions] of Object.entries(VULNERABLE_VERSIONS)) {
|
|
const versions = pinned.get(pkg);
|
|
if (!versions) continue;
|
|
for (const v of versions) {
|
|
if (badVersions.includes(v)) {
|
|
failures.push(`${relPath}: ${pkg}@${v} is in VULNERABLE_VERSIONS`);
|
|
}
|
|
}
|
|
}
|
|
for (const [pkg, ranges] of Object.entries(VULNERABLE_RANGES)) {
|
|
const versions = pinned.get(pkg);
|
|
if (!versions) continue;
|
|
for (const v of versions) {
|
|
for (const range of ranges) {
|
|
if (rangeMatches(v, range)) {
|
|
failures.push(`${relPath}: ${pkg}@${v} matches ${range}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (failures.length) {
|
|
console.error('❌ Vulnerable lockfile pin(s) detected:');
|
|
for (const f of failures) console.error(' ' + f);
|
|
process.exit(1);
|
|
}
|
|
console.log('✅ All lockfiles free of the pinned vulnerable versions in this guard.');
|