Skip to content

feat(security): add TypeScript audit chain verifier#1989

Open
gemini2026 wants to merge 2 commits intoNVIDIA:mainfrom
gemini2026:feat/audit-chain-verifier
Open

feat(security): add TypeScript audit chain verifier#1989
gemini2026 wants to merge 2 commits intoNVIDIA:mainfrom
gemini2026:feat/audit-chain-verifier

Conversation

@gemini2026
Copy link
Copy Markdown

@gemini2026 gemini2026 commented Apr 16, 2026

Companion to #916. Adds the TypeScript read/verify/query side for the tamper-evident audit chain.

What this does

TypeScript module (nemoclaw/src/security/audit-verifier.ts) that reads and validates the JSONL audit chain written by the Python orchestrator in #916. Three functions:

  • verifyChain(path) — recomputes SHA-256 hashes and validates prev_hash linkage across the entire chain. Detects modified data, broken links, deleted entries, and spliced chains.
  • exportEntries(path, since, limit) — query entries by Unix timestamp with optional limit.
  • tailEntries(path, n) — return the last N entries (default 50).

Uses canonical JSON serialization (sorted keys, compact separators) to match Python's json.dumps(sort_keys=True) for cross-language hash verification.

Test plan

  • 30 tests passing (npx vitest run nemoclaw/src/security/audit-verifier.test.ts)
  • tsc --noEmit clean
  • Full plugin suite: 349 tests passing (no regressions)
  • All pre-commit hooks pass
  • Tamper scenarios: modified event data, broken prev_hash, modified hash, deleted entries, chain splicing
  • Cross-format verification: manually constructed entries matching Python's output format
  • Edge cases: empty files, missing files, malformed JSON, missing hash fields

Three new files, no changes to existing NemoClaw code.

Signed-off-by: Anton Mishel [email protected]

Summary by CodeRabbit

  • New Features

    • Added an audit-chain verification API to validate integrity of tamper-evident audit logs.
    • Added query utilities to export entries by timestamp and to retrieve recent (tail) entries.
  • Documentation

    • Added comprehensive reference docs with usage examples and metadata for the audit verifier.
  • Tests

    • Added end-to-end tests covering verification, export, tailing, and interoperability scenarios.

Companion to NVIDIA#916's Python audit logger. Provides the TypeScript
plugin with read-side APIs for the tamper-evident audit chain:

- verifyChain: validate hash integrity and prev_hash linkage
- exportEntries: query entries by timestamp with optional limit
- tailEntries: return the last N entries

Uses canonical JSON serialization (sorted keys, compact separators)
to match Python's json.dumps(sort_keys=True) for cross-language
hash verification.

30 tests covering chain validation, tamper detection (modified data,
broken links, deleted entries, spliced chains), query APIs, and
cross-format verification against the Python entry format.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 16, 2026

📝 Walkthrough

Walkthrough

Added a TypeScript audit-chain verifier and JSONL query utilities that validate SHA-256 hash-chained audit logs, expose verifyChain, exportEntries, and tailEntries, plus documentation and comprehensive Vitest tests for interoperability with the Python orchestrator.

Changes

Cohort / File(s) Summary
Documentation & Skill Reference
.agents/skills/nemoclaw-user-reference/SKILL.md, .agents/skills/nemoclaw-user-reference/references/audit-verifier.md, docs/reference/audit-verifier.md
New reference pages and skill metadata updates describing the NemoClaw TypeScript audit verifier, its API (verifyChain, exportEntries, tailEntries), data formats, usage examples, and frontmatter metadata.
Core Implementation
nemoclaw/src/security/audit-verifier.ts
New TypeScript module exporting AuditEntry, VerifyResult, and functions verifyChain(path), exportEntries(path, since, limit?), tailEntries(path, n?). Implements canonical JSON serialization, SHA‑256 hashing, chain linkage checks, malformed-line skipping, and detailed error reporting.
Tests
nemoclaw/src/security/audit-verifier.test.ts
Added Vitest suite covering verification (valid/invalid chains, tampering, gaps, malformed lines), exportEntries (timestamp filtering, limits, malformed skipping), tailEntries (default/explicit n, malformed skipping), and Python serialization interoperability.

Sequence Diagram

sequenceDiagram
    actor App as Application
    participant Verifier as Audit Verifier
    participant FS as File System
    participant Hasher as Hash Processor

    App->>Verifier: verifyChain(path)
    Verifier->>FS: read file (audit.jsonl)
    FS-->>Verifier: JSONL content
    Verifier->>Verifier: parse non-empty lines -> entries
    loop each entry
        Verifier->>Hasher: compute SHA-256 of canonical JSON (exclude "hash")
        Hasher-->>Verifier: hash result
        Verifier->>Verifier: check prev_hash linkage and compare hashes
    end
    Verifier-->>App: VerifyResult { valid, entries, error? }

    App->>Verifier: exportEntries(path, since, limit?)
    Verifier->>FS: read file
    FS-->>Verifier: JSONL content
    Verifier->>Verifier: parse, filter timestamp >= since, apply limit, skip malformed
    Verifier-->>App: AuditEntry[]

    App->>Verifier: tailEntries(path, n?)
    Verifier->>FS: read file
    FS-->>Verifier: JSONL content
    Verifier->>Verifier: parse, skip malformed, return last n entries
    Verifier-->>App: AuditEntry[]
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped through logs in midnight light,
Linking hashes, keeping records tight,
From genesis seed to latest line,
I guard each entry, link by sign.
A rabbit's nod — your audit's right. 🥕🔒

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.73% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main change: adding a TypeScript audit chain verifier module, which aligns with the primary objective of the PR.

