14 Commits
Author SHA1 Message Date
edeaf135a1 fix(ui): use the three-bar action menu on the Details and Time View graph pages (#8641)
* fix(ui): use the three-bar action menu on the Details and Time View graph pages

The graph drilldown icons were switched to a hidden three-bar (hamburger)
action menu for the thumbnail/preview/tree graph areas, but the Details
(zoom) and Time View (utility) pages still rendered the raw glyph anchors.
Give those two areas the same graphDrillDownToggle treatment so the action
glyphs stay hidden behind the hamburger until it is activated, render the
drilldown container id through get_filter_request_var('local_graph_id'), and
move the Details graph title outside the toggle's positioning container
(matching html_graph_area() in lib/html.php) so a wide title no longer pushes
the hamburger off the graph corner.

Use the native Element.matches(':hover') in graphDrilldownStillHovered()
instead of jQuery's :hover: jQuery delegates :hover to Sizzle for a node
outside the document and throws "unsupported pseudo: hover" when a pending
close timer runs after the Details/Time View area reloads.  A detached node
is treated as not hovered, so the native check cannot throw.

Add a coverage fixture that renders html_graph_single_view() and
html_graph_zoom() with their I/O stubbed and asserts the menu structure and
title placement, so the changed PHP output - not just handwritten browser
HTML - is exercised by the changed-line and request coverage gates.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs(changelog): add issue#8641 entry

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* security(html_graph): html-escape local_graph_id in drilldown container id

SonarCloud/Semgrep flag the raw get_filter_request_var(local_graph_id)
echoed into the drilldown container id attribute in html_graph_single_view()
and html_graph_zoom() as reflected XSS. The value is already int-validated,
but wrap it in htmle() so the request-to-response taint path is closed and
the scanners are satisfied.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: TheWitness <copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-09 09:56:53 -05:00
4e5b2852dc security: harden tokens, imports, script substitution and SpikeKill mutations (#8237)
* security(batch12): constant-time 2FA cookie and reset-token compares (GHSA-4qx8-jj2h-p7q2)

auth_2fa.php used === to compare the 2FA bypass-cookie HMAC and
auth_resetpassword.php used != for the reset-token check; both are
byte-by-byte and leak timing. Switch to hash_equals() so the comparison
is constant-time. Develop-only paths (no released 1.2.x equivalent).

* security(batch12): route package-repo manifest fetches through cacti_http (GHSA-54fg-q9h9-88mm)

package_repos.php fetched the package.manifest for the GitHub and
Direct-URL (repo_type 0/2) repository types with raw file_get_contents()
- the Direct-URL path additionally disabled SSL verification - which
bypassed the hardened cacti_http() client and allowed SSRF (cloud
metadata, internal host/port probing) plus MITM. Both paths now use
cacti_http(), which restricts the scheme to http/https, blocks
private/loopback/link-local targets, pins DNS to prevent rebinding, and
verifies TLS. The GitHub Bearer token is passed via the headers option.
Develop-only (package_repos.php does not exist in released 1.2.x).

* security(batch12): shed matched quotes around resolved data-input tokens (GHSA-5v3j-wcrr-jxjg)

Field values are escaped with cacti_escapeshellarg(), which wraps them
in single quotes. When a Data Input template author placed the <field>
placeholder inside a matched pair of quotes (e.g. "<arg1>"), those
surrounding quotes made the value's own quoting literal, so $(...) or
backticks in the value executed via the shell on the next poll - a
realm-3 user reaching OS command execution, bypassing the CVE-2026-39902
hardening and not addressed by the single-pass fq9x fix.

substitute_script_path() now consumes one matched pair of surrounding
quotes when a token resolves, so the escaped value's own quoting is
authoritative. Unresolved tokens keep their literal form (including any
quotes), and the single-pass behaviour that fixed GHSA-fq9x is retained.
Affects released 1.2.x (<= 1.2.31) and develop; this is the develop fix,
to be backported to 1.2.x.

* security(batch12): regression tests + CHANGELOG for batch-12 develop fixes

Add tests/Unit/Security/Batch12SecurityRegressionTest.php guarding the
batch-12 sinks (GHSA-4qx8, GHSA-54fg, GHSA-5v3j) with a behavioral proof
that a double-quoted Data Input placeholder sheds its quotes, plus a
confirmation test that poller_spikekill.php escapes --rrdfile
(GHSA-vhh2, already fixed in #7235). List the four GHSAs in the CHANGELOG.

* security(batch12): update batch-11 fq9x regression test for quote-aware substitute_script_path

The GHSA-5v3j quote-shedding changed substitute_script_path()'s regex
and capture group, which the batch-11 fq9x test pinned verbatim. Update
those two assertions to the quote-aware pattern marker and $matches[2];
the single-pass guarantee the test covers is unchanged.

* security(batch12): narrow reset-hash row with is_array for PHPStan L6

The hash_equals() guard left $hash inferred as non-empty-array|true, so
$hash['user_id'] tripped PHPStan level 6. Guard with is_array() instead
of a truthy check; db_fetch_row_prepared returns false for no row, so
behavior is unchanged.

* security(batch12): drop the executable bit on the new security test files

* security(batch12): route import-time repo manifest fetches through cacti_http (GHSA-54fg)

get_repo_file() still used raw file_get_contents() for the GitHub and Direct-URL repo types, bypassing the SSRF hardening added to form_save(). Route both remote branches through cacti_http() (scheme/host validation, private-range block, DNS pinning, TLS verify; Bearer token via the headers option) and add a regression assertion covering the import sink.

* security(batch12): make substitute_script_path fully quote-aware (GHSA-5v3j)

The matched-pair quote shed only neutralised a token the template wrapped
exactly ("<arg1>"). A token embedded in a larger quoted word ("prefix<arg1>",
"<arg1>suffix", "<a><b>") kept the outer double quotes, so backticks/$(...) in
the already-escaped value stayed live - the bypass Copilot flagged.

Replace the regex with a single-pass shell-quote-state scanner: it still sheds
an exact wrapping pair, but for any token inside a larger quoted word it briefly
closes the template's quoting around the escaped value so it cannot be
re-interpreted. Values are never re-scanned (single-pass, GHSA-fq9x retained).

Updates the batch-11/12 source assertions, adds prefix/suffix/mixed regression
cases, and drops the Batch12 eval fallback (the unit bootstrap already loads
lib/functions.php via include/global.php).

* security(batch12): relocate batch-12 regressions into vuln-class subfolders

Split tests/Unit/Security/Batch12SecurityRegressionTest.php into behavior-named
files under the existing vulnerability-class layout, per the review feedback:

  Auth/TwoFaCookieAndResetTokenConstantTimeTest.php   GHSA-4qx8
  Ssrf/PackageRepoManifestFetchTest.php               GHSA-54fg
  ShellInjection/DataInputQuotedPlaceholderTest.php   GHSA-5v3j
  ShellInjection/SpikekillRrdfileEscapeTest.php       GHSA-vhh2

Each file reaches the repo root via dirname(__DIR__, 4) for its one
subfolder-deep location. No assertion changes; pure relocation.

* security: preserve template quoting for raw path tokens in substitute_script_path

Keep the develop fix in sync with the 1.2.x backport (#8238): substitute_script_path()
only sheds/closes the template's quotes for self-quoting (cacti_escapeshellarg) values.
Trusted raw path_* tokens are substituted in place so a quoted "<path_php_binary>" keeps
its quotes for the shell and a bare <path_cacti>/scripts/x.php stays unquoted for the PHP
script server, which resolves the first token as a filesystem path. Reformatted per
php-cs-fixer and added a raw-path-token regression test.

* security: keep substituting a token that follows a backslash in substitute_script_path

Mirror the 1.2.x fix (#8238): the scanner's backslash branches consumed the next
character unconditionally, so a legacy/custom template token such as \<arg> never
reached the matcher and the trailing cleanup stripped it to a bare backslash. Both
branches now only pair the backslash with a following quote/backslash (so an escaped
quote still cannot toggle quote-state); any other character - notably a <token> start -
leaves the backslash on its own and is processed on the next iteration. Reformatted per
php-cs-fixer and added a backslash-before-token regression test.

* security: normalize dangling backslash before substituted value in substitute_script_path (GHSA-5v3j)

A template backslash immediately before a <field> token (\<arg1>) escaped the
already-escaped value's opening quote, producing \'...' which the shell rejects
with an unmatched-quote error (odd backslash runs) or prepended a stray literal
backslash (even runs). The quote-aware scanner now sheds one backslash when the
run immediately preceding a self-quoted value is odd, in both the unquoted
in-place branch and the close-around branch, so the value's own quotes always
function as the shell's quotes and $(...)/backticks inside it can never
re-activate.

The backslash regression test now executes each substituted template through
/bin/sh (printf %s) and asserts a clean parse plus a literal round-trip across
odd/even backslash counts, rather than pinning the generated string.

* fix(security): preserve literal backslashes in quoted script fields

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test(security): cover changed guards and gate PR coverage

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* ci: measure committed PR changes on a policy-compliant runner

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* security: address review - nested cmd-sub/backtick handling, coverage gate, test skip

substitute_script_path() now follows the template's own $(...) and backtick
command substitutions as nested parsing contexts with their own quote state, so a
token placed inside one ("$(printf %s <field>)") is judged against that fresh
context instead of a suspended outer double quote. This closes the
nested-reinterpretation bypass where an escaped value sitting inside a command
substitution could be re-activated by an outer double quote. Backslash pairing in
both the unquoted and double-quoted contexts now also neutralizes $ and backtick,
so an escaped command substitution (\$(...) or \`...`) is not mis-detected as live.

.github/workflows/pr-changed-coverage.yml: drop the hard-coded PR-number
allowlist so the job runs whenever the path filter selects it, rather than
silently skipping every unlisted PR once these merge.

tests/Unit/Security/ShellInjection/ScriptFieldShellRoundTripTest.php: skip when
either exec() or proc_open() is unavailable (the test launches the fixture with
exec() while the fixture itself uses proc_open()). The fixture adds 7 nested
command-substitution and backtick round-trip cases (90 -> 97), each proving the
field value echoes back verbatim with nothing inside it executing.

* fix(security): clear quote-empty flag on single-quote content; cover new parser branches

The stack-based substitute_script_path() rewrite left quoteEmpty set while emitting literal content inside a single-quoted region, so a template like '\<field>' wrongly took the exact-wrap branch and dropped the backslash (ScriptSubstitutionMatrix 'shell 56'). Clear the frame's quoteEmpty whenever a single-quoted character is emitted, matching the pre-rewrite fallthrough.

ScriptSubstitutionMatrix.php: add 8 explicit parser cases exercising the \/backtick nested contexts and escaped \$ / backtick pairing so every rewritten line is covered by the changed-coverage probe (1285 -> 1293).

* style: short array syntax and php-cs-fixer formatting in substitute_script_path

* test(security): tolerate formatting in spikekill escaping contract

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(spikekill): bind menu requests and protect mutations on develop

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test(spikekill): validate real CSRF tokens and inline coverage

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* ci(coverage): install locked dependencies for Symfony CSRF probes

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* perf(spikekill): skip disabled float debug formatting

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(spikekill): enforce mutation scope and propagate RRD failures

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(spikekill): escape repair notes and cover window boundaries

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test: remove unused SpikeKill preference binding

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(security): reject unsupported nested shell grammar in #8237 (#8359)

* fix(security): reject unsupported nested shell grammar

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test(security): exercise nested grammar rejection in CI coverage

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(security): reject script fields in backtick substitutions

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test: retain native security fixture evidence in changed coverage

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(security): reject ambiguous shell expansion contexts

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test(ssrf): assemble insecure manifest schemes at runtime

The SSRF negative-test inputs in PackageManifestMatrix.php are rejected by
the import preflight policy, but storing them as clear-text URL literals
(http://, ftp://, file://) tripped SonarCloud php:S5332 and dropped the
New Code security rating to B. Join the scheme separator at runtime so the
fixture no longer carries clear-text literals while the assembled values
keep the exact insecure schemes the policy must reject. All 32 manifest
cases still pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(spikekill): drop dangling href in removeSpikes redirect check

The CSRF POST rewrite of removeSpikes replaced the GET href builder but
left checkForRedirects(data, href) referencing the now-undefined href,
throwing ReferenceError once a POST completed. Call checkForRedirects(data)
so a redirect response reloads (a POST has no GET href to follow).

Update the merged dom_dynamic_boundaries boundary test to exercise the
POST path: define csrfMagicToken and mock $.post instead of $.getJSON, so
it still verifies DOMPurify sanitization of spike results.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Co-authored-by: TheWitness <copilot@users.noreply.github.com>
Co-authored-by: TheWitness <1439914+TheWitness@users.noreply.github.com>
Co-authored-by: Thomas Vincent <thomasvincent@gmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-03 13:43:12 -08:00
Thomas Vincent ff31e051dd fix(a11y): [develop] correct labels, document semantics and duplicate IDs (#8295)
* fix(a11y): [develop] correct labels, document semantics and duplicate IDs

Associates form controls with their labels, replaces obsolete tt markup, removes duplicate element IDs, and adds a native accessibility browser suite with its database fixture.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(a11y): validate label IDs and recognize attribute escaping

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-26 23:19:18 +00:00
Thomas Vincent 6254b2c17d fix(package): name template archives without export-form fields (#8431)
* fix(packaging): name archives from supplied metadata

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(packaging): make archive leaf-name confinement explicit

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test(e2e): move the pointer off the Deepness multiselect button

Deepness sets a zero body margin, so the unhovered #ms-button sits at the viewport origin and headless Chromium on Linux intermittently applies :hover to it.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* ci: pull pinned integration database from authenticated mirror

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-23 10:37:58 -06:00
Thomas Vincent 2761fdc226 fix(rrd): [develop] retain source identity after CF fallback (#8339)
* fix(rrd): [develop] retain source identity after CF fallback

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test: measure native consolidation fallback coverage

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-23 10:37:54 -06:00
TheWitnessandCopilot 5afe0f8f68 Add one-click RRDfile repair (Apply Suggested Fixes) to Data Source Info mode (#8401)
* Add one-click RRDfile repair to Data Source Info mode

The Data Source Info mode already compares the RRDfile against the Cacti
definition and prints the rrdtool tune recommendations (add a missing Data
Source with DS:name:type:heartbeat:min:max, remove an extra one with DEL:name,
and correct the type, heartbeat, minimum and maximum). It did not, however,
offer a way to apply them from the page the way data_debug.php does.

- Add an "Apply Suggested Fixes" link to the "RRDtool Tune Info" box header when
  the web server can write the RRDfile; when it cannot, the header falls back to
  "Run Fix from Command Line" and the commands remain listed for manual use.
- Add a ds_repair action that runs the same tune recommendations server-side
  (gated on data_source_authorized() and on the RRDfile being writable, and
  skipping any recommendation carrying control characters), logs each command to
  DSDEBUG, then reloads the Data Source so the result is visible. This mirrors
  data_debug.php's run_repair / "Apply Suggested Fixes".
- Harden the Info-mode guard so a missing/unreadable RRDfile (rrdtool info
  returning false) no longer triggers an array-offset warning.

* Address review: POST-guard ds_repair and harden repair result check

- Route the "Apply Suggested Fixes" action through a CSRF-protected POST: the
  link now uses the cactiPostAction handler (href="#", data-url) instead of a
  GET navigation, and ds_repair is added to the $bad_actions guard in
  include/global.php so a tokenless request fails closed with 405. ds_repair is
  added to the curated CSRF action test for regression coverage.
- Use a strict comparison on the rrdtool_execute() result: it can return null
  on a failed exec/proxy connection, and null == '' would have reported a failed
  repair as "All RRDfile repairs succeeded."
- Refresh the sink inventory baseline for the new header redirects in ds_repair.

* Add request-coverage probe for the ds_repair RRDfile repair handler

The Data Source Info mode repair handler ds_repair() is a POST action that the
page-level e2e suites do not drive (it needs a Data Source whose RRDfile
mismatches its Cacti definition). Add a request-coverage fixture that loads the
handler in isolation via cacti_fixture_source_function(), stubs its I/O
boundaries, and drives every branch (unauthorized, unreadable RRDfile, no RRA,
nothing to tune, not writable, a control-character recommendation that is
skipped, and both tune success and failure). Register it in
run_request_coverage.sh for data_sources.php changes so the changed-line
coverage gate sees the handler exercised.

* Address review: use defined helpers and boolean repair result

- Replace the undefined cacti_has_control_chars() check with a comparison
  against rrd_strip_control_chars() (loaded from lib/rrd.php) to skip any
  tune recommendation that carries control characters.
- Execute each tune with RRDTOOL_OUTPUT_BOOLEAN and treat a strict true as
  success so proxy storage (which returns the raw 'OK u:...' status under
  RRDTOOL_OUTPUT_RETURN_STDERR) is not misreported as a failure.
- Add the new translatable strings to locales/po/cacti.pot and reuse the
  existing failure msgid.
- Update the request-coverage fixture to the new boundary contract.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-15 20:47:31 -04:00
315964cae8 Log administrative user-account actions on develop (#8281)
* log user action (create, delete, locked, ..)

* change log

* update changelog

* style(auth): apply coding standards to user action logging

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* Audit password changes only after successful account saves

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(auth): sanitize audit fields and confirm account removals

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test: cover native account audit state transitions

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test: synchronize native coverage fixture dispatch

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test: measure coverage at the exact PR revision

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Co-authored-by: Thomas Vincent <thomasvincent@gmail.com>
Co-authored-by: TheWitness <thewitness@cacti.net>
2026-10-14 22:50:14 -04:00
TheWitness ffd7a81109 Scope the tree Graph Templates filter to the drilled Device Template (#8370)
* Scope the tree Graph Templates filter to the drilled Device Template

When drilling a Site Branch into a Device Template (host_id=-1, host_template_id set), create_tree_filter() fell through to listing every Graph Template. Scope the dropdown to the Graph Templates present on that Device Template's devices (within the drilled Site when set), matching the graphs the branch actually shows.

* docs(CHANGELOG): add issue#8370 entry

* test: cover the tree Device Template graph-template scope; fix CHANGELOG ordering

Add an isolated source-slice probe that drives create_tree_filter()'s new host_template_id branch so the changed lines meet the >=80% changed-line coverage gate, wired into run_request_coverage.sh. Move the issue#8370 CHANGELOG entry into the issue block so it no longer precedes the security entries.

* docs(CHANGELOG): append issue#8370 at the end of the issue block
2026-10-14 20:56:12 -04:00
TheWitness 2fdc8747af Cross-filter Device and Graph Template on Preview and List views (#8363)
* Cross-filter Device and Graph Template on Preview and List views

Forward the selected Graph Template(s) to the ajax_hosts Device callback and filter the device list accordingly, so choosing a Graph Template narrows the Device list. Device-to-template narrowing already exists on these views.

* Use line comment style to satisfy php-cs-fixer

* fix(graph_view): validate raw graph_template_id against filter allowlist in html_make_device_where

The ajax_hosts endpoint calls html_make_device_where() before the graph filter is sanitized, so gnrv('graph_template_id') is raw request data. An array value such as graph_template_id[]=cg_1 reached html_transform_graph_template_ids()'s str_contains() and raised a TypeError (HTTP 500). Reject non-string input and validate against the same regexp allowlist the filter uses before transforming.

* test(graph_view): add request-coverage probe for the device/template cross-filter

The pr-php-coverage gate requires >=80 percent of changed executable PHP lines to be exercised. The new html_make_device_where() graph_template_id branch and the request_vars changes in create_graphs_preview_filter()/create_listview_filter() are only reached when a graph_template_id filter is present, which the page-level e2e suites do not drive. Add an isolated source-slice probe that runs those production functions with stubbed I/O boundaries and wire it into run_request_coverage.sh so the changed lines are covered.
2026-10-14 13:21:22 -04:00
Thomas Vincent 4d11070ced feat: expose RRDtool 1.11 graph axis controls (#8356)
* feat: expose RRDtool 1.11 graph axis controls

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix: constrain axis template inheritance checkboxes

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test(rrd): exercise native aggregate validation and enforce coverage

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* ci: share changed-code coverage contracts and enforce shell policy

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(ci): run request coverage with portable path matching

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* ci: retain bootstrap coverage and validate request dispatch

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test: synchronize native coverage fixture dispatch

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test: measure coverage at the exact PR revision

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-14 12:10:28 -04:00
TheWitnessandTheWitness 8acc5ec916 feature: theme-aware graph drilldown action menu (per-theme CSS palette) (#8355)
Label the open graph drilldown strip (layout.js labelGraphDrilldownIcons), shorten the action titles, and restyle the strip as a table menu. Each theme's main.css declares the --cacti-dd-* custom properties (foreground, hamburger/panel backgrounds, hairline border, hover accent) that include/css/graph_drilldown.css consumes via var() with a dark fallback, so each theme gets an appropriate light or dark look with its own accent and the hamburger reads correctly on light themes. Themes without a block (e.g. midwinter) use the dark fallback. Regenerates the affected locales/po/cacti.pot entries for the shortened titles.

Co-authored-by: TheWitness <1439914+TheWitness@users.noreply.github.com>
2026-10-13 12:33:14 -04:00
TheWitnessandThomas Vincent f39e74c65c Graph drilldown: keep hamburger fixed, icons reachable, hide on graph leave (#8315)
* Graph drilldown: keep hamburger fixed, icons reachable, hide on graph leave

The action-icon strip now slides down below the hamburger (flex column) instead
of the hamburger dropping to the bottom of the icons. Closing is driven by
leaving the whole graph rather than the small icon strip, so the mouse can move
from the hamburger onto the icons without the strip closing. Leaving the graph
also blurs the hamburger so it no longer lingers after interacting with it.

* changelog: graph drilldown interaction fixes (#8315)

* Graph drilldown: tolerate tooltip-induced mouseleave, drop hamburger title

A native tooltip appearing over a drilldown icon fires a spurious mouseleave on
the graph (relatedTarget null), which closed the strip. Defer the close and
re-check the graph is genuinely not hovered first, so the icon tooltips no longer
destabilize it. Also drop the hamburger's redundant title (its aria-label stays).

* Graph drilldown: widen the close grace period to 500ms

* Graph drilldown: tie hamburger visibility to the graph image

Reveal the hamburger while the pointer is over the graph image (or the toggle
overlaid on it) rather than anywhere in the graph cell, so moving off the image
hides it after the grace period.  Entering a different graph now immediately
hides and closes any other graph's open strip.

* Graph drilldown: lift the open graph so its strip clears thumbnails below

With thumbnails a short graph's open icon strip overflows onto the graph below;
sibling graphs paint in DOM order, so that graph rendered on top of the strip,
making the icons unclickable and stealing the hover (which closed the strip).
Raise the open graph (.graphDrillActive, position:relative; z-index) so its
overflowing strip stays on top and the graph beneath is not hovered.

* Graph drilldown: reveal via :focus-visible and stop blurring to preserve keyboard focus

* fix(ui): preserve graph action focus during pointer navigation

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test(ui): cover graph drilldown interactions and gate PR coverage

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* ci: measure committed PR changes on a policy-compliant runner

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Co-authored-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-12 06:29:12 -04:00
TheWitnessandThomas Vincent 34fc3f8642 Stop flagging a not-yet-created RRA subdirectory as a data_source_path escape (#8313)
* Stop data_source_path_within_rra() flagging a not-yet-created intermediate directory as an RRA escape

get_data_source_path() routed a legitimate <path_rra>/<host_id>/<local_data_id>.rrd through the generated-path fallback and logged a bogus 'SECURITY: ... escapes the RRA directory' message whenever the host_id subdirectory did not exist yet (first poll, or host_id 0). data_source_path_within_rra() only tolerated a missing final RRD file: for a missing file it resolved realpath(dirname(path)), which returns false when that parent directory has not been created, so the lexically-contained path was wrongly rejected.

Walk to the deepest ancestor that actually exists and confirm it resolves inside the RRA root instead. Lexical containment (no .., '.', or empty segments) and per-segment symlink rejection are unchanged, so traversal, lookalike-prefix, absolute, and symlink-pivot paths are still rejected. Adds a regression test for a not-yet-created intermediate directory.

* Drop explanatory comments from the RRA containment fix

* Add CHANGELOG fragment for the RRA containment fix

* Reject a dangling-symlink segment independently of file_exists()

* test(paths): cover RRA filesystem boundaries and gate PR coverage

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* ci: measure committed PR changes on a policy-compliant runner

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Co-authored-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-12 06:28:45 -04:00
Thomas Vincent 08d0f94f24 Fix develop client behavior, popup isolation and PHP method visibility (#8292)
* fix(core): correct client state and declare public method contracts

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(security): isolate Midwinter external homepage popup

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(client): isolate menu state and simplify validated JavaScript guards

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test(auth): use certificate expiry truth in rendering assertions

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* refactor(js): remove unused local storage while retaining initializer calls

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(js): encode inline translated values through the shared JSON boundary

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* style(js): wrap encoded translation literals without changing messages

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(client): remove unused checkbox reads from graph resizing

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* refactor(client): declare repeated function locals once

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test(ui): run column width regression in browser CI

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-11 21:15:39 -04:00