Skip to content

fix(deps): remediate advisories and make the dependency audit blocking - #44

Draft
MaazAhmed47 wants to merge 5 commits into
mainfrom
codex/dependency-audit-enforcement
Draft

fix(deps): remediate advisories and make the dependency audit blocking#44
MaazAhmed47 wants to merge 5 commits into
mainfrom
codex/dependency-audit-enforcement

Conversation

@MaazAhmed47

@MaazAhmed47 MaazAhmed47 commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Remediates every reproducible dependency advisory and makes the Dependency audit job's green state mean something. Dependency pins, CI policy, and tests
only — no application code changed.

The problem

The Dependency audit job was green while a real advisory was present, and it
never looked at the frontend at all:

- name: Audit dependencies (pip-audit, report-only)
  continue-on-error: true          # <- job stays green no matter what
  run: pip-audit --progress-spinner off
  • continue-on-error: true meant pip-audit exiting 1 did not fail the job.
  • The job installed no Node dependencies and ran no npm audit, so three npm
    advisories — including a high — were invisible while the job presented
    itself as a general "Dependency audit".
  • No job-level permissions, so it inherited more than it needed.

Advisories found (baseline at 9b34a17)

Package Installed Kind Scope Sev Advisory Vulnerable Patched
pytest 8.4.2 direct test moderate (6.8) PYSEC-2026-1845 / CVE-2025-71176 / GHSA-6w46-j5rx-g56g ≤9.0.2 9.0.3
postcss 8.5.15 transitive dev/build high (7.5) GHSA-r28c-9q8g-f849 ≤8.5.17 8.5.18
react-router 6.30.4 transitive production moderate (5.1) GHSA-wrjc-x8rr-h8h6 ≥6.0.0 <7.18.0 7.18.0
react-router 6.30.4 transitive production moderate (6.1) GHSA-337j-9hxr-rhxg ≥6.4.0 <7.18.0 7.18.0
react-router-dom 6.30.4 direct production moderate (6.9) GHSA-jjmj-jmhj-qwj2 ≥6.30.2 ≤6.30.4 none on 6.x

Dependency paths:

interlock-web@0.2.0-alpha.1 -> vite@6.4.3 -> postcss@8.5.15
interlock-web@0.2.0-alpha.1 -> react-router-dom@6.30.4 -> react-router@6.30.4

Reachability:

  • pytest — CI/test only; /tmp/pytest-of-{user} handling allows local DoS
    or privilege escalation on shared UNIX runners. Does not ship to browsers.
    Build-tool exposure is real, so it is fixed rather than waived.
  • postcss — executes during vite build in CI; does not ship to browsers.
    Arbitrary .map disclosure via sourceMappingURL path traversal.
  • react-router / react-router-dom — ships to browsers.

Remediation (smallest compatible upgrades)

Change From → To Type
requirements.txt: pytest 8.4.2 → 9.0.3 pinned to the first patched release
lockfile: postcss 8.5.15 → 8.5.25 patch, via npm audit fix (no --force)
lockfile: nanoid (postcss dep) 3.3.12 → 3.3.16 patch, pulled in by the above

The lockfile diff moves exactly those two packages and nothing else.
package.json is unchanged, npm ci stays deterministic, and no lockfile edit
was made by hand. npm audit fix --force was not used.

After remediation: pip-audit reports "No known vulnerabilities found"
(exit 0)
and the npm high advisory is gone.

The three advisories that remain, and why

react-router 6.x has no fix. GitHub records first_patched_version: none for
react-router-dom 6.x on GHSA-jjmj-jmhj-qwj2; the other two are patched only in
7.18.0. Removing them requires the react-router v7 major migration,
which is a framework migration rather than a dependency-security change — and it
would land directly on the dashboard routing that shipped in #43. That belongs
in its own PR with its own routing verification.

