diff --git a/.github/workflows/e2e-advisor.yaml b/.github/workflows/e2e-advisor.yaml index 1024e5c6243..ca096615ebe 100644 --- a/.github/workflows/e2e-advisor.yaml +++ b/.github/workflows/e2e-advisor.yaml @@ -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" diff --git a/src/lib/security/credential-filter.test.ts b/src/lib/security/credential-filter.test.ts index 0fd6089bb90..b0e7d3e8ec4 100644 --- a/src/lib/security/credential-filter.test.ts +++ b/src/lib/security/credential-filter.test.ts @@ -13,6 +13,7 @@ import { stripCredentials, sanitizeConfigFile, isSensitiveFile, + shouldScanSnapshotFileForCredentials, } from "./credential-filter.js"; describe("isCredentialField", () => { @@ -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); + }); +}); diff --git a/src/lib/security/credential-filter.ts b/src/lib/security/credential-filter.ts index b8578a73ebb..c4d16a6e9ee 100644 --- a/src/lib/security/credential-filter.ts +++ b/src/lib/security/credential-filter.ts @@ -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(text: string): T { return JSON.parse(text); @@ -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. */ @@ -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"); +} diff --git a/test/e2e/docs/parity-inventory.generated.json b/test/e2e/docs/parity-inventory.generated.json index 1f957f177da..fb5dad16730 100644 --- a/test/e2e/docs/parity-inventory.generated.json +++ b/test/e2e/docs/parity-inventory.generated.json @@ -14905,7 +14905,7 @@ }, { "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", @@ -14913,7 +14913,7 @@ }, { "script": "test/e2e/test-snapshot-commands.sh", - "line": 255, + "line": 262, "text": "Credentials found: $CRED_LEAKS", "polarity": "fail", "normalized_id": "credentials.found.cred.leaks", @@ -14921,7 +14921,7 @@ }, { "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", @@ -14929,7 +14929,7 @@ }, { "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", @@ -14937,7 +14937,7 @@ }, { "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", @@ -14945,7 +14945,7 @@ }, { "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", diff --git a/test/e2e/test-snapshot-commands.sh b/test/e2e/test-snapshot-commands.sh index 4adb2aa6b73..e70d4953497 100755 --- a/test/e2e/test-snapshot-commands.sh +++ b/test/e2e/test-snapshot-commands.sh @@ -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