From 4740d0ef8f4056e77061a1a240f18413d21a563e Mon Sep 17 00:00:00 2001 From: Deepak Jain Date: Mon, 17 Aug 2026 18:41:28 -0700 Subject: [PATCH 1/3] fix(e2e): detect credential values in sandbox scans Fixes #9363 Signed-off-by: Deepak Jain --- .../live/cloud-inference-secret-pattern.ts | 5 ++++ test/e2e/live/cloud-inference.test.ts | 5 ++-- ...cloud-inference-credential-pattern.test.ts | 26 +++++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 test/e2e/live/cloud-inference-secret-pattern.ts create mode 100644 test/e2e/support/cloud-inference-credential-pattern.test.ts diff --git a/test/e2e/live/cloud-inference-secret-pattern.ts b/test/e2e/live/cloud-inference-secret-pattern.ts new file mode 100644 index 00000000000..173a61fb5c4 --- /dev/null +++ b/test/e2e/live/cloud-inference-secret-pattern.ts @@ -0,0 +1,5 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +export const SANDBOX_SECRET_TOKEN_PATTERN = + "(nvapi-[A-Za-z0-9_-]{10,}|ghp_[A-Za-z0-9_-]{10,}|npm_[A-Za-z0-9]{10,})"; diff --git a/test/e2e/live/cloud-inference.test.ts b/test/e2e/live/cloud-inference.test.ts index fca3f231b0b..24817dd0469 100644 --- a/test/e2e/live/cloud-inference.test.ts +++ b/test/e2e/live/cloud-inference.test.ts @@ -32,6 +32,7 @@ import { parseCloudChatResponse, type PreContractExternalProviderFailure, } from "./cloud-inference-provider-skip.ts"; +import { SANDBOX_SECRET_TOKEN_PATTERN } from "./cloud-inference-secret-pattern.ts"; const REPO_SKILL_VALIDATOR = path.join( REPO_ROOT, @@ -305,7 +306,7 @@ async function expectSandboxCredentialBoundary( const secretScanCommand = [ "for dir in /sandbox/.openclaw /sandbox/.nemoclaw; do", ' [ -d "$dir" ] || continue', - ` matches=$(grep -rIlE 'nvapi-|ghp_|npm_' "$dir")`, + ` matches=$(grep -rIlE '${SANDBOX_SECRET_TOKEN_PATTERN}' "$dir")`, " scan_status=$?", ' case "$scan_status" in', ` 0) filtered=$(printf '%s\\n' "$matches" | grep -Ev '/policies/|/plugin-runtime-deps/|/extensions/[^/]+/(dist|node_modules)/')`, @@ -319,7 +320,7 @@ async function expectSandboxCredentialBoundary( " write_status=$?", ' case "$write_status" in 0) ;; *) exit "$write_status" ;; esac', " while IFS= read -r file; do", - ` matching_lines=$(grep -IE 'nvapi-|ghp_|npm_' "$file")`, + ` matching_lines=$(grep -IE '${SANDBOX_SECRET_TOKEN_PATTERN}' "$file")`, " match_status=$?", ' case "$match_status" in', ` 0) printf '%s' "$matching_lines" | grep -qv 'STRIPPED'`, diff --git a/test/e2e/support/cloud-inference-credential-pattern.test.ts b/test/e2e/support/cloud-inference-credential-pattern.test.ts new file mode 100644 index 00000000000..9953598ebd0 --- /dev/null +++ b/test/e2e/support/cloud-inference-credential-pattern.test.ts @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from "vitest"; + +import { SANDBOX_SECRET_TOKEN_PATTERN } from "../live/cloud-inference-secret-pattern.ts"; + +const credentialPattern = new RegExp(SANDBOX_SECRET_TOKEN_PATTERN, "u"); + +describe("cloud inference credential scan", () => { + it.each([ + "/sandbox/.openclaw/extensions/example/node_modules/thread-stream/test/ts/transpile.sh", + "/sandbox/.openclaw/extensions/example/node_modules/jwks-rsa/package.json", + "package-lock entry for npm_config_cache metadata", + ])("accepts dependency text without a credential value: %s", (value) => { + expect(value).not.toMatch(credentialPattern); + }); + + it.each([ + `NVIDIA_INFERENCE_API_KEY=nvapi-${"a".repeat(30)}`, + `GITHUB_TOKEN=ghp_${"b".repeat(36)}`, + `NPM_TOKEN=npm_${"c".repeat(36)}`, + ])("detects a credential-shaped value", (value) => { + expect(value).toMatch(credentialPattern); + }); +}); From 1a3443f6c6d48c71defc582413813c94ca5fa88f Mon Sep 17 00:00:00 2001 From: Deepak Jain Date: Mon, 17 Aug 2026 19:04:50 -0700 Subject: [PATCH 2/3] test(e2e): cover credential payload boundaries Signed-off-by: Deepak Jain --- .../cloud-inference-credential-pattern.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/e2e/support/cloud-inference-credential-pattern.test.ts b/test/e2e/support/cloud-inference-credential-pattern.test.ts index 9953598ebd0..e88cc9c5f39 100644 --- a/test/e2e/support/cloud-inference-credential-pattern.test.ts +++ b/test/e2e/support/cloud-inference-credential-pattern.test.ts @@ -23,4 +23,18 @@ describe("cloud inference credential scan", () => { ])("detects a credential-shaped value", (value) => { expect(value).toMatch(credentialPattern); }); + + it.each(["nvapi-", "ghp_", "npm_"])( + "rejects a nine-character payload for %s", + (prefix) => { + expect(`${prefix}${"a".repeat(9)}`).not.toMatch(credentialPattern); + }, + ); + + it.each(["nvapi-", "ghp_", "npm_"])( + "detects a ten-character payload for %s", + (prefix) => { + expect(`${prefix}${"a".repeat(10)}`).toMatch(credentialPattern); + }, + ); }); From e9284802f197e85998bc88e3cfff75cc7787e463 Mon Sep 17 00:00:00 2001 From: Deepak Jain Date: Mon, 17 Aug 2026 19:59:59 -0700 Subject: [PATCH 3/3] refactor(security): share provider token patterns Signed-off-by: Deepak Jain --- nemoclaw/src/security/secret-scanner.ts | 46 +++++++++++++++---- .../live/cloud-inference-secret-pattern.ts | 5 -- test/e2e/live/cloud-inference.test.ts | 6 +-- ...cloud-inference-credential-pattern.test.ts | 24 +++++----- 4 files changed, 54 insertions(+), 27 deletions(-) delete mode 100644 test/e2e/live/cloud-inference-secret-pattern.ts diff --git a/nemoclaw/src/security/secret-scanner.ts b/nemoclaw/src/security/secret-scanner.ts index 5e543702743..3f9b3022b69 100644 --- a/nemoclaw/src/security/secret-scanner.ts +++ b/nemoclaw/src/security/secret-scanner.ts @@ -21,16 +21,49 @@ interface SecretPattern { regex: RegExp; } +/** Provider token formats shared by the in-process and sandbox scanners. */ +export const HIGH_CONFIDENCE_PREFIXED_TOKEN_SPECS = [ + { + name: "NVIDIA API key", + prefixes: ["nvapi-"], + payloadCharacterClass: "A-Za-z0-9_-", + minimumPayloadLength: 20, + }, + { + name: "GitHub token", + prefixes: ["ghp_", "gho_", "ghu_", "ghs_", "ghr_", "github_pat_"], + payloadCharacterClass: "A-Za-z0-9", + minimumPayloadLength: 36, + }, + { + name: "npm token", + prefixes: ["npm_"], + payloadCharacterClass: "A-Za-z0-9", + minimumPayloadLength: 36, + }, +] as const; + +/** POSIX ERE for high-confidence provider tokens in sandbox shell scans. */ +export const HIGH_CONFIDENCE_PREFIXED_TOKEN_ERE = `(${HIGH_CONFIDENCE_PREFIXED_TOKEN_SPECS.flatMap( + ({ prefixes, payloadCharacterClass, minimumPayloadLength }) => + prefixes.map( + (prefix) => `${prefix}[${payloadCharacterClass}]{${minimumPayloadLength},}`, + ), +).join("|")})`; + const SECRET_PATTERNS: SecretPattern[] = [ - // NVIDIA - { name: "NVIDIA API key", regex: /\bnvapi-[A-Za-z0-9_-]{20,}\b/ }, + ...HIGH_CONFIDENCE_PREFIXED_TOKEN_SPECS.map( + ({ name, prefixes, payloadCharacterClass, minimumPayloadLength }) => ({ + name, + regex: new RegExp( + `\\b(?:${prefixes.join("|")})[${payloadCharacterClass}]{${minimumPayloadLength},}\\b`, + ), + }), + ), // OpenAI — exclude sk-ant- (Anthropic) to avoid double-matching { name: "OpenAI API key", regex: /\bsk-(?!ant-)[A-Za-z0-9_-]{20,}\b/ }, - // GitHub - { name: "GitHub token", regex: /\b(ghp|gho|ghu|ghs|ghr|github_pat)_[A-Za-z0-9]{36,}\b/ }, - // AWS { name: "AWS access key", regex: /\bAKIA[0-9A-Z]{16}\b/ }, { @@ -48,9 +81,6 @@ const SECRET_PATTERNS: SecretPattern[] = [ /(?<=(?:discord|bot|DISCORD_TOKEN|BOT_TOKEN|token)\s*[=:]\s*["']?)[A-Za-z0-9]{24}\.[A-Za-z0-9_-]{6}\.[A-Za-z0-9_-]{27,}/, }, - // npm - { name: "npm token", regex: /\bnpm_[A-Za-z0-9]{36,}\b/ }, - // Private keys (PEM) { name: "Private key", diff --git a/test/e2e/live/cloud-inference-secret-pattern.ts b/test/e2e/live/cloud-inference-secret-pattern.ts deleted file mode 100644 index 173a61fb5c4..00000000000 --- a/test/e2e/live/cloud-inference-secret-pattern.ts +++ /dev/null @@ -1,5 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -export const SANDBOX_SECRET_TOKEN_PATTERN = - "(nvapi-[A-Za-z0-9_-]{10,}|ghp_[A-Za-z0-9_-]{10,}|npm_[A-Za-z0-9]{10,})"; diff --git a/test/e2e/live/cloud-inference.test.ts b/test/e2e/live/cloud-inference.test.ts index 24817dd0469..0abedb4a5b6 100644 --- a/test/e2e/live/cloud-inference.test.ts +++ b/test/e2e/live/cloud-inference.test.ts @@ -32,7 +32,7 @@ import { parseCloudChatResponse, type PreContractExternalProviderFailure, } from "./cloud-inference-provider-skip.ts"; -import { SANDBOX_SECRET_TOKEN_PATTERN } from "./cloud-inference-secret-pattern.ts"; +import { HIGH_CONFIDENCE_PREFIXED_TOKEN_ERE } from "../../../nemoclaw/src/security/secret-scanner.ts"; const REPO_SKILL_VALIDATOR = path.join( REPO_ROOT, @@ -306,7 +306,7 @@ async function expectSandboxCredentialBoundary( const secretScanCommand = [ "for dir in /sandbox/.openclaw /sandbox/.nemoclaw; do", ' [ -d "$dir" ] || continue', - ` matches=$(grep -rIlE '${SANDBOX_SECRET_TOKEN_PATTERN}' "$dir")`, + ` matches=$(grep -rIlE '${HIGH_CONFIDENCE_PREFIXED_TOKEN_ERE}' "$dir")`, " scan_status=$?", ' case "$scan_status" in', ` 0) filtered=$(printf '%s\\n' "$matches" | grep -Ev '/policies/|/plugin-runtime-deps/|/extensions/[^/]+/(dist|node_modules)/')`, @@ -320,7 +320,7 @@ async function expectSandboxCredentialBoundary( " write_status=$?", ' case "$write_status" in 0) ;; *) exit "$write_status" ;; esac', " while IFS= read -r file; do", - ` matching_lines=$(grep -IE '${SANDBOX_SECRET_TOKEN_PATTERN}' "$file")`, + ` matching_lines=$(grep -IE '${HIGH_CONFIDENCE_PREFIXED_TOKEN_ERE}' "$file")`, " match_status=$?", ' case "$match_status" in', ` 0) printf '%s' "$matching_lines" | grep -qv 'STRIPPED'`, diff --git a/test/e2e/support/cloud-inference-credential-pattern.test.ts b/test/e2e/support/cloud-inference-credential-pattern.test.ts index e88cc9c5f39..a145ad93347 100644 --- a/test/e2e/support/cloud-inference-credential-pattern.test.ts +++ b/test/e2e/support/cloud-inference-credential-pattern.test.ts @@ -3,9 +3,9 @@ import { describe, expect, it } from "vitest"; -import { SANDBOX_SECRET_TOKEN_PATTERN } from "../live/cloud-inference-secret-pattern.ts"; +import { HIGH_CONFIDENCE_PREFIXED_TOKEN_ERE } from "../../../nemoclaw/src/security/secret-scanner.ts"; -const credentialPattern = new RegExp(SANDBOX_SECRET_TOKEN_PATTERN, "u"); +const credentialPattern = new RegExp(HIGH_CONFIDENCE_PREFIXED_TOKEN_ERE, "u"); describe("cloud inference credential scan", () => { it.each([ @@ -24,17 +24,19 @@ describe("cloud inference credential scan", () => { expect(value).toMatch(credentialPattern); }); - it.each(["nvapi-", "ghp_", "npm_"])( - "rejects a nine-character payload for %s", - (prefix) => { - expect(`${prefix}${"a".repeat(9)}`).not.toMatch(credentialPattern); - }, - ); + it.each([ + ["nvapi-", 20], + ["ghp_", 36], + ["npm_", 36], + ])("enforces the minimum payload for %s", (prefix, minimumPayloadLength) => { + expect(`${prefix}${"a".repeat(minimumPayloadLength - 1)}`).not.toMatch(credentialPattern); + expect(`${prefix}${"a".repeat(minimumPayloadLength)}`).toMatch(credentialPattern); + }); - it.each(["nvapi-", "ghp_", "npm_"])( - "detects a ten-character payload for %s", + it.each(["gho_", "ghu_", "ghs_", "ghr_", "github_pat_"])( + "detects a supported GitHub token prefix: %s", (prefix) => { - expect(`${prefix}${"a".repeat(10)}`).toMatch(credentialPattern); + expect(`${prefix}${"a".repeat(36)}`).toMatch(credentialPattern); }, ); });