|
| 1 | +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. |
| 2 | +// SPDX-License-Identifier: Apache-2.0 |
| 3 | + |
| 4 | +import assert from "node:assert/strict"; |
| 5 | +import { execFileSync } from "node:child_process"; |
| 6 | +import fs from "node:fs"; |
| 7 | +import os from "node:os"; |
| 8 | +import path from "node:path"; |
| 9 | + |
| 10 | +import { afterEach, describe, expect, it } from "vitest"; |
| 11 | + |
| 12 | +import { HIGH_CONFIDENCE_PREFIXED_TOKEN_SPECS } from "../../../nemoclaw/src/security/secret-scanner.ts"; |
| 13 | +import { buildSandboxCredentialScanCommand } from "../live/cloud-inference-credential-boundary.ts"; |
| 14 | + |
| 15 | +const roots: string[] = []; |
| 16 | + |
| 17 | +afterEach(() => { |
| 18 | + for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true }); |
| 19 | +}); |
| 20 | + |
| 21 | +/** Create and track an isolated sandbox-state fixture root. */ |
| 22 | +function createScanRoot(): string { |
| 23 | + const root = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-cloud-credential-scan-")); |
| 24 | + roots.push(root); |
| 25 | + return root; |
| 26 | +} |
| 27 | + |
| 28 | +/** Write one text or binary sandbox-state fixture and return its path. */ |
| 29 | +function writeFixture(root: string, relativePath: string, body: string | Uint8Array): string { |
| 30 | + const rootPath = path.resolve(root); |
| 31 | + assert(!path.isAbsolute(relativePath), "Fixture path must be relative to the scan root"); |
| 32 | + const file = path.resolve(rootPath, relativePath); |
| 33 | + const relativeFile = path.relative(rootPath, file); |
| 34 | + assert( |
| 35 | + relativeFile !== ".." && !relativeFile.startsWith(`..${path.sep}`), |
| 36 | + "Fixture path must stay inside the scan root", |
| 37 | + ); |
| 38 | + fs.mkdirSync(path.dirname(file), { recursive: true }); |
| 39 | + fs.writeFileSync(file, body); |
| 40 | + return file; |
| 41 | +} |
| 42 | + |
| 43 | +/** Run the exact live credential scan command against a fixture root. */ |
| 44 | +function scan(root: string): string { |
| 45 | + return execFileSync("sh", ["-lc", buildSandboxCredentialScanCommand([root])], { |
| 46 | + encoding: "utf8", |
| 47 | + }); |
| 48 | +} |
| 49 | + |
| 50 | +describe("cloud inference sandbox credential scan", () => { |
| 51 | + it("rejects fixture paths outside the temporary scan root", () => { |
| 52 | + const root = createScanRoot(); |
| 53 | + |
| 54 | + expect(() => writeFixture(root, "../outside.txt", "outside\n")).toThrow( |
| 55 | + "Fixture path must stay inside the scan root", |
| 56 | + ); |
| 57 | + expect(() => writeFixture(root, path.join(root, "absolute.txt"), "outside\n")).toThrow( |
| 58 | + "Fixture path must be relative to the scan root", |
| 59 | + ); |
| 60 | + }); |
| 61 | + |
| 62 | + it("accepts npm dependency metadata that does not contain a credential value (#9363)", () => { |
| 63 | + const root = createScanRoot(); |
| 64 | + writeFixture( |
| 65 | + root, |
| 66 | + "npm/projects/openclaw-whatsapp/node_modules/thread-stream/test/ts/transpile.sh", |
| 67 | + 'echo "${npm_config_user_agent}"\n', |
| 68 | + ); |
| 69 | + writeFixture( |
| 70 | + root, |
| 71 | + "npm/projects/openclaw-msteams/node_modules/jwks-rsa/package.json", |
| 72 | + '{"scripts":{"release":"git tag $npm_package_version"}}\n', |
| 73 | + ); |
| 74 | + writeFixture(root, "configuration/token-key-path.txt", "ordinary dependency metadata\n"); |
| 75 | + |
| 76 | + expect(scan(root)).toBe(""); |
| 77 | + }); |
| 78 | + |
| 79 | + it.each([ |
| 80 | + ["NVIDIA", "nvapi-nemoclaw-credential-boundary-canary"], |
| 81 | + ["GitHub", `ghp_${"a".repeat(36)}`], |
| 82 | + ["GitHub fine-grained", `github_pat_${"a".repeat(15)}_${"b".repeat(14)}`], |
| 83 | + ["npm", `npm_${"b".repeat(36)}`], |
| 84 | + ])("reports only the path of a file that contains a %s credential canary", (_label, canary) => { |
| 85 | + const root = createScanRoot(); |
| 86 | + const leakedFile = writeFixture(root, "openclaw.json", `{"apiKey":"${canary}"}\n`); |
| 87 | + |
| 88 | + const output = scan(root); |
| 89 | + |
| 90 | + expect(output.trim()).toBe(leakedFile); |
| 91 | + expect(output).not.toContain(canary); |
| 92 | + }); |
| 93 | + |
| 94 | + it.each( |
| 95 | + HIGH_CONFIDENCE_PREFIXED_TOKEN_SPECS.flatMap(({ prefixes, minimumPayloadLength }) => |
| 96 | + prefixes.map((prefix) => [prefix, minimumPayloadLength] as const), |
| 97 | + ), |
| 98 | + )("enforces the shared minimum payload for %s", (prefix, minimumPayloadLength) => { |
| 99 | + const root = createScanRoot(); |
| 100 | + writeFixture(root, "short.txt", `${prefix}${"a".repeat(minimumPayloadLength - 1)}\n`); |
| 101 | + |
| 102 | + expect(scan(root)).toBe(""); |
| 103 | + |
| 104 | + const detectedFile = writeFixture( |
| 105 | + root, |
| 106 | + "minimum.txt", |
| 107 | + `${prefix}${"a".repeat(minimumPayloadLength)}\n`, |
| 108 | + ); |
| 109 | + expect(scan(root).trim()).toBe(detectedFile); |
| 110 | + }); |
| 111 | + |
| 112 | + it.each([ |
| 113 | + ["prefixed GitHub token", `prefixghp_${"a".repeat(36)}`], |
| 114 | + ["suffixed GitHub token", `ghp_${"a".repeat(36)}_suffix`], |
| 115 | + ["prefixed npm token", `prefixnpm_${"b".repeat(36)}`], |
| 116 | + ["suffixed npm token", `npm_${"b".repeat(36)}_suffix`], |
| 117 | + ])("does not report a token embedded in a larger identifier: %s", (_label, value) => { |
| 118 | + const root = createScanRoot(); |
| 119 | + writeFixture(root, "embedded.txt", `${value}\n`); |
| 120 | + |
| 121 | + expect(scan(root)).toBe(""); |
| 122 | + }); |
| 123 | + |
| 124 | + it("reports a credential canary in a NUL-containing file", () => { |
| 125 | + const root = createScanRoot(); |
| 126 | + const canary = "nvapi-nemoclaw-binary-credential-canary"; |
| 127 | + const leakedFile = writeFixture(root, "state.bin", Buffer.from(`prefix\0${canary}\n`)); |
| 128 | + |
| 129 | + const output = scan(root); |
| 130 | + |
| 131 | + expect(output.trim()).toBe(leakedFile); |
| 132 | + expect(output).not.toContain(canary); |
| 133 | + }); |
| 134 | +}); |
0 commit comments