Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/e2e-advisor.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ jobs:
- name: Install Pi SDK
run: |
PI_SDK_DIR="$RUNNER_TEMP/pi-sdk"
npm install --prefix "$PI_SDK_DIR" --ignore-scripts --no-save --package-lock=false "@earendil-works/pi-coding-agent@${PI_SDK_VERSION}"
npm install --prefix "$PI_SDK_DIR" --ignore-scripts --no-save --package-lock=false --before=2026-05-14T00:00:00.000Z "@earendil-works/pi-coding-agent@${PI_SDK_VERSION}"
rm -rf "$ADVISOR_DIR/node_modules"
ln -s "$PI_SDK_DIR/node_modules" "$ADVISOR_DIR/node_modules"

Expand Down
22 changes: 22 additions & 0 deletions src/lib/security/credential-filter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
stripCredentials,
sanitizeConfigFile,
isSensitiveFile,
shouldScanSnapshotFileForCredentials,
} from "./credential-filter.js";

describe("isCredentialField", () => {
Expand Down Expand Up @@ -146,3 +147,24 @@ describe("isSensitiveFile", () => {
expect(isSensitiveFile("SOUL.md")).toBe(false);
});
});

describe("shouldScanSnapshotFileForCredentials", () => {
it("scans runtime config and env files", () => {
expect(shouldScanSnapshotFileForCredentials("openclaw.json")).toBe(true);
expect(shouldScanSnapshotFileForCredentials("config.json")).toBe(true);
expect(shouldScanSnapshotFileForCredentials(".env")).toBe(true);
expect(shouldScanSnapshotFileForCredentials("service.env")).toBe(true);
});

it("skips dependency lockfiles that can contain non-secret package metadata matches", () => {
expect(shouldScanSnapshotFileForCredentials("package-lock.json")).toBe(false);
expect(shouldScanSnapshotFileForCredentials("npm-shrinkwrap.json")).toBe(false);
expect(shouldScanSnapshotFileForCredentials("yarn.lock")).toBe(false);
expect(shouldScanSnapshotFileForCredentials("pnpm-lock.yaml")).toBe(false);
});

it("applies lockfile exclusions to paths by basename", () => {
expect(shouldScanSnapshotFileForCredentials("/tmp/snapshot/package-lock.json")).toBe(false);
expect(shouldScanSnapshotFileForCredentials("/tmp/snapshot/config.json")).toBe(true);
});
});
25 changes: 25 additions & 0 deletions src/lib/security/credential-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
// They are injected at runtime via OpenShell's provider credential mechanism.

import { chmodSync, existsSync, readFileSync, writeFileSync } from "node:fs";
import { basename } from "node:path";

function parseJson<T>(text: string): T {
return JSON.parse(text);
Expand Down Expand Up @@ -41,6 +42,20 @@ const CREDENTIAL_PLACEHOLDER = "[STRIPPED_BY_MIGRATION]";
*/
export const CREDENTIAL_SENSITIVE_BASENAMES = new Set(["auth-profiles.json", "auth.json"]);

/**
* Dependency lockfiles may contain package metadata that resembles credentials
* (for example package names or tarball URLs with `sk-` substrings). They do
* not store NemoClaw runtime credentials and should not fail snapshot leak
* checks.
*/
const SNAPSHOT_CREDENTIAL_SCAN_EXCLUDED_BASENAMES = new Set([
"package-lock.json",
"npm-shrinkwrap.json",
"yarn.lock",
"pnpm-lock.yaml",
"pnpm-lock.yml",
]);

/**
* Credential field names that MUST be stripped from config files.
*/
Expand Down Expand Up @@ -151,3 +166,13 @@ export function sanitizeConfigFile(configPath: string): void {
export function isSensitiveFile(filename: string): boolean {
return CREDENTIAL_SENSITIVE_BASENAMES.has(filename.toLowerCase());
}

/**
* Return whether a snapshot file should be scanned for credential-looking
* payloads by coarse-grained E2E leak checks.
*/
export function shouldScanSnapshotFileForCredentials(filename: string): boolean {
const normalizedBasename = basename(filename).toLowerCase();
if (SNAPSHOT_CREDENTIAL_SCAN_EXCLUDED_BASENAMES.has(normalizedBasename)) return false;
return normalizedBasename === ".env" || normalizedBasename.endsWith(".env") || normalizedBasename.endsWith(".json");
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
12 changes: 6 additions & 6 deletions test/e2e/docs/parity-inventory.generated.json
Original file line number Diff line number Diff line change
Expand Up @@ -14905,47 +14905,47 @@
},
{
"script": "test/e2e/test-snapshot-commands.sh",
"line": 253,
"line": 260,
"text": "No credentials in snapshot directories",
"polarity": "pass",
"normalized_id": "no.credentials.in.snapshot.directories",
"mapping_status": "mapped"
},
{
"script": "test/e2e/test-snapshot-commands.sh",
"line": 255,
"line": 262,
"text": "Credentials found: $CRED_LEAKS",
"polarity": "fail",
"normalized_id": "credentials.found.cred.leaks",
"mapping_status": "mapped"
},
{
"script": "test/e2e/test-snapshot-commands.sh",
"line": 258,
"line": 265,
"text": "Backup directory missing: $BACKUP_DIR",
"polarity": "fail",
"normalized_id": "backup.directory.missing.backup.dir",
"mapping_status": "deferred"
},
{
"script": "test/e2e/test-snapshot-commands.sh",
"line": 266,
"line": 273,
"text": "snapshot help exited with code $_CAPTURE_RC: ${HELP_OUTPUT}",
"polarity": "fail",
"normalized_id": "snapshot.help.exited.with.code.capture.rc.help.output",
"mapping_status": "deferred"
},
{
"script": "test/e2e/test-snapshot-commands.sh",
"line": 271,
"line": 278,
"text": "snapshot help shows create/list/restore",
"polarity": "pass",
"normalized_id": "snapshot.help.shows.create.list.restore",
"mapping_status": "deferred"
},
{
"script": "test/e2e/test-snapshot-commands.sh",
"line": 273,
"line": 280,
"text": "snapshot help incomplete: ${HELP_OUTPUT}",
"polarity": "fail",
"normalized_id": "snapshot.help.incomplete.help.output",
Expand Down
9 changes: 8 additions & 1 deletion test/e2e/test-snapshot-commands.sh
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,14 @@ info "Phase 8: Checking snapshots for leaked credentials..."

BACKUP_DIR="$HOME/.nemoclaw/rebuild-backups/${SANDBOX_NAME}"
if [ -d "$BACKUP_DIR" ]; then
CRED_LEAKS=$(find "$BACKUP_DIR" \( -name "*.json" -o -name "*.env" -o -name ".env" \) -exec grep -l "nvapi-\|sk-\|Bearer " {} \; 2>/dev/null || true)
CRED_LEAKS=$(find "$BACKUP_DIR" \
\( -name "*.json" -o -name "*.env" -o -name ".env" \) \
! -name "package-lock.json" \
! -name "npm-shrinkwrap.json" \
! -name "yarn.lock" \
! -name "pnpm-lock.yaml" \
! -name "pnpm-lock.yml" \
-exec grep -l "nvapi-\|sk-\|Bearer " {} \; 2>/dev/null || true)
if [ -z "$CRED_LEAKS" ]; then
pass "No credentials in snapshot directories"
else
Expand Down
Loading