Skip to content

feat(security): add tamper-evident audit chain logger#892

Open
gemini2026 wants to merge 1 commit intoNVIDIA:mainfrom
gemini2026:feat/audit-chain
Open

feat(security): add tamper-evident audit chain logger#892
gemini2026 wants to merge 1 commit intoNVIDIA:mainfrom
gemini2026:feat/audit-chain

Conversation

@gemini2026
Copy link

@gemini2026 gemini2026 commented Mar 25, 2026

Closes #891

Summary

Adds a tamper-evident, hash-chained JSONL audit logger to NemoClaw's security toolkit.

Hash chain:

  • Each entry includes a SHA-256 hash of the previous entry's payload
  • Modifying any entry breaks all downstream hashes, making tampering detectable
  • Chain resumes from existing file on startup (reads last entry for seq/prev_hash/chain_id)

Entry structure:

  • seq — monotonic sequence number
  • chain_id — 24-char random hex identifier unique per audit instance
  • prev_hash — hash of previous entry (empty for first)
  • entry_hashsha256:<64 hex chars> computed from canonical JSON payload
  • type — event type string
  • time — ISO 8601 UTC timestamp
  • data — arbitrary JSON payload

API:

  • AuditLogger class with log(type, data) method
  • verifyChain(path) — validates entire hash chain, detects any tampering or gaps
  • exportEntries(path, since, limit?) — export entries by sequence number range
  • tailEntries(path, n?) — return last N entries (defaults to 50)

Hardening:

  • Input validation on all public functions
  • Per-operation error handling in verify/export (malformed lines skipped gracefully)
  • readonly on all output interface fields
  • Append-only JSONL persistence via appendFileSync

Full Vitest test coverage (35 tests) including tamper detection, error paths, boundary conditions, and rapid-write stress test.

Self-contained under nemoclaw/src/security/ with no dependencies on existing NemoClaw internals.
Reference documentation at docs/reference/audit-chain.md.

Test plan

  • cd nemoclaw && npx vitest run src/security/audit-chain.test.ts — 35/35 tests pass
  • npm test — all tests pass, 0 regressions
  • tsc --noEmit — clean type check
  • make check — all linters and hooks pass
  • Verified hash chain links correctly across entries
  • Verified tamper detection catches modified data and broken prev_hash links
  • Verified chain resumes correctly from existing file
  • Verified exportEntries filters by sequence number and respects limit
  • Verified tailEntries returns last N with default of 50
  • Verified error paths: malformed JSONL, empty inputs, nonexistent files
  • Verified boundary conditions: single entry, large payloads, 100-entry rapid writes

Summary by CodeRabbit

  • New Features

    • Added tamper-evident audit logging with integrity verification.
    • Ability to verify audit chains and detect unauthorized modifications.
    • Query functions to export and retrieve audit entries.
  • Documentation

    • Added reference documentation for the audit logging module.
  • Tests

    • Added comprehensive test suite for audit logging functionality.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 25, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 35fef780-9ba5-47da-8d7d-62afcd6dc514

📥 Commits

Reviewing files that changed from the base of the PR and between 66b520e and f68e97c.

📒 Files selected for processing (3)
  • docs/reference/audit-chain.md
  • nemoclaw/src/security/audit-chain.test.ts
  • nemoclaw/src/security/audit-chain.ts
✅ Files skipped from review due to trivial changes (2)
  • docs/reference/audit-chain.md
  • nemoclaw/src/security/audit-chain.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • nemoclaw/src/security/audit-chain.ts

📝 Walkthrough

Walkthrough

Introduces a tamper-evident, append-only audit logging module with SHA-256 hash chaining persistence via JSONL format. Includes a new AuditLogger class with resume capability, verification function to detect tampering, and query APIs for filtering and tail operations. New module has no impact on existing code.

Changes