They are therefore accepted as narrow, owned, expiring exceptions in
.github/dependency-audit-policy.json, each backed by a control that exists:

  • GHSA-jjmj-jmhj-qwj2 / GHSA-wrjc-x8rr-h8h6 (open redirect). The only
    attacker-influenced navigation sink is navigate(returnTo) in
    pages/OIDCCallback.tsx. Every producer of returnTo passes it through
    sanitizeReturnTo on both write and read, which requires the literal
    /dashboard prefix — so an authority-position payload (//host, /\host)
    can never reach the router. Pinned by
    interlock-web/tests/returnToRedirect.test.ts (13 payloads).
  • GHSA-337j-9hxr-rhxg (SSR hydration). Unreachable: deserializeErrors()
    runs only during SSR hydration and this is a client-only Vite SPA with no
    hydrateRoot / StaticRouter / renderToString / createStaticHandler.
    Pinned by interlock-web/tests/noSsrHydration.test.ts, which fails the moment
    SSR is introduced — forcing the v7 upgrade instead of a stale waiver.

Every exception expires 2026-11-01, is owned by maaz@getinterlock.dev, and
names one exact GHSA id. Wildcards are rejected and tests/test_dependency_audit_policy.py
fails on an expired, incomplete, unowned, duplicated, or stale entry, or one
whose cited control file does not exist.

CI enforcement

scripts/audit_npm.py is a policy-aware gate. npm audit --audit-level cannot
express "block everything except these three, and only until they expire", and
the alternative — redirecting npm audit to a file with || true — is the same
exit-swallowing that caused this bug. The gate runs npm audit itself, so one
command per scope both records JSON and decides pass/fail with no shell escape
hatch.

The job now:

  • installs frontend deps with npm ci;
  • runs pip-audit blocking (no continue-on-error);
  • gates production deps (npm audit --omit=dev) and build/development
    deps separately, both blocking at moderate;
  • fails on an un-excepted advisory, an expired exception, or a stale policy
    entry;
  • uploads pip-audit.json + both npm payloads as artifacts (if: always());
  • declares permissions: contents: read and pins every action.

Commands and results

Command Result
npm ci (clean) 75 packages, deterministic
npm audit --json high: 1 → 0; 3 advisories → 3 (all excepted)
npm audit --omit=dev --json 2 packages / 3 advisories, all excepted
pip-audit (clean venv, before) 1 vulnerability (pytest), exit 1
pip-audit (after) No known vulnerabilities found, exit 0
npm test 53 passed (44 → 53; +9 new)
npx tsc --noEmit exit 0
npm run build built, index.html 42.46 kB
pytest -q (full, on pytest 9.0.3) 1929 passed, 134 skipped, 6 xfailed
ruff / black --check (changed Python) clean
actionlint (docker, pinned image) exit 0
YAML + policy JSON parse OK
git diff --check exit 0
Secret / private-path scan of diff 0 matches
Generated artifacts staged none (dist/, node_modules, audit JSON all ignored)

Full suite is unchanged in shape on pytest 9.0.3 (1871 → 1929 passed; +58 are
the dependency-audit policy tests, same 134 skipped / 6 xfailed), so the major
test-runner bump is behaviour-preserving here.

Proof the fix is not cosmetic

Pre-fix, from 9b34a17: the audit job had exactly one audit step, it carried
continue-on-error: true, it ran no npm command, and it had no job
permissions. pip-audit exited 1 on that tree — a reproduced advisory
with a green job.

Post-fix, against a controlled critical fixture the gate returns:

CRITICAL evil-pkg (direct) GHSA-fixture-critical
         NOT EXCEPTED -> blocking
summary: 1 advisories, 1 blocking, 0 accepted, 3 stale
RESULT: FAIL      (exit 1)

tests/test_dependency_audit_policy.py (58 tests) pins the full matrix below.

Follow-up repair (this branch, commit b71c448)

An independent review found the first gate approved advisories too loosely and
could fail open. All four issues are fixed and each is pinned by a test.

1. Exceptions are now exact and fail closed. Matching was by GHSA id alone,
so the same id on a different package or escalated to critical was silently
accepted — both reproduced as exit 0 before the fix. An exception now applies
only when id, package, severity, scope, installedVersion and affectedRange
all still match the observed advisory; any drift blocks with a per-field diff
and demands re-review. Installed versions are parsed structurally from
package-lock.json, not taken from the audit payload.

2. npm audit operational failures now fail closed. {"error":{"code":"ENOAUDIT"}},
{}, a null vulnerabilities, a missing/unsupported auditReportVersion, and
missing metadata all previously read as a clean tree (exit 0 on the
development scope). The gate now validates subprocess completion, stdout
presence, JSON parsability and full report structure, and exits 2
"an untrusted audit is not a clean tree" — while still writing evidence when the
payload is structurally usable.

3. The development gate no longer relabels production findings. It was
handed the full tree and called it "development", so the three production
react-router advisories were reported twice. Development is now full minus
production
: 0 dev-only advisories.

4. Ecosystems run independently. pip-audit was blocking without
if: always(), so a Python advisory skipped npm ci and the production gate
entirely — no npm evidence at all. Every audit step now runs with
if: always() and records an outcome id, and a new Dependency audit verdict
step fails the job if any of them failed. No continue-on-error, || true,
|| exit 0, or set +e anywhere.

5. CVSS corrected by removal. GHSA-wrjc-x8rr-h8h6 carried "cvss": 0,
which reads as "no impact" (the advisory is ~5.1). The gate never validated or
used any CVSS value, so the field is removed from all entries rather than
carrying an unvalidated score, and a test forbids reintroducing it.

Hostile-fixture matrix (58 tests, all passing)

Fixture Before After
exact current exceptions pass pass (0)
same id, wrong package pass (fail-open) blocked (1)
same id, critical escalation pass (fail-open) blocked (1)
changed advisory range not checked blocked (1)
installed-version mismatch not checked blocked (1)
package absent from lockfile not checked blocked (1)
wrong scope relabelled blocked (1)
unexcepted moderate / high / critical blocked blocked (1)
below threshold informational informational (0)
expired exception blocked blocked (1)
stale exception blocked blocked (1)
ENOAUDIT payload pass (fail-open) unusable (2)
empty / null / no-metadata / bad version pass (fail-open) unusable (2)
unparsable JSON exit 2 unusable (2)
blocked run still writes evidence n/a asserted

Real CI on b71c448 shows production 3 advisories, 0 blocking, 3 accepted, 0 stale — PASS, development no advisories in this scope — PASS, and the
verdict step reporting PIP_AUDIT success / NPM_PRODUCTION success / NPM_DEVELOPMENT success — all dependency audits passed.

React Router is not migrated to v7 in this repair, as instructed; the
documented controls and the 2026-11-01 expiry are preserved, now behind an
exception gate proven exact.

Second follow-up repair (commit 973f4fe)

A further read-only review found two more fail-open paths. Both were reproduced
against the previous head before being fixed.

B1 — production/full-tree differencing was unsafe. Development findings were
computed by subtracting production rows keyed on (id, package) alone.
Reproduced: a production row at moderate erased a full-tree row for the same
id/package at critical — the dev finding vanished entirely. Differencing now
compares the complete security identity (id, package, severity, range,
node_range):

  • identical identity and identical vulnerable paths → subtract;
  • identical identity but the full tree lists extra vulnerable instances
    the row is retained, narrowed to just those dev-only paths, and evaluated;
  • same (id, package) but different severity/range → the two reports disagree
    about the advisory itself, so the gate raises AuditUnusable rather than
    guessing which report is authoritative.

B1 — multiple installed versions were collapsed. installed_versions() used
setdefault, so a lockfile with node_modules/postcss@8.5.25 and
node_modules/some-tool/node_modules/postcss@7.0.1 silently kept only the first.
An exception could then be validated against a version that is not the
vulnerable one. Replaced by installed_instances(), which keeps every
lockfile path → version pair. Vulnerable nodes paths are resolved to the
specific installed instance; when several distinct versions are implicated, one
recorded installedVersion cannot describe them all and the exception is
refused.

B2 — run_npm_audit ignored proc.returncode. Reproduced: npm exiting
2 and 127 with valid-looking audit JSON on stdout was accepted as a
clean tree. Only {0, 1} — the codes that mean "the audit completed" — are now
accepted; every other code raises AuditUnusable even when stdout parses.
The diagnostic carries the exit code plus a sanitize_stderr() summary that
caps output to 4 lines / 240 chars and replaces any credential-bearing line
wholesale, so tokens, _authToken entries, and Authorization headers cannot
reach a public CI log.

Added regression tests (+15, 26 → 41)

Area Test
B1 identical rows subtract
B1 critical-only-in-full raises AuditUnusable
B1 changed-range-only-in-full raises AuditUnusable
B1 additional vulnerable node retained as development-only
B1 genuinely dev-only advisory stays visible, and blocks end-to-end
B1 multiple installed versions all retained
B1 node paths resolve to the specific installed instance
B1 ambiguous multi-version install blocks an exception
B2 exit 0 + empty report succeeds
B2 exit 1 + advisory report reaches policy evaluation
B2 exit 2 + valid-looking report fails closed
B2 exit 3/127/254/-1 fail closed
B2 diagnostics carry the exit code and leak no credentials
B2 stderr summary is length- and line-capped

All pre-existing ENOAUDIT, malformed-payload, exactness, expiry and staleness
tests remain green.

Third follow-up repair (commit c186414)

A further review found three more fail-open paths. All eight failure modes were
reproduced against the previous head before being fixed.

F1 — the superset invariant was never checked. development_only() only
walked the full tree, so it could find extra dev findings but could never notice
that a production finding had gone missing — which would mean the two npm runs
did not describe the same dependency graph. Reproduced: production reporting
GHSA-y (pkg-b, high) that the full tree omitted returned [] with no error;
likewise a production vulnerable path absent from full. Differencing is now
bidirectional: every production identity and every production vulnerable
path must be present in the full tree, or the gate raises AuditUnusable.
Rows are also grouped by identity with their paths unioned, so a duplicate
identity can no longer overwrite an earlier row's paths. Full-only identities
and full-only paths still surface as development findings, and a security-field
disagreement still raises — now with the more specific "reports disagree"
diagnosis taking precedence over "missing".

F2 — partial node-path resolution was accepted. implicated_versions()
kept whichever nodes paths happened to resolve and silently dropped the rest,
so an advisory reporting a known and an unknown instance shrank to the known
one — potentially the very instance whose version matches an exception. A path
resolving to a different package was also accepted. Now: nodes must be a
list of non-empty strings; when npm names instances, every one must resolve
to an instance of the expected package, or the gate fails closed. The
all-instances conservative fallback applies only when npm supplies no paths.

F3 — report metadata was never cross-checked. {} with total: 1,
a populated body with total: 0, buckets not summing to total, and negative,
string, or boolean counts were all accepted. For auditReportVersion 2 the gate
now requires every severity bucket and total to be a non-negative integer
(bool is rejected explicitly, since it subclasses int), total == sum(buckets),
and total == len(vulnerabilities) — npm counts vulnerable package nodes, not
flattened advisories. Each vulnerability node is validated for object shape,
severity, via list (which legitimately mixes advisory objects with plain-string
indirect edges), nodes list, and range type before flattening. Verified
against the live production and full reports, which both satisfy
total == len(vulnerabilities) == sum(buckets) == 2.

While fixing F2 I also found that an AuditUnusable raised during exception
matching escaped the fail-closed boundary and surfaced as a traceback; the
evaluation is now inside that boundary and reports the standard
AUDIT UNUSABLE … RESULT: FAIL with exit 2.

Added regression tests (+17, 41 → 58)

Area Test
F1 production row absent from full fails
F1 production vulnerable path absent from full fails
F1 duplicate identities union paths instead of overwriting
F1 duplicate production paths are not lost during subtraction
F1 full-only identity remains visible
F1 full-only path remains visible
F2 one known + one unknown node path fails
F2 wrong-package node path fails
F2 empty/absent nodes use conservative all-instance evaluation
F2 malformed nodes values fail
F3 empty vulnerabilities with total=1 fails
F3 non-empty vulnerabilities with total=0 fails
F3 severity buckets not summing to total fail
F3 missing, negative, string and boolean counts fail
F3 malformed vulnerability nodes fail
F3 via mixing objects and name strings is accepted
F3 the current real npm v2 reports remain accepted

Every earlier hostile regression (exactness, expiry, staleness, ENOAUDIT,
malformed payloads, exit-code validation, multi-version lockfiles) remains green.

Fourth follow-up repair (commit 4d4f935)

A final review found the parser-to-decision path itself could still return a
clean verdict for a report that declares a vulnerability. Reproduced against
c186414 before being fixed.

The hole. The verdict is computed from advisory objects flattened out of
each node's via. But a report declares vulnerabilities through metadata and
per-node severity — fields the flattening never reads. So a report naming one
high-severity vulnerable package, with metadata that agrees with itself in
every way the F3 checks test (total == len(vulnerabilities) == sum(buckets) == 1),
and via: [], validated cleanly, flattened to zero advisory rows, and
printed:

== npm audit gate :: scope=production threshold=moderate ==
  no advisories in this scope

  summary: 0 advisories, 0 blocking, 0 accepted, 0 stale
  RESULT: PASS          (exit 0)

This repo's own three production exceptions happen to mask it — they would all
read as stale and exit 1 — but that is incidental cover, not a guarantee. A repo
with no exceptions, or any advisory nobody wrote an exception for, gets the PASS
above. The regression test uses an exception-free policy so it pins the real
verdict rather than the accident.

Post-fix, the same fixture returns exit 2:

  AUDIT UNUSABLE: …: vulnerabilities['evil-pkg'] declares severity 'high' but no
  advisory object is reachable from it (it declares no via entries at all); a
  declared vulnerability carrying no advisory flattens to zero rows and would be
  reported as a clean scope
  RESULT: FAIL (fail-closed; an untrusted audit is not a clean tree)

G1 — advisory-graph completeness. npm states causation two ways inside
via: an advisory object is direct evidence, and a bare string names another
vulnerable package node that makes this one vulnerable. Every node must now
reach a real advisory object by walking that graph. Rejected: an empty
via; a dangling edge naming a package the report does not contain, so the
report is missing its own cause; and a cycle that never terminates in
evidence — pkg-a → pkg-b → pkg-a, and self-reference — each with its own
diagnostic. The walk is cycle-safe and its visited set doubles as the
diagnostic. Preserved as valid, because this is what npm actually emits:
object/string mixtures on one node, multi-hop string chains that do terminate in
an advisory, and cycles that contain one.

G2 — advisory-object validation. severity, range, name and the
url/source identity are each facts an exception is matched against, and a
missing one silently became an empty string that was then compared as though it
were an observed fact. All are now required and well-formed: severity in the
known buckets, range a non-empty string, name a non-empty string that
equals the package the advisory is listed under, source a non-negative
integer (bool rejected explicitly), url a non-empty string when present, and
at least one usable identity — without one, no exception could ever be matched
to the advisory.

G3 — exact severity-bucket comparison. F3 compared sum(buckets) against
total; matching totals are not agreement. A report can hold exactly as many
vulnerable package nodes as total claims, with the buckets summing to that same
total, and still restate a critical finding as moderate — precisely the
drift the exception matcher exists to catch. Buckets are now compared against the
declared node severities one for one.

G4 — a closing invariant in the flattening. Independent of how a report
reached that point, a summary declaring vulnerable packages can never produce
zero advisory rows. Zero rows is the one result the gate cannot distinguish from
a clean tree, so it has to be earned. A genuinely empty report still flattens to
[] and still passes.

G5 — no raw tracebacks. Policy loading and --today parsing sat outside
the fail-closed boundary. An unreadable policy file, a malformed expires, or an
entry missing id / compensatingControl escaped as a Python traceback and exit
1 — the same exit code as "policy violation", so a broken policy file read as
a blocked build. Policy loading, per-entry validation and date parsing now run
inside the boundary and exit 2 with a clean diagnostic and no traceback.

Added regression tests (+21, 58 → 79)

Area Test
G1 declared high severity with via: [] cannot pass end-to-end
G1 empty via is rejected at validation
G1 via edge naming an absent package fails
G1 dangling edge fails even when a sibling via carries an advisory
G1 cyclic via graph without an advisory fails
G1 self-referential via without an advisory fails
G1 string chain that reaches an advisory is accepted
G1 cycle with an advisory reachable through it is accepted
G2 advisory-object fields validated (16 hostile mutations)
G2 object without url but with source is accepted, id falls back
G3 metadata bucket disagreeing with node severity fails
G3 matching buckets are accepted
G3 severity-bucket swap across two nodes fails
G4 declared total cannot flatten to zero advisories
G4 zero declared vulnerabilities still flattens to nothing
G4 a clean tree still passes the gate
G5 graph failures print a clean diagnostic, no traceback
G5 unreadable policy file fails closed without a traceback
G5 malformed policy exception fails closed without a traceback
G5 bad --today override fails closed without a traceback
current real npm reports pass full advisory-graph validation

Every earlier hostile regression (exactness, expiry, staleness, ENOAUDIT,
malformed payloads, exit-code validation, multi-version lockfiles, superset
differencing, node-path resolution, metadata counts) remains green.

Verified against the live trees after the change: production still reports
3 advisories, 0 blocking, 3 accepted, 0 stale — PASS, development still reports
no advisories in this scope — PASS, and the real object/string via mixture on
react-router-dom is accepted unchanged. Full backend suite: 1950 passed,
134 skipped, 6 xfailed
. ruff, black --check --target-version py312 and
mypy clean on both changed files.

Limitations

  • Three moderate production advisories remain accepted, not fixed. The residual
    risk is a react-router open redirect if sanitizeReturnTo is ever weakened —
    which is exactly what the pinned test prevents. Follow-up: react-router v7
    migration before 2026-11-01.
  • Exception expiry is enforced against the CI clock; a long-lived branch could
    pass locally and fail later on the same commit. That is intended.
  • affectedRange and installedVersion are compared as exact strings, not
    parsed semver. An upstream range rewritten to an equivalent form blocks and
    needs a one-line policy refresh — deliberately fail-closed.
  • Metadata cross-checks assume npm's auditReportVersion 2 semantics
    (total counts vulnerable package nodes). A future report version fails
    closed until reviewed.
  • Supported npm exit codes are pinned to {0, 1}; a future npm that adds a
    third "completed" code fails closed until reviewed.
  • The gate reasons over npm audit output only; it does not do its own
    reachability analysis. Reachability claims here are manual and recorded in the
    policy file.
  • npm audit advisory counts are per-package in metadata but per-advisory in
    the gate output (2 packages / 3 advisories). Both are reported.
  • The Python audit covers requirements.txt; it does not pin transitive wheels
    with hashes, so a hash-pinning/SBOM step is still future work.
  • No production deployment occurred. A Vercel preview may be automatically
    created by the repository's branch-push integration; main and the production
    backend remain unchanged.
  • A via string edge must name a node present in the same report. That
    holds in both live trees (react-router-dom -> react-router resolves in
    the production tree as well as the full one) and follows npm's own
    semantics: the cause must be in the tree that contains the effect. If some
    npm edge case ever emits an out-of-scope edge, the gate fails closed
    (exit 2), not open.
  • An advisory object's name must equal the package node it is listed under,
    which is what npm emits today. A future report format that hangs an advisory
    under a different package fails closed until reviewed.

@vercel

vercel Bot commented Aug 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
interlock-web Ready Ready Preview Aug 2, 2026 10:55am

The gate computed its verdict from advisory objects flattened out of each
node's `via`, but a report declares vulnerabilities through `metadata` and
per-node `severity` — fields the flattening never reads. A report naming one
high-severity vulnerable package with entirely self-consistent metadata and
`via: []` therefore validated, flattened to zero rows, printed "no advisories
in this scope" and exited 0.

Close it at every layer the verdict passes through:

* advisory-graph completeness — every vulnerable node must reach a real
  advisory object. Empty `via`, an edge naming a package the report does not
  contain, and cycles that never terminate in evidence are all rejected, each
  with its own diagnostic. Object/string `via` mixtures and multi-hop string
  chains stay valid, because that is what npm actually emits;
* advisory-object validation — severity, range, package name and the
  url/source identity must all be present and well-formed, since each is a
  fact an exception is matched against and a missing one silently became an
  empty string;
* exact severity-bucket comparison — buckets are compared against the node
  severities one for one, so a finding cannot be restated on its way into the
  summary while the totals still add up;
* a closing invariant in the flattening itself — a summary declaring
  vulnerable packages can never produce zero advisory rows, whatever path the
  report took to get there.

Policy loading and date parsing move inside the fail-closed boundary. An
unreadable policy file or an unparsable expiry used to escape as a traceback
and exit 1 — indistinguishable from "policy violation". Both now exit 2 with a
clean diagnostic.

Verified against the live production and full npm trees: both still validate,
still flatten to the same three advisories, and the real object/string via
mixture in react-router-dom is still accepted.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant