Skip to content

Remove rendered manifest values from verbose logging (#649) - #668

Draft
leynos wants to merge 3 commits into
mainfrom
feat/remove-secret-leaks-from-verbose-ast-tracing
Draft

Remove rendered manifest values from verbose logging (#649)#668
leynos wants to merge 3 commits into
mainfrom
feat/remove-secret-leaks-from-verbose-ast-tracing

Conversation

@leynos

@leynos leynos commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Summary

This branch removes the raw rendered-manifest JSON dump from --verbose
tracing output so secrets interpolated through env() can no longer leak
into terminal history, CI logs, or diagnostic bundles, closing the medium
severity Codex finding for issue #649.

Closes #649.

Review walkthrough

  • Start with
    src/runner/graph_generation.rs
    to see the leak point removed: the tracing::enabled!(DEBUG) guard,
    serde_json::to_string_pretty(&manifest) serialisation, and the
    debug!("AST:...") event are replaced by a call to the new bounded
    structural telemetry entry point immediately after the manifest loads.
  • Then review
    src/runner/manifest_structure_telemetry.rs
    for the replacement boundary. It records only fixed-vocabulary integer
    counts of the loaded manifest shape (variables, macros, rules, actions,
    targets, defaults) through a TRACE span and a metrics::counter! with a
    one-time describe_counter! registration. No manifest text, paths, recipe
    contents, variable values, or descriptions cross the telemetry boundary,
    because rendered values can carry secret material from env()
    interpolation.
  • Finish with
    tests/logging_stderr/verbose_secret_absence.rs
    for the regression proof. The test drives the real --verbose generate
    CLI path with a distinctive sentinel injected through env('CI_SECRET')
    at the command, script, and variable rendering sites, and asserts the
    sentinel is absent from both stdout and stderr while the generated Ninja
    file still resolves the interpolation.

Validation

  • make check-fmt: passes (Rust, Python, and Markdown formatting clean).
  • make lint: passes (Clippy -D warnings, Whitaker Dylint suite, Pylint
    10.00/10, ambrleaks).
  • make test: passes (cargo-nextest full workspace suite plus doctests,
    including the new secret-absence regression test).
  • make doc-coverage: passes at 99.13%, above the 80% threshold.

Notes

  • The retired runner.context.serialise_manifest Fluent key was removed
    from src/localization/keys.rs and all 35 locales/*/messages.ftl
    catalogues in one coordinated change so the localisation audit stays
    balanced without suppressing dead-code warnings.
  • Generic keyword-based redaction is not used: after rendering, a secret
    may occur in an arbitrary string with no secret-bearing field name, so
    the fix removes the leak at the serialisation boundary instead.

Summary by Sourcery

Prevent rendered manifest contents from being emitted by verbose logging while retaining safe structural observability.

Bug Fixes:

  • Prevent verbose tracing from exposing secrets contained in rendered manifests through environment interpolation.

Enhancements:

  • Replace raw manifest dumps with bounded structural telemetry containing only aggregate manifest counts.
  • Remove the obsolete manifest serialization localization key from all locale catalogues.

Tests:

  • Add an end-to-end regression test confirming verbose generation omits rendered secrets while generated output still resolves them.

@coderabbitai

coderabbitai Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Warning

Your free Security trial is over. An organization admin can upgrade to Advanced for continuous pull request security review or dismiss this notice.


Comment @coderabbitai help to get the list of available commands.

@sourcery-ai

sourcery-ai Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

The PR closes the verbose logging secret leak by eliminating rendered-manifest JSON serialization, emitting only bounded structural counts through telemetry, and adding an end-to-end test that verifies env()-resolved secrets remain in generated output but never appear in CLI logs; associated dead localization entries are removed.

Sequence diagram for secret-safe verbose manifest telemetry

sequenceDiagram
    participant CLI
    participant Runner
    participant Manifest as NetsukeManifest
    participant Telemetry
    participant Output as NinjaOutput

    CLI->>Runner: generate_ninja_with_shell()
    Runner->>Manifest: load_manifest_with_stage_reporting()
    Manifest-->>Runner: rendered manifest
    Runner->>Telemetry: record_manifest_structure(manifest)
    Telemetry->>Telemetry: describe_counter()
    Telemetry->>Telemetry: trace_span(variable_count, macro_count, rule_count, action_count, target_count, default_count)
    Telemetry->>Telemetry: counter().increment(1)
    Runner->>Output: generate Ninja graph
    Output-->>CLI: generated file contains resolved values
    Runner-->>CLI: verbose logs contain structural counts only
Loading

File-Level Changes

Change Details Files
Remove rendered manifest serialization from verbose tracing and replace it with bounded structural telemetry.
  • Delete the DEBUG-gated pretty-printed AST logging and its serialization error path.
  • Record only fixed-vocabulary collection counts via a TRACE span and a once-described metrics counter.
  • Invoke structural telemetry immediately after manifest loading.
src/runner/graph_generation.rs
src/runner/manifest_structure_telemetry.rs
src/runner/mod.rs
Add end-to-end regression coverage proving secret values are not exposed by verbose generation.
  • Inject one sentinel through env() at variable, command, and script rendering sites.
  • Assert the sentinel is absent from stdout and stderr while remaining present in generated Ninja output.
  • Register the test in the logging-stderr integration test suite.
tests/data/env_secret_sites.yml
tests/logging_stderr/verbose_secret_absence.rs
tests/logging_stderr_tests.rs
Remove the obsolete manifest-serialization localization key from all catalogs.
  • Delete the unused Fluent key declaration.
  • Remove the corresponding translation entry from every locale catalog.
src/localization/keys.rs
locales/*/messages.ftl

Assessment against linked issues

Issue Objective Addressed Explanation
#649 Remove serialization and logging of the fully rendered manifest from normal --verbose/TRACE output so rendered commands, scripts, variables, descriptions, paths, and other manifest values cannot leak through tracing.
#649 Retain useful bounded structural diagnostics without emitting manifest text or rendered values.
#649 Add a regression test covering env()-derived values in command, script, and variable sites through the real verbose CLI path, verifying absence from stdout and stderr while preserving rendering behavior.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

codescene-access[bot]

This comment was marked as outdated.

Replace the raw AST JSON dump with bounded structural telemetry that
emits only fixed-vocabulary aggregate counts of manifest collections.
Retire the runner.context.serialise_manifest Fluent key across all
locales now that no call site remains.
Replace the retired raw AST dump with a fixed-vocabulary structural
telemetry module emitting only collection-size counts, add a
secret-absence regression test covering command, script, and variable
rendering sites, and retire the serialise-manifest localization key
across all locales.
codescene-access[bot]

This comment was marked as outdated.

@leynos
leynos force-pushed the feat/remove-secret-leaks-from-verbose-ast-tracing branch from 42a905e to e06f18c Compare September 3, 2026 01:11
codescene-access[bot]

This comment was marked as outdated.

@codescene-access codescene-access Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No quality gates enabled for this code.

@leynos

leynos commented Sep 8, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Please investigate the cause of the following issue using codegraph exploration and research, identify a fix and provide an AI coding agent prompt for the fix:

        FAIL [   0.403s] (1974/2493) netsuke-build::logging_stderr_tests verbose_secret_absence::verbose_generate_never_emits_rendered_secret
  stdout ───

    running 1 test
    test verbose_secret_absence::verbose_generate_never_emits_rendered_secret ... FAILED

    failures:

    failures:
        verbose_secret_absence::verbose_generate_never_emits_rendered_secret

    test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 18 filtered out; finished in 0.15s
    
  stderr ───
    Error: env('CI_SECRET') must still resolve in generated output

    Stack backtrace:
       0: std::backtrace_rs::backtrace::win64::trace
                 at /rustc/c54751567b19c4ceb08b0412d83529c2568cba8b/library\std\src\..\..\backtrace\src\backtrace\win64.rs:85
       1: std::backtrace_rs::backtrace::trace_unsynchronized
                 at /rustc/c54751567b19c4ceb08b0412d83529c2568cba8b/library\std\src\..\..\backtrace\src\backtrace\mod.rs:66
       2: std::backtrace::Backtrace::create
                 at /rustc/c54751567b19c4ceb08b0412d83529c2568cba8b/library\std\src\backtrace.rs:331
       3: std::backtrace::Backtrace::capture
                 at /rustc/c54751567b19c4ceb08b0412d83529c2568cba8b/library\std\src\backtrace.rs:296
       4: <anyhow::Error>::msg::<&str>
       5: anyhow::__private::format_err
       6: cap_primitives::fs::copy::copy
       7: <core::cell::RefCell<core::option::Option<tempfile::file::NamedTempFile>>>::borrow
       8: logging_stderr_tests::verbose_secret_absence::verbose_generate_never_emits_rendered_secret::{closure#0}
       9: <logging_stderr_tests::verbose_secret_absence::verbose_generate_never_emits_rendered_secret::{closure#0} as core::ops::function::FnOnce<()>>::call_once
      10: core::ops::function::FnOnce::call_once
                 at /rustc/c54751567b19c4ceb08b0412d83529c2568cba8b/library\core\src\ops\function.rs:250
      11: test::__rust_begin_short_backtrace<enum2$<core::result::Result<tuple$<>,alloc::string::String> >,enum2$<core::result::Result<tuple$<>,alloc::string::String> > (*)()>
                 at /rustc/c54751567b19c4ceb08b0412d83529c2568cba8b/library\test\src\lib.rs:733
      12: test::run_test_in_process
                 at /rustc/c54751567b19c4ceb08b0412d83529c2568cba8b/library\test\src\lib.rs:756
      13: test::run_test::closure$0
                 at /rustc/c54751567b19c4ceb08b0412d83529c2568cba8b/library\test\src\lib.rs:677
      14: test::run_test::closure$1
                 at /rustc/c54751567b19c4ceb08b0412d83529c2568cba8b/library\test\src\lib.rs:707
      15: std::sys::backtrace::__rust_begin_short_backtrace<test::run_test::closure_env$1,tuple$<> >
                 at /rustc/c54751567b19c4ceb08b0412d83529c2568cba8b/library\std\src\sys\backtrace.rs:166
      16: std::thread::lifecycle::spawn_unchecked::closure$1::closure$0
                 at /rustc/c54751567b19c4ceb08b0412d83529c2568cba8b/library\std\src\thread\lifecycle.rs:70
      17: core::panic::unwind_safe::impl$25::call_once
                 at /rustc/c54751567b19c4ceb08b0412d83529c2568cba8b/library\core\src\panic\unwind_safe.rs:275
      18: std::panicking::catch_unwind::do_call
                 at /rustc/c54751567b19c4ceb08b0412d83529c2568cba8b/library\std\src\panicking.rs:574
      19: std::panicking::catch_unwind
                 at /rustc/c54751567b19c4ceb08b0412d83529c2568cba8b/library\std\src\panicking.rs:542
      20: std::panic::catch_unwind
                 at /rustc/c54751567b19c4ceb08b0412d83529c2568cba8b/library\std\src\panic.rs:359
      21: std::thread::lifecycle::spawn_unchecked::closure$1
                 at /rustc/c54751567b19c4ceb08b0412d83529c2568cba8b/library\std\src\thread\lifecycle.rs:68
      22: core::ops::function::FnOnce::call_once<std::thread::lifecycle::spawn_unchecked::closure_env$1<test::run_test::closure_env$1,tuple$<> >,tuple$<> >
                 at /rustc/c54751567b19c4ceb08b0412d83529c2568cba8b/library\core\src\ops\function.rs:250
      23: std::sys::thread::windows::impl$0::new::thread_start
                 at /rustc/c54751567b19c4ceb08b0412d83529c2568cba8b/library\std\src\sys\thread\windows.rs:58
      24: BaseThreadInitThunk
      25: RtlUserThreadStart

  Cancelling due to test failure: 3 tests still running
        SLOW [> 60.000s] (─────────) netsuke-build::command_env_ui_tests cli_configuration_fixture_compiles
        PASS [   8.465s] (1975/2493) netsuke-build::logging_stderr_tests verbose::verbose_build_logs_resolved_ninja_program_for_any_valid_override
        PASS [  89.283s] (1976/2493) netsuke-build::command_env_ui_tests cli_configuration_fixture_compiles
        SLOW [> 60.000s] (─────────) netsuke-build::locale_stub_ui_tests harness_compiles_under_a_split_build_dir
        PASS [  92.928s] (1977/2493) netsuke-build::locale_stub_ui_tests harness_compiles_under_a_split_build_dir
  stdout ───

    running 1 test
    test harness_compiles_under_a_split_build_dir has been running for over 60 seconds
    2026-09-03T01:26:17.679738Z  INFO locale_stub_ui_tests: cargo build test_support completed elapsed_seconds=92.2049488
    2026-09-03T01:26:17.832074Z  INFO locale_stub_ui_tests: rustc metadata harness completed elapsed_seconds=0.1092156
    test harness_compiles_under_a_split_build_dir ... ok

    test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 28 filtered out; finished in 92.87s

Seek a systemic fix rather than tactical. Ask yourself, can this happen again or happen elsewhere? If so, think about a long term fix of the underlying issue.

@coderabbitai

coderabbitai Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== Candidate files =='
fd -t f . src tests | rg '(^|/)(graph_generation|manifest_structure_telemetry|verbose_secret_absence|logging_stderr|command_env).*\.rs$' || true

printf '%s\n' '== Affected-file outlines =='
for f in \
  src/runner/graph_generation.rs \
  src/runner/manifest_structure_telemetry.rs \
  tests/logging_stderr/verbose_secret_absence.rs
do
  if [ -f "$f" ]; then
    echo "--- $f"
    ast-grep outline "$f" --items all || true
  fi
done

printf '%s\n' '== Environment setup and generated-output assertions =='
rg -n -C 5 --type rust \
  '(Command::new|cargo_bin|assert_cmd|env\(|env_remove\(|CI_SECRET|generated output|output\(\)|read_to_string|Ninja)' \
  tests src

printf '%s\n' '== Test module wiring and platform conditionals =='
rg -n -C 4 --type rust \
  '(verbose_secret_absence|cfg\!\(windows\)|target_os\s*=\s*"windows"|windows)' \
  tests src

Length of output: 170678


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== Secret-absence regression =='
cat -n tests/logging_stderr/verbose_secret_absence.rs

printf '%s\n' '== Shared logging test support =='
cat -n tests/logging_stderr/support.rs

printf '%s\n' '== Test crate entry point and verbose sibling =='
sed -n '1,180p' tests/logging_stderr_tests.rs
sed -n '1,150p' tests/logging_stderr/verbose.rs

printf '%s\n' '== CLI integration helper definitions and uses =='
rg -n -C 6 --type rust \
  '(fn\s+(netsuke|command|run_|assert_).*|CommandCargoExt|cargo_bin\(|assert\(\)|get_output\()' \
  tests test_support src

printf '%s\n' '== env() implementation and comparable fixture coverage =='
rg -n -C 5 --type rust \
  '(env\(\x27|env\(\x22|fn\s+.*env|ENV.*FILTER|Environment.*filter)' \
  src/stdlib src/manifest tests \
  -g '*.rs'

Length of output: 170388


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== Exact regression source =='
sed -n '1,120p' tests/logging_stderr/verbose_secret_absence.rs

printf '%s\n' '== Locate and inspect CLI runner helper =='
helper=$(fd -t f -a 'netsuke.rs' test_support tests | head -n 1 || true)
if [ -z "$helper" ]; then
  rg -n -l --type rust 'fn\s+run_netsuke_in_with_env\b' test_support tests
else
  echo "--- $helper"
  rg -n -C 35 --type rust 'fn\s+run_netsuke_in_with_env\b' "$helper"
fi

printf '%s\n' '== Generate dispatch semantics =='
sed -n '59,100p' src/runner/dispatch.rs

printf '%s\n' '== Manifest loading environment boundary =='
rg -n -C 14 --type rust \
  '(process_env_reader\(|from_path_with_policy_and_env\(|from_path.*env_reader|load_manifest)' \
  src/runner src/manifest

printf '%s\n' '== Existing end-to-end env interpolation tests =='
rg -n -C 20 --type rust \
  '(run_netsuke_in_with_env\(|NETSUKE_TEST_ENV|CI_SECRET|env\(\x27.*\x27\).*(generate|output))' \
  tests \
  -g '*.rs'

Length of output: 96989


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== Isolated Netsuke CLI helper =='
cat -n test_support/src/netsuke/mod.rs

printf '%s\n' '== Secret-site fixture =='
cat -n tests/data/env_secret_sites.yml

printf '%s\n' '== Windows/PowerShell generated command rendering =='
ast-grep outline src/ninja_gen_recipe_shell.rs --items all
sed -n '1,210p' src/ninja_gen_recipe_shell.rs

printf '%s\n' '== Recipe and variable Ninja emission =='
rg -n -C 10 --type rust \
  '(command_value\(|RecipeShell|vars|variables|description|escape_ninja_value)' \
  src/ninja_gen* src/ir \
  -g '*.rs'

printf '%s\n' '== CLI helper contract documentation and tests =='
rg -n -C 15 --type rust \
  '(run_netsuke_in_with_env|env_clear\(\)|NetsukeRun)' \
  test_support tests \
  -g '*.rs'

Length of output: 182348


@leynos

Fix the regression test. Do not change the secret-leak remediation.

Cause

tests/logging_stderr/verbose_secret_absence.rs makes two assertions:

  1. CI_SECRET must not occur in stdout or stderr.
  2. The literal sentinel must occur in build.ninja.

The first assertion is correct.

The second assertion is not portable. On Windows, RecipeShell::host_default() selects RecipeShell::PowerShell. The Ninja generator serializes PowerShell recipes with -EncodedCommand. That payload is Base64-encoded. Therefore, the generated build.ninja file contains an encoded representation of the rendered command or script, not the plaintext value of CI_SECRET.

The test failure proves this representation difference. It does not show that env('CI_SECRET') failed to resolve.

The subprocess environment is correct:

  • test_support/src/netsuke/mod.rs calls env_clear().
  • The same helper then applies each extra_env entry.
  • The test supplies ("CI_SECRET", SENTINEL).
  • src/runner/graph_generation.rs uses the full build manifest loader.
  • The full build manifest loader permits env() interpolation.

Systemic fix

Separate semantic assertions from serialization assertions.

Keep the binary-level test responsible for the security boundary:

  • Run the real --verbose generate command.
  • Assert that stdout and stderr do not contain the sentinel.
  • Assert that generation succeeds.
  • Assert that the expected Ninja structure exists.

Move the positive interpolation assertion to a representation-aware boundary:

  • For POSIX output, inspect the generated Ninja text for the plaintext sentinel.
  • For PowerShell output, extract and decode every -EncodedCommand payload and every PowerShell response-file payload before inspecting for the sentinel.
  • Reuse or extract the existing PowerShell decoding logic from tests/ninja_gen_tests/power_shell.rs.
  • Assert that at least one decoded or plaintext generated recipe contains the sentinel.

This fix applies to all future generated-output tests. A generated format can legitimately encode, escape, hash, compress, or externalize rendered content. Tests must validate semantic content after decoding the format-specific representation. Tests must not require a plaintext representation unless plaintext is part of the public contract.

Do not force NETSUKE_WINDOWS_SHELL=bash for this test. That would hide the Windows production path and create a tactical test-only workaround.

AI coding agent prompt

Fix the Windows failure in `tests/logging_stderr/verbose_secret_absence.rs`.

Do not modify the production secret-leak fix in `src/runner/graph_generation.rs` or weaken the stdout/stderr secret-absence assertions.

Cause:
- The test correctly injects `CI_SECRET` through `test_support::netsuke::run_netsuke_in_with_env`.
- The production generate path uses the full manifest loader, so `env('CI_SECRET')` resolves.
- On Windows, Netsuke uses `RecipeShell::PowerShell`.
- PowerShell recipe serialization uses `-EncodedCommand`, so `build.ninja` contains Base64-encoded recipe content instead of the plaintext sentinel.
- The existing assertion `generated.contains(SENTINEL)` is invalid on Windows even when rendering succeeds.

Implement a representation-aware generated-recipe assertion.

Requirements:
1. Keep the existing end-to-end command:
   `--verbose generate --output build.ninja`
2. Keep assertions that:
   - the command succeeds;
   - stdout does not contain `SENTINEL`;
   - stderr does not contain `SENTINEL`.
3. Add a helper in the logging stderr test support area, or a narrowly scoped test helper, that determines whether generated Ninja semantics contain a specified string:
   - On non-Windows, inspect generated Ninja text directly.
   - On Windows, inspect plaintext Ninja bindings and decode each PowerShell `-EncodedCommand` payload before matching.
   - Also support PowerShell response-file content if the renderer can emit it. Decode the response-file representation before matching.
4. Reuse existing PowerShell test decoding conventions from `tests/ninja_gen_tests/power_shell.rs`. Do not duplicate incompatible Base64 or UTF-16LE decoding logic.
5. Replace the direct `generated.contains(SENTINEL)` assertion with this helper.
6. Give the failure message enough context to identify the active recipe-shell representation. Do not include the sentinel or generated manifest contents in failure output. This test protects secret handling.
7. Add focused tests for the helper:
   - plaintext Ninja content matches on non-Windows;
   - a PowerShell `-EncodedCommand` containing the sentinel matches after decoding;
   - encoded content does not create false plaintext matches;
   - malformed encoded payloads return a controlled test error without echoing the payload.
8. Keep production code unchanged unless the test cannot access an existing safe rendering abstraction.
9. Run the focused regression test on Windows and Linux. Run the relevant Ninja generator PowerShell tests.

The final test contract must be:
- verbose logs never expose rendered secrets;
- generated Ninja preserves the rendered recipe semantics;
- the test does not assume rendered secrets are stored in plaintext.

You are interacting with an AI system.

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.

Remove rendered manifest values from verbose logging

1 participant