Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
2 changes: 1 addition & 1 deletion 0-AI-MANIFEST.a2ml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
(rule "Run tests; report inaccessible services and missing evidence honestly."))
(evidence
(application-ci ".github/workflows/ci.yml")
(governance ".github/workflows/governance.yml")
(governance ".github/workflows/hypatia-scan.yml")
(history-scan ".github/workflows/secret-scanner.yml")
(security-practices ".github/workflows/scorecard.yml")
(fleet-enrolment ".gitbot-fleet/enrolment.json")
Expand Down
2 changes: 1 addition & 1 deletion MetaManifold-WebUI_chora.deed
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
:contractiles ".machine_readable/contractiles/"
:enforcement ".github/workflows/")
(status
:evidence-source ".github/workflows/governance.yml"
:evidence-source ".github/workflows/hypatia-scan.yml"
:evidence-status "Fork governance lane: DEED syntax, K9 L0-L2 contract conformance (must, trust, intend at Yard tier; L3 signatures not verified in CI), and the Hypatia verdict grid with gitbot-fleet report-backs. Upstream owns the application code and its CI; this deed describes the fork.")
(policies
(licensing
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
[![codecov](https://codecov.io/gh/JoshuaJewell/MetaManifold-WebUI/graph/badge.svg?token=20F1VLF590)](https://codecov.io/gh/JoshuaJewell/MetaManifold-WebUI)

Fork governance (hyperpolymath/MetaManifold-WebUI only; nothing here is part of upstream):
[![Governance (fork)](https://github.com/hyperpolymath/MetaManifold-WebUI/actions/workflows/governance.yml/badge.svg)](https://github.com/hyperpolymath/MetaManifold-WebUI/actions/workflows/governance.yml)
[![Governance (fork)](https://github.com/hyperpolymath/MetaManifold-WebUI/actions/workflows/hypatia-scan.yml/badge.svg)](https://github.com/hyperpolymath/MetaManifold-WebUI/actions/workflows/hypatia-scan.yml)
[![DEED syntax](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fhyperpolymath%2FMetaManifold-WebUI%2Fgovernance-badges%2Fdeed.json)](MetaManifold-WebUI_chora.deed)
[![K9 contracts](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fhyperpolymath%2FMetaManifold-WebUI%2Fgovernance-badges%2Fk9-grid.json)](.machine_readable/contractiles/)
[![Hypatia grid](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fhyperpolymath%2FMetaManifold-WebUI%2Fgovernance-badges%2Fhypatia-grid.json)](https://github.com/hyperpolymath/standards/blob/main/docs/VERDICT-GRID-SPEC.adoc)
Expand Down
20 changes: 12 additions & 8 deletions docs/release-notes/2026-10-23-governance-security.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,12 @@ It is historical evidence, not a freshly executed scan of this change.
Refresh the reviewed path snapshot deliberately when syncing upstream. The
pinned scanner does not honor `.hypatia-ignore` for this rule, so that file
would not implement this exception.
- **Scanner discovery:** Hypatia is called by `governance.yml`, not the
heuristic's expected `hypatia-scan.yml`. CodeQL is already supplied by the
GitHub App (Actions, JS/TS and Python checks on #227); do not run a second
scan just to satisfy filename detection. No mirror target is configured;
adding a no-op `mirror.yml` would manufacture coverage.
- **Scanner discovery (at this audit's snapshot):** Hypatia was called by
`governance.yml`, not the heuristic's expected `hypatia-scan.yml`. PR #265
now places that same active governance workflow at `hypatia-scan.yml`, rather
than adding a duplicate scan. CodeQL is already supplied by the GitHub App
(Actions, JS/TS and Python checks on #227); no second scan was added. No
mirror target is configured; an empty `mirror.yml` would manufacture coverage.
- **HTTP identifiers:** W3C SVG/XLink namespace URIs in `figure/export.ts`
and `TreeView.tsx` are identifiers, not insecure network requests. Replacing
their `http` scheme breaks namespace identity.
Expand All @@ -146,9 +147,12 @@ It is historical evidence, not a freshly executed scan of this change.
fails the job; the logging regression tests guard this boundary.
- **Licensing:** four upstream source files lack SPDX identifiers. Existing
headers are protected by `CONTRIBUTING.md`; no licence text was changed.
- **Further review:** outbound-egress monitoring on secret-bearing jobs,
panic-attack's dynamic R evaluation/FFI/input-boundary heuristics and pons's
loop/unreachable heuristics still need scoped review, not blanket dismissal.
- **Further review (at this audit's snapshot):** outbound-egress monitoring
on secret-bearing jobs and the scanner findings still needed scoped review,
not blanket dismissal. PR #265's R-evaluation/FFI and pons process-cleanup
review is recorded in [`scanner-review-2026-10-24.md`](2026-10-24-scanner-review.md);
the remaining heuristic findings and unrelated CI concerns are not declared
clean by that review.
- **Fleet:** seven enrolled bots had no report-back. They remain `not_run`;
no synthetic reports or success records have been created.
- **Alert permissions:** the connected GitHub integration returns HTTP 403 for
Expand Down
97 changes: 97 additions & 0 deletions docs/release-notes/2026-10-24-scanner-review.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Scanner finding review — 2026-10-24

This is a code review of the Hypatia, panic-attack and pons findings reported on
PR #265. It records evidence and code changes; it is **not** a scanner baseline,
suppression list or claim that every finding is clean. Raw scanner reports stay
available to CI.

## Changes made from the reported findings

- The format-probe child supervisor now uses an explicit monotonic deadline
instead of `while True`. Process-disappearance races are handled as explicit
`continue` cases. Cleanup also checks whether a direct child is still alive
when its process group has disappeared, and kills or reports it instead of
silently discarding that condition.
- The active DADA2 command logger remains because pipeline stages rely on its
one-string/one-record provenance. Its R-literal encoder now escapes quotes,
backslashes, line breaks, tabs and control characters, and rejects NUL (which
R strings cannot represent). Regression tests cover those boundaries.
- The PERMANOVA reference test now binds permutation-block labels as R data.
It no longer interpolates an R expression string into the test's R program.
- The high-precision property test now requires the **returned candidate** to
be a `BigFloat` as part of the numerical comparison. The previous assertion
checked only the independent reference value's type; it did not establish the
result's type or accuracy.
- The installer now calls Julia's `Base.Libc.geteuid()` abstraction rather than
making a direct `ccall` for the root check.

## R execution findings: remaining boundary, reviewed

`RCall.reval` executes R source and is treated as an execution boundary. The
pinned panic-attack Julia detector is a content-substring heuristic: it reports
`eval(` or `Meta.parse(` without distinguishing RCall's `reval`, fixed R source,
or parse-only syntax validation. See the pinned detector implementation at
[panic-attack `b689a74`](https://github.com/hyperpolymath/panic-attack/blob/b689a74d2762a1c19839ae3fbd59b7fe5ef920ea/src/assail/analyzer.rs#L4735-L4756).
No inline `panic-attack: accepted` marker was added; these findings remain
visible pending a more precise detector or a separately approved refactor.

The current boundaries were reviewed individually:

| Location | Boundary and evidence |
|---|---|
| `src/analysis/analysis.jl` | R snippets are application-owned source. Matrices, labels and seeds are bound as R variables. The one formula assembled from metadata uses `_permanova_formula_rhs`, which accepts only plain, non-reserved ASCII R identifiers; expression-shaped names are rejected and tested in `test/unit/test_permanova.jl`. |
| `src/pipeline/dada2/context.jl` | `_r_run_logged` is active and therefore retained. Stage call templates are fixed in source; every config-derived string/number is rendered by `_r_lit`. The serializer now escapes quote/backslash and line/control characters and refuses NUL; `test/unit/test_dada2_commands.jl` covers these cases. |
| `src/analysis/differential.jl` | Model programs are source constants. Counts, labels, offsets and validated method selections cross through RCall bindings, not source interpolation. Cleanup expressions use fixed variable names. |
| `src/analysis/ordination.jl` | R receives `ParsedFormula.canonical_rhs`, rebuilt from a strict Julia parser that admits identifiers, `+`, two-way `:`/`*`, and `Condition(...)` only. The parser rejects arbitrary calls and is covered by ordination formula tests. |
| `src/core/provenance.jl` | The package name interpolated into a query is restricted to `[A-Za-z][A-Za-z0-9.]*`; quotes, separators and control characters are rejected before source construction. |
| `scripts/check-julia-syntax.jl` | `Meta.parse` constructs syntax trees only. The script does not evaluate parsed expressions; it explicitly rejects parser errors, incomplete expressions and top-level stray `end`. |
| `test/unit/test_permanova.jl` | The former interpolated permutation expression has been replaced by a bound vector and fixed R control flow. Remaining R calls run the test's fixed reference program. |

The remaining `RCall.reval` sites are required to run the existing pinned R
analysis code. They are not suppressed or claimed to be harmless by virtue of
using R; the input-to-source boundaries above are the reviewed controls.

## FFI finding: intentional low-level measurement boundary

`install.jl` no longer contains a direct foreign call. The three direct libc
calls in `src/core/memory_probe.jl` remain: `open`, `read` and `close` read
`/proc/<pid>/status` into preallocated buffers so the opt-in occupancy probe
does not add Julia allocations to the measurement. The path is built from an
integer child PID, is NUL-terminated, and is kept alive with `GC.@preserve`; no
user-supplied pointer or function name reaches the calls. Read failures are
reported as a missing process sample, and the opened descriptor is closed.

This is a real, deliberately narrow FFI boundary, not a false positive or a
waiver. The panic-attack finding remains reportable; replacing it with allocating
Julia IO would change the measurement the probe exists to make.

## Pons findings: verified cleanup races

The pons findings were on handlers for `ProcessLookupError` and
`ChildProcessError`. These are TOCTOU races between reading `/proc`, signaling,
and reaping a child. The handlers now state the race outcome with explicit
control flow; the direct-child cleanup path fails closed if a child is still
live. The supervisor's cleanup deadline is now visible in the loop condition.
The pinned pons rule only reports empty or Python-`pass`-only handlers; its
[implementation at `32e5567`](https://github.com/hyperpolymath/pons-asinorum/blob/32e55677d8fd1f3010848dee22e822c2f385c2a0/crates/pons-rules/src/t0/swallowed_error.rs)
was reviewed. No pons suppression comments were added.

## Hypatia findings not waived

- The seven reported TypeScript paths were checked against the exact upstream
snapshot used by `config/hypatia/upstream-typescript-allowlist.txt` and are
absent from that snapshot. They remain reportable; the upstream exception was
not broadened.
- The Python files are independent statistical references and format-readiness
test/probe harnesses. Their language-policy findings are not safety bugs, but
they are not allowlisted or suppressed. Rewriting the independent oracles
solely to satisfy the scanner would remove their cross-implementation value.
- The workflow-name finding is addressed by moving the **existing, active** fork
governance workflow to `.github/workflows/hypatia-scan.yml`; it still runs the
same governance jobs and scanner, so this does not add a duplicate or no-op
scan. CodeQL is already run by the configured GitHub App; no second CodeQL
workflow was added. There is no configured mirror target, so no empty
`mirror.yml` was added.
- Findings tied to stored code-scanning alerts and any findings in the
unavailable detailed artifact are not represented as fixed here. The scan
artifact remains the authoritative evidence for those items.
2 changes: 1 addition & 1 deletion install.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1073,7 +1073,7 @@ end

## Preflight checks
"""Return true if the current process is running as root."""
has_root() = try; ccall(:geteuid, Cuint, ()) == 0; catch; false; end
has_root() = try; Base.Libc.geteuid() == 0; catch; false; end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore a working effective-user-ID check

When the installer runs as root on Linux, this now returns false: the project's supported Julia 1.12 does not define Base.Libc.geteuid (Julia 1.12 Libc source), and the catch silently swallows the resulting UndefVarError. Consequently, package_install_cmd refuses installation unless passwordless sudo is available, and main skips automatic R system-dependency installation in root containers without sudo. Retain the working libc call or replace it with an API that exists in the supported Julia version.

Useful? React with 👍 / 👎.


"""Return true if sudo is available and can run non-interactively."""
function has_passwordless_sudo()::Bool
Expand Down
7 changes: 6 additions & 1 deletion scripts/check-governance-contract.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ set -euo pipefail
ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"

CALLER=.github/workflows/governance.yml
CALLER=.github/workflows/hypatia-scan.yml
REUSABLE=.github/workflows/hypatia-scan-reusable.yml
ALLOWLIST=config/hypatia/upstream-typescript-allowlist.txt
FORMAT_TOOLS=config/format-tools.toml
Expand Down Expand Up @@ -237,6 +237,11 @@ fi
if gate="$(step_block "$REUSABLE" 'Gate on baseline (blocking when a baseline is committed)')"; then
contains "$gate" 'hypatia-findings.effective.json' \
'the baseline gate consumes exception-aware findings'
if grep -Fxq " if: '!inputs.block-on-high && (steps.inherited-language.outputs.findings_count > 0 || hashFiles(''.hypatia-baseline.json'') != '''')'" <<<"$gate"; then
pass 'the advisory baseline gate requires nonblocking mode and either findings or a committed baseline'
else
fail 'the advisory baseline gate requires nonblocking mode and either findings or a committed baseline'
fi
else
fail 'the baseline gate step exists'
fi
Expand Down
35 changes: 25 additions & 10 deletions scripts/filter-hypatia-inherited-language.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,35 @@
fi
done

# Resolve relative components and symlinks even when output directories do not
# exist yet. Neither output may overwrite either raw audit artifact.
raw_findings=$(realpath -m -- "$raw_findings")
raw_sarif=$(realpath -m -- "$raw_sarif")
effective_findings=$(realpath -m -- "$effective_findings")
effective_sarif=$(realpath -m -- "$effective_sarif")
# Print the absolute path for $1 to standard output, followed by a newline.
# Resolve relative components and symlinks portably, including output paths
# whose parent directories do not exist yet, without GNU `realpath -m`.
# Resolution is non-strict: inaccessible components or symlink loops may remain
# unresolved. Hard-link aliases require the caller's file-identity checks.
# Python invocation failures propagate as a non-zero exit status.
canonicalize_path() {
# A non-newline sentinel prevents command substitution from trimming the path.
python3 -c 'import os, sys; sys.stdout.write(os.path.realpath(sys.argv[1]) + ".")' "$1"

Check warning on line 40 in scripts/filter-hypatia-inherited-language.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_MetaManifold-WebUI&issues=AaFtEn1kGh8uurdkf-DA&open=AaFtEn1kGh8uurdkf-DA&pullRequest=265
}
raw_findings=$(canonicalize_path "$raw_findings")
raw_findings=${raw_findings%.}
raw_sarif=$(canonicalize_path "$raw_sarif")
raw_sarif=${raw_sarif%.}
allowlist=$(canonicalize_path "$allowlist")
allowlist=${allowlist%.}
effective_findings=$(canonicalize_path "$effective_findings")
effective_findings=${effective_findings%.}
effective_sarif=$(canonicalize_path "$effective_sarif")
effective_sarif=${effective_sarif%.}
for output in "$effective_findings" "$effective_sarif"; do
if [[ "$output" == "$raw_findings" || "$output" == "$raw_sarif" || "$output" -ef "$raw_findings" || "$output" -ef "$raw_sarif" ]]; then
echo "Raw inputs and effective outputs must use distinct paths." >&2
if [[ "$output" == "$raw_findings" || "$output" == "$raw_sarif" || "$output" == "$allowlist" \
|| "$output" -ef "$raw_findings" || "$output" -ef "$raw_sarif" || "$output" -ef "$allowlist" ]]; then
echo "Input, allowlist, and output paths must resolve to distinct files." >&2
exit 1
fi
done
if [[ "$effective_findings" == "$effective_sarif" || "$effective_findings" -ef "$effective_sarif" ]]; then
echo "Raw inputs and effective outputs must use distinct paths." >&2
echo "Input, allowlist, and output paths must resolve to distinct files." >&2
exit 1
fi

Expand Down Expand Up @@ -113,7 +128,7 @@
mv "$tmpdir/effective-findings.jcs.json" "$tmpdir/effective-findings.json"
fi

mkdir -p "$(dirname "$effective_findings")" "$(dirname "$effective_sarif")"
mkdir -p "${effective_findings%/*}" "${effective_sarif%/*}"
cp "$tmpdir/effective-findings.json" "$effective_findings"
cp "$tmpdir/effective.sarif" "$effective_sarif"

Expand Down
37 changes: 28 additions & 9 deletions scripts/formats/descendants.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,17 @@ def _kill_tree(pid):
current = pending.pop()
try:
os.kill(current, signal.SIGSTOP)
stopped.append(current)
pending.extend(_children(current))
except ProcessLookupError:
pass
# The process exited after its parent list was read; it has no live descendants to freeze.
continue
stopped.append(current)
pending.extend(_children(current))
for current in reversed(stopped):
try:
os.kill(current, signal.SIGKILL)
except ProcessLookupError:
pass
# A frozen process can still disappear if it was reaped concurrently.
continue


class Supervise:
Expand All @@ -75,7 +77,7 @@ def _set(self, value):

def _drain(self):
deadline = time.monotonic() + 0.5
while True:
while time.monotonic() < deadline:
children = _children(os.getpid())
if not children:
return
Expand All @@ -84,10 +86,27 @@ def _drain(self):
try:
os.waitpid(pid, os.WNOHANG)
except ChildProcessError:
pass
if time.monotonic() >= deadline:
raise SupervisionError("descendants did not exit within cleanup budget")
time.sleep(0.005) # Bounded cleanup; also reap newly adopted orphans.
# Popen.wait or an earlier nonblocking wait already reaped this child.
continue
remaining = deadline - time.monotonic()
if remaining > 0:
# Bounded cleanup; also reap newly adopted orphans.
time.sleep(min(0.005, remaining))

# A final census lets a child that exited at the deadline be reaped,
# and catches anything newly adopted by the subreaper during cleanup.
children = _children(os.getpid())
if not children:
return
for pid in children:
_kill_tree(pid)
try:
os.waitpid(pid, os.WNOHANG)
except ChildProcessError:
# Another wait already reaped it during the final census.
continue
if _children(os.getpid()):
raise SupervisionError("descendants did not exit within cleanup budget")

def __exit__(self, *_):
try:
Expand Down
26 changes: 19 additions & 7 deletions scripts/formats/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,25 @@ def _remaining(deadline):
def _cleanup(child):
# Fast group cleanup; Supervise also adopts/kills detached descendants.
try:
os.killpg(child.pid, signal.SIGKILL)
except ProcessLookupError:
pass
try:
child.wait(timeout=0.5)
except subprocess.TimeoutExpired as error:
raise ToolError("tool did not exit within cleanup budget") from error
try:
os.killpg(child.pid, signal.SIGKILL)
except ProcessLookupError:
# A tool can leave its session; do not mistake a missing process
# group for a stopped direct child.
if child.poll() is None:
try:
child.kill()
except ProcessLookupError as error:
# The child may exit between poll() and kill(); verify that
# race instead of silently discarding an unexpected live PID.
if child.poll() is None:
raise ToolError(
"tool escaped its process group and could not be terminated"
) from error
try:
child.wait(timeout=0.5)
except subprocess.TimeoutExpired as error:
raise ToolError("tool did not exit within cleanup budget") from error
finally:
for stream in (child.stdin, child.stdout, child.stderr):
if not stream.closed:
Expand Down
Loading
Loading