✏️ 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
Copy Markdown
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 (1)
docs/reference/audit-verifier.md (1)

23-23: Title contains a colon.

Per the style guide: "No colons in titles." Consider rephrasing to "NemoClaw Audit Verifier for Tamper-Evident Audit Chain" or "TypeScript API for Tamper-Evident Audit Chain Verification".

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

In `@docs/reference/audit-verifier.md` at line 23, The title header "# NemoClaw
Audit Verifier: TypeScript API for Tamper-Evident Audit Chain" contains a colon
which violates the style guide; change the header to remove the colon and use
one of the suggested phrasings (for example "NemoClaw Audit Verifier for
Tamper-Evident Audit Chain" or "TypeScript API for Tamper-Evident Audit Chain
Verification") by updating the Markdown header text at the top of the file.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@nemoclaw/src/security/audit-verifier.ts`:
- Around line 187-188: The call to readFileSync that assigns to content (and
subsequent split into lines) in audit-verifier.ts lacks try/catch handling like
exportEntries/verifyChain; wrap the readFileSync and parsing (the code that sets
content and lines) in a try/catch, use the same error handling pattern as
exportEntries/verifyChain (log the error via the module/process logger and
either rethrow or return an appropriate failure result), and reference the
readFileSync usage and the variables content and lines so the change is applied
exactly around that block.
- Around line 143-144: The readFileSync call that produces content (and then
lines) lacks error handling; wrap the readFileSync(path, "utf-8") call in a
try/catch like verifyChain does (referencing verifyChain for the pattern), catch
filesystem/permission errors, log or include the original error when failing
(e.g., using the same audit logger or rethrow a new Error that includes the path
and the caught error), and ensure the function returns/propagates a consistent
failure result instead of letting an unhandled exception bubble up.

---

Nitpick comments:
In `@docs/reference/audit-verifier.md`:
- Line 23: The title header "# NemoClaw Audit Verifier: TypeScript API for
Tamper-Evident Audit Chain" contains a colon which violates the style guide;
change the header to remove the colon and use one of the suggested phrasings
(for example "NemoClaw Audit Verifier for Tamper-Evident Audit Chain" or
"TypeScript API for Tamper-Evident Audit Chain Verification") by updating the
Markdown header text at the top of the file.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: de63e87e-77c1-4c7a-aab5-f0022729e788

📥 Commits

Reviewing files that changed from the base of the PR and between 002ae01 and 8b5c5a8.

📒 Files selected for processing (5)
  • .agents/skills/nemoclaw-user-reference/SKILL.md
  • .agents/skills/nemoclaw-user-reference/references/audit-verifier.md
  • docs/reference/audit-verifier.md
  • nemoclaw/src/security/audit-verifier.test.ts
  • nemoclaw/src/security/audit-verifier.ts

Comment thread nemoclaw/src/security/audit-verifier.ts Outdated
Comment thread nemoclaw/src/security/audit-verifier.ts Outdated
Address CodeRabbit review:
- exportEntries and tailEntries now wrap readFileSync in try/catch
  for consistent error handling with verifyChain
- Add field-level JSDoc to AuditEntry and VerifyResult interfaces
  to meet the 80% docstring coverage threshold
Copy link
Copy Markdown
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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@nemoclaw/src/security/audit-verifier.ts`:
- Around line 160-173: The parsed value from JSON.parse in audit-verifier.ts
(variable entry) can be null or a primitive and accessing entry.timestamp will
throw or allow invalid items into result/entries; add a runtime shape guard
after parsing to ensure the value is a non-null object and matches the
AuditEntry shape (e.g., has timestamp and other required fields and timestamp is
a finite number/parsable date) and only then cast and push into result/entries;
update both the loop that reads lines (where entry is pushed into result) and
the code that constructs entries (around the entries push) to perform the same
validation and skip any malformed values.
- Around line 80-99: The verifyChain loop currently assumes JSON.parse returns
an object and dereferences entry.hash; guard against non-object parsed values
(e.g., null, number, string) by validating that the parsed value is a plain
object before treating it as an AuditEntry. In the loop in verifyChain, after
parsing lines[i] and before checking entry.hash, add a type/shape check (e.g.,
typeof entry === "object" && entry !== null) and return the structured
VerifyResult failure (valid: false, entries: count, error: ...) if the check
fails; keep using the AuditEntry identifier when describing the expected shape.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 88d508af-02db-4ba8-aae9-e3ef6cf73162

📥 Commits

Reviewing files that changed from the base of the PR and between 8b5c5a8 and 144feba.

📒 Files selected for processing (1)
  • nemoclaw/src/security/audit-verifier.ts

Comment on lines +80 to +99
for (let i = 0; i < lines.length; i++) {
let entry: AuditEntry;
try {
entry = JSON.parse(lines[i]) as AuditEntry;
} catch {
return {
valid: false,
entries: count,
error: `malformed JSON at line ${String(i + 1)}`,
};
}

if (typeof entry.hash !== "string") {
return {
valid: false,
entries: count,
error: `missing hash field at line ${String(i + 1)}`,
};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

verifyChain can throw on valid JSON that is not an object.

At Line 92, entry.hash is dereferenced without guarding parsed shape. A line like null parses successfully and then throws, which bypasses your structured VerifyResult failure path.

🛠️ Proposed fix
   for (let i = 0; i < lines.length; i++) {
-    let entry: AuditEntry;
+    let parsed: unknown;
     try {
-      entry = JSON.parse(lines[i]) as AuditEntry;
+      parsed = JSON.parse(lines[i]) as unknown;
     } catch {
       return {
         valid: false,
         entries: count,
         error: `malformed JSON at line ${String(i + 1)}`,
       };
     }
 
+    if (parsed === null || typeof parsed !== "object") {
+      return {
+        valid: false,
+        entries: count,
+        error: `invalid entry type at line ${String(i + 1)}`,
+      };
+    }
+
+    const entry = parsed as Partial<AuditEntry>;
     if (typeof entry.hash !== "string") {
       return {
         valid: false,
         entries: count,
         error: `missing hash field at line ${String(i + 1)}`,
       };
     }
+    if (typeof entry.prev_hash !== "string") {
+      return {
+        valid: false,
+        entries: count,
+        error: `missing prev_hash field at line ${String(i + 1)}`,
+      };
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nemoclaw/src/security/audit-verifier.ts` around lines 80 - 99, The
verifyChain loop currently assumes JSON.parse returns an object and dereferences
entry.hash; guard against non-object parsed values (e.g., null, number, string)
by validating that the parsed value is a plain object before treating it as an
AuditEntry. In the loop in verifyChain, after parsing lines[i] and before
checking entry.hash, add a type/shape check (e.g., typeof entry === "object" &&
entry !== null) and return the structured VerifyResult failure (valid: false,
entries: count, error: ...) if the check fails; keep using the AuditEntry
identifier when describing the expected shape.

Comment on lines +160 to +173
for (const line of lines) {
let entry: AuditEntry;
try {
entry = JSON.parse(line) as AuditEntry;
} catch {
continue;
}

if (entry.timestamp < since) {
continue;
}

result.push(entry);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Query helpers need runtime shape checks after JSON.parse.

At Line 168, entry.timestamp can throw when parsed value is null. Also, Line 210 can push primitives/non-entries into entries, violating the AuditEntry[] contract.

🛠️ Proposed fix
   for (const line of lines) {
-    let entry: AuditEntry;
+    let parsed: unknown;
     try {
-      entry = JSON.parse(line) as AuditEntry;
+      parsed = JSON.parse(line) as unknown;
     } catch {
       continue;
     }
+    if (
+      parsed === null ||
+      typeof parsed !== "object" ||
+      typeof (parsed as { timestamp?: unknown }).timestamp !== "number"
+    ) {
+      continue;
+    }
+    const entry = parsed as AuditEntry;
 
     if (entry.timestamp < since) {
       continue;
@@
   for (const line of lines) {
     try {
-      entries.push(JSON.parse(line) as AuditEntry);
+      const parsed = JSON.parse(line) as unknown;
+      if (
+        parsed !== null &&
+        typeof parsed === "object" &&
+        typeof (parsed as { timestamp?: unknown }).timestamp === "number" &&
+        typeof (parsed as { prev_hash?: unknown }).prev_hash === "string" &&
+        typeof (parsed as { hash?: unknown }).hash === "string"
+      ) {
+        entries.push(parsed as AuditEntry);
+      }
     } catch {
       continue;
     }
   }

Also applies to: 208-211

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

In `@nemoclaw/src/security/audit-verifier.ts` around lines 160 - 173, The parsed
value from JSON.parse in audit-verifier.ts (variable entry) can be null or a
primitive and accessing entry.timestamp will throw or allow invalid items into
result/entries; add a runtime shape guard after parsing to ensure the value is a
non-null object and matches the AuditEntry shape (e.g., has timestamp and other
required fields and timestamp is a finite number/parsable date) and only then
cast and push into result/entries; update both the loop that reads lines (where
entry is pushed into result) and the code that constructs entries (around the
entries push) to perform the same validation and skip any malformed values.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant