|
@@ -201,15 +201,50 @@ jobs:
|
|
|
if path and not info.get('dev') and not info.get('devOptional'):
|
|
if path and not info.get('dev') and not info.get('devOptional'):
|
|
|
prod.add(path.split('node_modules/')[-1])
|
|
prod.add(path.split('node_modules/')[-1])
|
|
|
vulns = data.get('vulnerabilities', {})
|
|
vulns = data.get('vulnerabilities', {})
|
|
|
|
|
+ # Documented advisory exceptions: high/critical findings whose only offered
|
|
|
|
|
+ # 'fix' is a semver-major change and which do not apply to how Bambuddy ships.
|
|
|
|
|
+ # Keyed by GHSA id; RE-REVIEW ON EVERY react-router BUMP.
|
|
|
|
|
+ # GHSA-qwww-vcr4-c8h2 - React Router RSC-mode CSRF. Bambuddy is a Vite SPA
|
|
|
|
|
+ # using BrowserRouter with no RSC runtime (@react-router/server is NOT
|
|
|
|
|
+ # installed), so the vulnerable code path is unreachable. No non-major fix
|
|
|
|
|
+ # exists (7.18.1 is the most-patched 7.x - it clears 14 other advisories that
|
|
|
|
|
+ # older 7.x carry - and the RSC fix landed only in the 8.3.0 major). react-router
|
|
|
|
|
+ # /-dom are pinned to 7.18.1 in package.json. If a non-major fix ships, this stops
|
|
|
|
|
+ # being exempt (major-only guard below) and the gate fails until we take it.
|
|
|
|
|
+ ALLOWLIST = {'GHSA-qwww-vcr4-c8h2'}
|
|
|
|
|
+ def advisory_ids(name, seen=None):
|
|
|
|
|
+ seen = seen if seen is not None else set()
|
|
|
|
|
+ if name in seen:
|
|
|
|
|
+ return set()
|
|
|
|
|
+ seen.add(name)
|
|
|
|
|
+ ids = set()
|
|
|
|
|
+ for item in vulns.get(name, {}).get('via', []):
|
|
|
|
|
+ if isinstance(item, dict):
|
|
|
|
|
+ url = item.get('url', '')
|
|
|
|
|
+ if '/advisories/' in url:
|
|
|
|
|
+ ids.add(url.rsplit('/', 1)[-1])
|
|
|
|
|
+ elif isinstance(item, str):
|
|
|
|
|
+ ids |= advisory_ids(item, seen)
|
|
|
|
|
+ return ids
|
|
|
|
|
+ def fix_is_major(v):
|
|
|
|
|
+ fa = v.get('fixAvailable')
|
|
|
|
|
+ return isinstance(fa, dict) and fa.get('isSemVerMajor')
|
|
|
|
|
+ def exempt(name, v):
|
|
|
|
|
+ ids = advisory_ids(name)
|
|
|
|
|
+ return bool(ids) and ids <= ALLOWLIST and fix_is_major(v)
|
|
|
fixable = {n: v for n, v in vulns.items()
|
|
fixable = {n: v for n, v in vulns.items()
|
|
|
- if n in prod and v.get('severity') in ('high', 'critical') and v.get('fixAvailable')}
|
|
|
|
|
|
|
+ if n in prod and v.get('severity') in ('high', 'critical')
|
|
|
|
|
+ and v.get('fixAvailable') and not exempt(n, v)}
|
|
|
skipped = len(vulns) - len({n: v for n, v in vulns.items() if n in prod})
|
|
skipped = len(vulns) - len({n: v for n, v in vulns.items() if n in prod})
|
|
|
if fixable:
|
|
if fixable:
|
|
|
for name, v in fixable.items():
|
|
for name, v in fixable.items():
|
|
|
print(f'FIXABLE {v[\"severity\"].upper()}: {name}')
|
|
print(f'FIXABLE {v[\"severity\"].upper()}: {name}')
|
|
|
sys.exit(1)
|
|
sys.exit(1)
|
|
|
total = sum(1 for n, v in vulns.items() if n in prod and v.get('severity') in ('high', 'critical'))
|
|
total = sum(1 for n, v in vulns.items() if n in prod and v.get('severity') in ('high', 'critical'))
|
|
|
|
|
+ exempted = sorted(n for n, v in vulns.items() if n in prod and exempt(n, v))
|
|
|
print(f'npm audit: {total} high/critical (0 fixable), {len(vulns)} total ({skipped} npm-internal filtered)')
|
|
print(f'npm audit: {total} high/critical (0 fixable), {len(vulns)} total ({skipped} npm-internal filtered)')
|
|
|
|
|
+ if exempted:
|
|
|
|
|
+ print('exempted (documented, unreachable): ' + ', '.join(exempted))
|
|
|
"
|
|
"
|
|
|
|
|
|
|
|
frontend-typecheck:
|
|
frontend-typecheck:
|