Cohort / File(s) Summary
Audit Chain Implementation
nemoclaw/src/security/audit-chain.ts
Implements append-only JSONL audit logger with SHA-256 hash chaining, directory initialization, and resume-from-last-entry semantics. Exports AuditLogger class, verifyChain() for integrity validation, and exportEntries()/tailEntries() for log querying.
Audit Chain Tests
nemoclaw/src/security/audit-chain.test.ts
Comprehensive Vitest suite covering initialization, hash chaining across restarts, tampering detection (modified data, broken linkage, malformed JSON), edge cases (empty/missing files, empty paths), and sequential write performance.
Documentation
docs/reference/audit-chain.md
Reference documentation describing entry structure, field semantics, chain initialization rules, API specifications with examples, and TypeScript interfaces.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~15 minutes

Poem

🐰 Hash chains link each hop with care,
A tamper trail through the air,
SHA-256 seals the trail so tight,
No sneaky bits escape this sight!
Append forever, truth takes flight. 🌟

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding a tamper-evident audit chain logger in the security module.
Linked Issues check ✅ Passed All objectives from issue #891 are met: hash-chained JSONL implementation, entry schema with SHA-256 hashing, verifyChain/exportEntries/tailEntries utilities, robust error handling, tests covering tampering scenarios, and docs included.
Out of Scope Changes check ✅ Passed All changes are scoped to the audit-chain implementation: new security module files, tests, and documentation. No existing NemoClaw code was modified.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (7)
docs/reference/audit-chain.md (3)

39-40: One sentence per line.

Lines 39–40 contain two sentences on the same line.
Split them for cleaner diffs.

The `chain_id` is a random 24-character hex string generated when the logger creates a new file.
It remains constant across all entries in a single chain and persists when the logger resumes from an existing file.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/reference/audit-chain.md` around lines 39 - 40, The line describing
`chain_id` currently contains two sentences on the same line—split it into two
separate lines so each sentence is its own line: one line stating "The
`chain_id` is a random 24-character hex string generated when the logger creates
a new file." and the next line stating "It remains constant across all entries
in a single chain and persists when the logger resumes from an existing file."
so diffs are clean.

85-86: One sentence per line.

Multiple sentences appear on a single line here.
Split each sentence onto its own line for readable diffs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/reference/audit-chain.md` around lines 85 - 86, The documentation line
describing the constructor currently contains multiple sentences on one line;
split each sentence into its own line so diffs are readable—e.g., separate "The
constructor creates parent directories if they do not exist." and "If the file
already contains entries, the logger resumes the chain from the last entry."
into two lines under the constructor documentation.

113-114: One sentence per line.

