feat(security): add TypeScript audit chain verifier#1989
feat(security): add TypeScript audit chain verifier#1989gemini2026 wants to merge 2 commits intoNVIDIA:mainfrom
Conversation
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.
📝 WalkthroughWalkthroughAdded a TypeScript audit-chain verifier and JSONL query utilities that validate SHA-256 hash-chained audit logs, expose Changes
Sequence DiagramsequenceDiagram
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[]
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (5)
.agents/skills/nemoclaw-user-reference/SKILL.md.agents/skills/nemoclaw-user-reference/references/audit-verifier.mddocs/reference/audit-verifier.mdnemoclaw/src/security/audit-verifier.test.tsnemoclaw/src/security/audit-verifier.ts
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
There was a problem hiding this comment.
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
📒 Files selected for processing (1)
nemoclaw/src/security/audit-verifier.ts
| 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)}`, | ||
| }; | ||
| } | ||
|
|
There was a problem hiding this comment.
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.
| 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); | ||
|
|
There was a problem hiding this comment.
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.
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 validatesprev_hashlinkage 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
npx vitest run nemoclaw/src/security/audit-verifier.test.ts)tsc --noEmitcleanThree new files, no changes to existing NemoClaw code.
Signed-off-by: Anton Mishel [email protected]
Summary by CodeRabbit
New Features
Documentation
Tests