Lines 113–114 and 119–120 each have two sentences on the same line.
Split them for cleaner diffs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/reference/audit-chain.md` around lines 113 - 114, Split the two
combined-sentence lines so each sentence is on its own line: change the line
that currently contains "When `limit` is omitted or zero, all matching entries
are returned. Malformed lines are silently skipped." into two lines ("When
`limit` is omitted or zero, all matching entries are returned." and "Malformed
lines are silently skipped.") and do the same for the other occurrence around
lines 119–120; ensure each sentence is a separate line to keep diffs clean.
nemoclaw/src/security/audit-chain.test.ts (1)

558-578: Test name "concurrent writes" is misleading.

This test performs rapid sequential writes from a single thread, not concurrent writes from multiple threads.
Since appendFileSync is synchronous, the writes are serialized.
Consider renaming to "rapid sequential writes" or similar to accurately reflect what's being tested.

True concurrency testing would require multiple worker threads writing simultaneously.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nemoclaw/src/security/audit-chain.test.ts` around lines 558 - 578, The test
title "concurrent writes" is misleading because it performs serialized writes
using AuditLogger (which uses appendFileSync); rename the describe or it block
to accurately reflect the behavior (e.g., change describe("concurrent writes")
or it("handles rapid sequential writes without data loss") to "rapid sequential
writes" or similar) so the test name matches the implementation, referencing the
AuditLogger, appendFileSync, readEntries, and verifyChain symbols to locate the
test to update.
nemoclaw/src/security/audit-chain.ts (2)

346-350: Consider a structured logging approach instead of console.error.

Using console.error during readTailState can produce unexpected output in production or library consumers.
Options:

  1. Return a diagnostic in the TailState result for the caller to handle.
  2. Accept an optional logger callback.
  3. Silently skip (current behavior minus the log).

Given this is a security module where malformed lines could indicate tampering, silent skipping may be appropriate since verifyChain handles detection separately.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nemoclaw/src/security/audit-chain.ts` around lines 346 - 350, The
console.error in readTailState should be removed and replaced with a structured
diagnostic approach: update the readTailState signature to either accept an
optional logger callback (e.g., logger?: (level: string, msg: string, meta?:
any) => void) or add a diagnostic field on the returned TailState (e.g.,
diagnostics: Array<{line: string, error: string}>), then use that mechanism
instead of console.error when skipping a malformed line; ensure callers (or
verifyChain) can inspect diagnostics if needed and that default behavior remains
silent if no logger is provided.

110-113: No file locking for multi-process writes.

appendFileSync provides atomicity at the OS level for small writes, but concurrent appends from multiple processes can interleave or corrupt entries on some platforms.

If multi-process logging is a future requirement, consider:

  • Advisory file locking (flock equivalent)
  • A separate log aggregator process
  • A database-backed alternative

For single-process use as currently tested, this is fine.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nemoclaw/src/security/audit-chain.ts` around lines 110 - 113, The current
append in AuditChain.write (appendFileSync(this.path, ...)) can interleave or
corrupt when multiple processes write; replace the direct append with a
multi-process-safe approach such as acquiring an advisory lock before writing
(e.g., flock via a native binding or fs-ext's flockSync), or implement a
dedicated log-writer/aggregator process or DB-backed store; ensure the code
acquires the lock on this.path (or on a separate .lock file), performs the
append of JSON.stringify(entry)+"\n", updates this.seq and this.prevHash only
after a successful write, and always releases the lock even on error.
docs/reference/injection-scanner.md (1)

23-25: One sentence per line.

Lines 23-24 contain multiple sentences on single lines. Per coding guidelines, each sentence should be on its own line to make diffs readable.

 The injection scanner detects prompt injection patterns in agent tool inputs and outputs.
-It applies text normalization and pattern matching to identify attempts to override system prompts, inject instructions, manipulate tools, or exfiltrate data.
+It applies text normalization and pattern matching to identify attempts to override system prompts, inject instructions, manipulate tools, or exfiltrate data.

(The second sentence is already on its own line, but verify the source has one sentence per line throughout.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/reference/injection-scanner.md` around lines 23 - 25, The two sentences
in the README paragraph should be split so each sentence is on its own line:
replace the single line containing "The injection scanner detects prompt
injection patterns in agent tool inputs and outputs. It applies text
normalization and pattern matching to identify attempts to override system
prompts, inject instructions, manipulate tools, or exfiltrate data." with two
lines — one containing "The injection scanner detects prompt injection patterns
in agent tool inputs and outputs." and a second containing "It applies text
normalization and pattern matching to identify attempts to override system
prompts, inject instructions, manipulate tools, or exfiltrate data."; also scan
the rest of the document to ensure every sentence follows the
one-sentence-per-line guideline.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/reference/injection-scanner.md`:
- Around line 113-115: The documentation for maxSeverity incorrectly states its
return type as `Severity | ""`; update the doc to match the implementation and
tests by changing the return type to `Severity | null` and the description to
say it returns null for an empty findings array; reference the maxSeverity
function and the existing test that asserts null to ensure consistency.

---

Nitpick comments:
In `@docs/reference/audit-chain.md`:
- Around line 39-40: The line describing `chain_id` currently contains two
sentences on the same line—split it into two separate lines so each sentence is
its own line: one line stating "The `chain_id` is a random 24-character hex
string generated when the logger creates a new file." and the next line stating
"It remains constant across all entries in a single chain and persists when the
logger resumes from an existing file." so diffs are clean.
- Around line 85-86: The documentation line describing the constructor currently
contains multiple sentences on one line; split each sentence into its own line
so diffs are readable—e.g., separate "The constructor creates parent directories
if they do not exist." and "If the file already contains entries, the logger
resumes the chain from the last entry." into two lines under the constructor
documentation.
- Around line 113-114: Split the two combined-sentence lines so each sentence is
on its own line: change the line that currently contains "When `limit` is
omitted or zero, all matching entries are returned. Malformed lines are silently
skipped." into two lines ("When `limit` is omitted or zero, all matching entries
are returned." and "Malformed lines are silently skipped.") and do the same for
the other occurrence around lines 119–120; ensure each sentence is a separate
line to keep diffs clean.

In `@docs/reference/injection-scanner.md`:
- Around line 23-25: The two sentences in the README paragraph should be split
so each sentence is on its own line: replace the single line containing "The
injection scanner detects prompt injection patterns in agent tool inputs and
outputs. It applies text normalization and pattern matching to identify attempts
to override system prompts, inject instructions, manipulate tools, or exfiltrate
data." with two lines — one containing "The injection scanner detects prompt
injection patterns in agent tool inputs and outputs." and a second containing
"It applies text normalization and pattern matching to identify attempts to
override system prompts, inject instructions, manipulate tools, or exfiltrate
data."; also scan the rest of the document to ensure every sentence follows the
one-sentence-per-line guideline.

In `@nemoclaw/src/security/audit-chain.test.ts`:
- Around line 558-578: The test title "concurrent writes" is misleading because
it performs serialized writes using AuditLogger (which uses appendFileSync);
rename the describe or it block to accurately reflect the behavior (e.g., change
describe("concurrent writes") or it("handles rapid sequential writes without
data loss") to "rapid sequential writes" or similar) so the test name matches
the implementation, referencing the AuditLogger, appendFileSync, readEntries,
and verifyChain symbols to locate the test to update.

In `@nemoclaw/src/security/audit-chain.ts`:
- Around line 346-350: The console.error in readTailState should be removed and
replaced with a structured diagnostic approach: update the readTailState
signature to either accept an optional logger callback (e.g., logger?: (level:
string, msg: string, meta?: any) => void) or add a diagnostic field on the
returned TailState (e.g., diagnostics: Array<{line: string, error: string}>),
then use that mechanism instead of console.error when skipping a malformed line;
ensure callers (or verifyChain) can inspect diagnostics if needed and that
default behavior remains silent if no logger is provided.
- Around line 110-113: The current append in AuditChain.write
(appendFileSync(this.path, ...)) can interleave or corrupt when multiple
processes write; replace the direct append with a multi-process-safe approach
such as acquiring an advisory lock before writing (e.g., flock via a native
binding or fs-ext's flockSync), or implement a dedicated log-writer/aggregator
process or DB-backed store; ensure the code acquires the lock on this.path (or
on a separate .lock file), performs the append of JSON.stringify(entry)+"\n",
updates this.seq and this.prevHash only after a successful write, and always
releases the lock even on error.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a1657469-f7fc-48b6-b329-fb8a4cbfad82

📥 Commits

Reviewing files that changed from the base of the PR and between b2164e7 and d20cb17.

⛔ Files ignored due to path filters (2)
  • nemoclaw/package-lock.json is excluded by !**/package-lock.json
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (10)
  • .github/PULL_REQUEST_TEMPLATE.md
  • .github/workflows/pr.yaml
  • Dockerfile
  • docs/reference/audit-chain.md
  • docs/reference/injection-scanner.md
  • install.sh
  • nemoclaw/src/security/audit-chain.test.ts
  • nemoclaw/src/security/audit-chain.ts
  • nemoclaw/src/security/injection-scanner.test.ts
  • nemoclaw/src/security/injection-scanner.ts
💤 Files with no reviewable changes (1)
  • .github/workflows/pr.yaml

Dockerfile Outdated
Comment on lines 29 to 32
RUN curl -fsSL -o /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.19/gosu-amd64" \
&& echo "52c8749d0142edd234e9d6bd5237dff2d81e71f43537e2f4f66f75dd4b243dd0 /usr/local/bin/gosu" | sha256sum -c - \
&& chmod +x /usr/local/bin/gosu \
&& gosu --version
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat Dockerfile

Repository: NVIDIA/NemoClaw

Length of output: 9506


🏁 Script executed:

# Check what gosu releases are available for version 1.19
web_search
gosu 1.19 releases github tianon available architectures

Repository: NVIDIA/NemoClaw

Length of output: 152


🌐 Web query:

gosu 1.19 github releases available architectures arm64

💡 Result:

Yes, gosu version 1.19 on GitHub (tianon/gosu releases) provides binaries for multiple architectures, including arm64. The release has 22 assets, which include architecture-specific binaries like gosu-amd64 and corresponding ones for other arches such as arm64, armhf, ppc64le, s390x, riscv64, mips64el, and more, based on the standard Debian dpkg architecture naming convention used in the project (e.g., gosu-arm64). This is confirmed by the release page listing assets and the project's build process documented in Debian sources, which explicitly builds for arm64 (GOARCH=arm64) among others. Wolfi also provides gosu-1.19-r0.apk for aarch64.

Citations:


Hardcoded amd64 gosu binary breaks arm64 builds.

Lines 29–32 unconditionally download gosu-amd64. When the build runs on an arm64 builder or with --platform=linux/arm64, line 32 will fail with "exec format error" trying to execute an incompatible binary. gosu 1.19 provides arm64 binaries (gosu-arm64). Restore architecture-aware selection to download the correct binary for the build architecture, or explicitly pin both FROM stages to --platform=linux/amd64 if arm64 support is intentionally dropped.

Comment on lines +113 to +115
### `maxSeverity(findings: Finding[]): Severity | ""`

The highest severity level present in the findings array, or an empty string if the array is empty.
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Documentation-code mismatch: maxSeverity returns null, not "".

The documented return type Severity | "" does not match the implementation, which returns Severity | null. The test at injection-scanner.test.ts:623-625 confirms maxSeverity([]) returns null via .toBeNull(). Callers following this documentation would incorrectly check for an empty string instead of null.

📝 Proposed fix
-### `maxSeverity(findings: Finding[]): Severity | ""`
+### `maxSeverity(findings: Finding[]): Severity | null`

-The highest severity level present in the findings array, or an empty string if the array is empty.
+The highest severity level present in the findings array, or `null` if the array is empty.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
### `maxSeverity(findings: Finding[]): Severity | ""`
The highest severity level present in the findings array, or an empty string if the array is empty.
### `maxSeverity(findings: Finding[]): Severity | null`
The highest severity level present in the findings array, or `null` if the array is empty.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/reference/injection-scanner.md` around lines 113 - 115, The
documentation for maxSeverity incorrectly states its return type as `Severity |
""`; update the doc to match the implementation and tests by changing the return
type to `Severity | null` and the description to say it returns null for an
empty findings array; reference the maxSeverity function and the existing test
that asserts null to ensure consistency.

SHA-256 hash-chained JSONL audit logger with verify, export, and
tail utilities. Each entry includes the hash of the previous entry,
making any tampering detectable via the verifyChain function.

Includes sequence continuity and chain_id consistency checks to
detect block-deletion and splice attacks. Graceful handling of
malformed JSONL lines in all read paths.
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.

feat: tamper-evident audit chain for sandbox events

1 participant