From e3a76ca57715da2b50cd288721904c7c89c491a5 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Sat, 16 Aug 2025 13:15:32 +0000 Subject: [PATCH] fix(appdata): Redact sensitive values in AppData logs and add redaction tests --- changelog.md | 9 ++++ pnpm-lock.yaml | 30 ++++++------ test/test.redaction.ts | 85 ++++++++++++++++++++++++++++++++++ ts/00_commitinfo_data.ts | 2 +- ts/npmextra.classes.appdata.ts | 55 ++++++++++++++++++++-- 5 files changed, 160 insertions(+), 21 deletions(-) create mode 100644 test/test.redaction.ts diff --git a/changelog.md b/changelog.md index 6bc8072..7bd2a75 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## 2025-08-16 - 5.3.3 - fix(appdata) +Redact sensitive values in AppData logs and add redaction tests + +- Add redactSensitiveValue helper in AppData to mask secrets (API keys, tokens, passwords, JWTs, etc.) during logging. +- Use redaction when logging raw and final mapping values in processMappingValue and nested key logging to avoid leaking sensitive data. +- Improve log output for long or special values (JWT/base64 detection, length-aware previews) while preserving actual stored values. +- Add test/test.redaction.ts to verify sensitive environment values are redacted in console output but still stored correctly in the kv store. +- Add local config .claude/settings.local.json (editor/CI permissions/settings). + ## 2025-08-16 - 5.3.2 - fix(dependencies) Bump @push.rocks/qenv to ^6.1.3 and add local Claude settings diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 61f07db..d6180a4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -244,8 +244,8 @@ packages: '@borewit/text-codec@0.1.1': resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==} - '@cloudflare/workers-types@4.20250813.0': - resolution: {integrity: sha512-RFFjomDndGR+p7ug1HWDlW21qOJyRZbmI99dUtuR9tmwJbSZhUUnSFmzok9lBYVfkMMrO1O5vmB+IlgiecgLEA==} + '@cloudflare/workers-types@4.20250816.0': + resolution: {integrity: sha512-R9ADrrINo1CqTwCddH39Tjlsc3grim6KeO7l8yddNbldH3uTkaAXYCzO0WiyLG7irLzLDrZVc4tLhN6BO3tdFw==} '@colors/colors@1.6.0': resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} @@ -722,8 +722,8 @@ packages: '@push.rocks/smarts3@2.2.5': resolution: {integrity: sha512-OZjD0jBCUTJCLnwraxBcyZ3he5buXf2OEM1zipiTBChA2EcKUZWKk/a6KR5WT+NlFCIIuB23UG+U+cxsIWM91Q==} - '@push.rocks/smartshell@3.2.3': - resolution: {integrity: sha512-BWA/DH1H9lG7Er23d4uYgirfYaya5dX4g/WpWm2la7mOzuL9o2FnPIhel52DQUKIh7ty3Ql305ApV8YaAb4+/w==} + '@push.rocks/smartshell@3.2.4': + resolution: {integrity: sha512-zZEKfRl3qBaII9BJULk4rB/+EelUpgM2/qHCQLui7c4589HTge4o0nWn+olFw/Hge88qUO77OK1sN7hQFZ6zeg==} '@push.rocks/smartsitemap@2.0.3': resolution: {integrity: sha512-jIcms8V1b2mt3dS4PKNlLR1DRC8pCDWMRVbnyM/2+snZOJZonQRlQzAyX8No0EfLbfdrfnxv2IjPX13X29Re6g==} @@ -734,8 +734,8 @@ packages: '@push.rocks/smartspawn@3.0.3': resolution: {integrity: sha512-DyrGPV69wwOiJgKkyruk5hS3UEGZ99xFAqBE9O2nM8VXCRLbbty3xt1Ug5Z092ZZmJYaaGMSnMw3ijyZJFCT0Q==} - '@push.rocks/smartstate@2.0.25': - resolution: {integrity: sha512-gWmbDCx5esezHDQnD2nOClxeTiWtvU1wEdP0XbheCcXzaGEv0H8apRjQBksRZJd9FC3ezrJU00GLh0eH9rPyMQ==} + '@push.rocks/smartstate@2.0.26': + resolution: {integrity: sha512-lMcf0ZWWs9jej9wjapuonuIZiQNiD9NcAcvRDFXq7GtQf/HUyr6zr5K1XxGZaCIGyYrbYnBHBpNU+8DBoarHrA==} '@push.rocks/smartstream@2.0.8': resolution: {integrity: sha512-GlF/9cCkvBHwKa3DK4DO5wjfSgqkj6gAS4TrY9uD5NMHu9RQv4WiNrElTYj7iCEpnZgUnLO3tzw1JA3NRIMnnA==} @@ -3890,7 +3890,7 @@ snapshots: '@api.global/typedrequest': 3.1.10 '@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedsocket': 3.0.1 - '@cloudflare/workers-types': 4.20250813.0 + '@cloudflare/workers-types': 4.20250816.0 '@design.estate/dees-comms': 1.0.27 '@push.rocks/lik': 6.2.2 '@push.rocks/smartchok': 1.1.1 @@ -4511,7 +4511,7 @@ snapshots: '@borewit/text-codec@0.1.1': {} - '@cloudflare/workers-types@4.20250813.0': {} + '@cloudflare/workers-types@4.20250816.0': {} '@colors/colors@1.6.0': {} @@ -4543,7 +4543,7 @@ snapshots: '@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartrouter': 1.3.3 '@push.rocks/smartrx': 3.0.10 - '@push.rocks/smartstate': 2.0.25 + '@push.rocks/smartstate': 2.0.26 '@push.rocks/smartstring': 4.0.15 '@push.rocks/smarturl': 3.1.0 '@push.rocks/webrequest': 3.0.37 @@ -4709,14 +4709,14 @@ snapshots: '@push.rocks/smartnpm': 2.0.4 '@push.rocks/smartpath': 6.0.0 '@push.rocks/smartrequest': 4.2.1 - '@push.rocks/smartshell': 3.2.3 + '@push.rocks/smartshell': 3.2.4 transitivePeerDependencies: - aws-crt '@git.zone/tsrun@1.3.3': dependencies: '@push.rocks/smartfile': 11.2.5 - '@push.rocks/smartshell': 3.2.3 + '@push.rocks/smartshell': 3.2.4 tsx: 4.20.4 '@git.zone/tstest@2.3.2(@aws-sdk/credential-providers@3.864.0)(socks@2.8.7)(typescript@5.8.3)': @@ -4740,7 +4740,7 @@ snapshots: '@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartrequest': 2.1.0 '@push.rocks/smarts3': 2.2.5 - '@push.rocks/smartshell': 3.2.3 + '@push.rocks/smartshell': 3.2.4 '@push.rocks/smarttime': 4.1.1 '@types/ws': 8.18.1 figures: 6.1.0 @@ -5319,7 +5319,7 @@ snapshots: '@push.rocks/smartpuppeteer@2.0.5(typescript@5.8.3)': dependencies: '@push.rocks/smartdelay': 3.0.5 - '@push.rocks/smartshell': 3.2.3 + '@push.rocks/smartshell': 3.2.4 puppeteer: 24.16.2(typescript@5.8.3) tree-kill: 1.2.2 transitivePeerDependencies: @@ -5368,7 +5368,7 @@ snapshots: - aws-crt - supports-color - '@push.rocks/smartshell@3.2.3': + '@push.rocks/smartshell@3.2.4': dependencies: '@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartexit': 1.0.23 @@ -5420,7 +5420,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@push.rocks/smartstate@2.0.25': + '@push.rocks/smartstate@2.0.26': dependencies: '@push.rocks/lik': 6.2.2 '@push.rocks/smarthash': 3.2.3 diff --git a/test/test.redaction.ts b/test/test.redaction.ts new file mode 100644 index 0000000..21d1880 --- /dev/null +++ b/test/test.redaction.ts @@ -0,0 +1,85 @@ +import { expect, tap } from '@git.zone/tstest/tapbundle'; +import * as npmextra from '../ts/index.js'; + +// Test that sensitive values are properly redacted in logs +tap.test('should redact sensitive values in console output', async () => { + // Capture console.log output + const originalLog = console.log; + const logOutput: string[] = []; + console.log = (...args: any[]) => { + logOutput.push(args.join(' ')); + }; + + try { + // Set up environment variables with sensitive data + process.env['API_KEY'] = 'super-secret-api-key-12345'; + process.env['DATABASE_PASSWORD'] = 'myP@ssw0rd123'; + process.env['AUTH_TOKEN'] = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ'; + process.env['PUBLIC_URL'] = 'https://example.com'; + process.env['DEBUG_MODE'] = 'true'; + + // Create AppData with sensitive environment mappings + const appData = await npmextra.AppData.createAndInit({ + ephemeral: true, + envMapping: { + apiKey: 'API_KEY', + dbPassword: 'DATABASE_PASSWORD', + authToken: 'AUTH_TOKEN', + publicUrl: 'PUBLIC_URL', + debugMode: 'boolean:DEBUG_MODE', + nestedConfig: { + secretKey: 'API_KEY', + nonSecret: 'PUBLIC_URL' + } + } + }); + + // Restore console.log + console.log = originalLog; + + // Check that sensitive values were redacted in logs + const combinedOutput = logOutput.join('\n'); + + // API_KEY should be redacted + expect(combinedOutput).toContain('sup...[26 chars]'); + expect(combinedOutput).not.toContain('super-secret-api-key-12345'); + + // DATABASE_PASSWORD should be redacted + expect(combinedOutput).toContain('myP...[13 chars]'); + expect(combinedOutput).not.toContain('myP@ssw0rd123'); + + // AUTH_TOKEN should be redacted (JWT tokens starting with eyJ) + expect(combinedOutput).toContain('eyJ...['); + expect(combinedOutput).not.toContain('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'); + + // PUBLIC_URL should not be redacted (not sensitive) + expect(combinedOutput).toContain('https://example.com'); + + // DEBUG_MODE should not be redacted (not sensitive) + expect(combinedOutput).toContain('true'); + + // Verify data is still stored correctly (not redacted in actual storage) + const kvStore = await appData.getKvStore(); + const apiKey = await kvStore.readKey('apiKey'); + const dbPassword = await kvStore.readKey('dbPassword'); + const publicUrl = await kvStore.readKey('publicUrl'); + + // Actual values should be stored correctly + expect(apiKey).toEqual('super-secret-api-key-12345'); + expect(dbPassword).toEqual('myP@ssw0rd123'); + expect(publicUrl).toEqual('https://example.com'); + + } finally { + // Restore console.log in case of test failure + console.log = originalLog; + + // Clean up environment variables + delete process.env['API_KEY']; + delete process.env['DATABASE_PASSWORD']; + delete process.env['AUTH_TOKEN']; + delete process.env['PUBLIC_URL']; + delete process.env['DEBUG_MODE']; + } +}); + +export default tap.start(); \ No newline at end of file diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 3209af0..7e80394 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/npmextra', - version: '5.3.2', + version: '5.3.3', description: 'A utility to enhance npm with additional configuration, tool management capabilities, and a key-value store for project setups.' } diff --git a/ts/npmextra.classes.appdata.ts b/ts/npmextra.classes.appdata.ts index 3881e45..1c7fcb9 100644 --- a/ts/npmextra.classes.appdata.ts +++ b/ts/npmextra.classes.appdata.ts @@ -16,6 +16,48 @@ function getQenv(): plugins.qenv.Qenv { return sharedQenv; } +// ============================================================================ +// Security - Redaction for sensitive data +// ============================================================================ +/** + * Redacts sensitive values in logs to prevent exposure of secrets + */ +function redactSensitiveValue(key: string, value: unknown): string { + // List of patterns that indicate sensitive data + const sensitivePatterns = [ + /secret/i, /token/i, /key/i, /password/i, /pass/i, + /api/i, /credential/i, /auth/i, /private/i, /jwt/i, + /cert/i, /signature/i, /bearer/i + ]; + + // Check if key contains sensitive pattern + const isSensitive = sensitivePatterns.some(pattern => pattern.test(key)); + + if (isSensitive) { + if (typeof value === 'string') { + // Show first 3 chars and length for debugging + return value.length > 3 + ? `${value.substring(0, 3)}...[${value.length} chars]` + : '[redacted]'; + } + return '[redacted]'; + } + + // Check if value looks like a JWT token or base64 secret + if (typeof value === 'string') { + // JWT tokens start with eyJ + if (value.startsWith('eyJ')) { + return `eyJ...[${value.length} chars]`; + } + // Very long strings might be encoded secrets + if (value.length > 100) { + return `${value.substring(0, 50)}...[${value.length} chars total]`; + } + } + + return JSON.stringify(value); +} + // ============================================================================ // Type Converters - Centralized conversion logic // ============================================================================ @@ -210,12 +252,14 @@ function applyTransforms(value: unknown, transforms: Transform[]): unknown { */ async function processMappingValue(mappingString: string): Promise { const spec = parseMappingSpec(mappingString); + const keyName = spec.source.type === 'env' ? spec.source.key : 'hardcoded'; + console.log(` 🔍 Processing mapping: "${mappingString}"`); console.log(` Source: ${spec.source.type === 'env' ? `env:${spec.source.key}` : `hard:${spec.source.value}`}`); console.log(` Transforms: ${spec.transforms.length > 0 ? spec.transforms.join(', ') : 'none'}`); const rawValue = await resolveSource(spec.source); - console.log(` Raw value: ${JSON.stringify(rawValue)} (type: ${typeof rawValue})`); + console.log(` Raw value: ${redactSensitiveValue(keyName, rawValue)} (type: ${typeof rawValue})`); if (rawValue === undefined || rawValue === null) { console.log(` ⚠️ Raw value is undefined/null, returning undefined`); @@ -223,7 +267,7 @@ async function processMappingValue(mappingString: string): Promise { } const result = applyTransforms(rawValue, spec.transforms); - console.log(` Final value: ${JSON.stringify(result)} (type: ${typeof result})`); + console.log(` Final value: ${redactSensitiveValue(keyName, result)} (type: ${typeof result})`); return result; } @@ -253,7 +297,7 @@ async function evaluateMappingValue(mappingValue: any): Promise { // Only skip if explicitly undefined if (evaluated !== undefined) { result[key] = evaluated; - console.log(` ✓ Nested key "${key}" = ${JSON.stringify(evaluated)} (type: ${typeof evaluated})`); + console.log(` ✓ Nested key "${key}" = ${redactSensitiveValue(key, evaluated)} (type: ${typeof evaluated})`); } else { console.log(` ⚠️ Nested key "${key}" evaluated to undefined, skipping`); } @@ -262,7 +306,8 @@ async function evaluateMappingValue(mappingValue: any): Promise { } // For any other type (numbers, booleans, etc.), return as-is - console.log(` 📎 Returning value as-is: ${JSON.stringify(mappingValue)} (type: ${typeof mappingValue})`); + // Note: We don't have key context here, so we'll just indicate the type + console.log(` 📎 Returning value as-is: [value] (type: ${typeof mappingValue})`); return mappingValue; } @@ -434,7 +479,7 @@ export class AppData { const valuePreview = evaluated === null ? 'null' : typeof evaluated === 'object' ? (Array.isArray(evaluated) ? `[${evaluated.length} items]` : `{${Object.keys(evaluated).length} keys}`) : - JSON.stringify(evaluated); + redactSensitiveValue(key, evaluated); console.log(` ✅ Successfully processed key "${key}" = ${valuePreview} (type: ${valueType})`); } else { console.log(` ⚠️ Key "${key}" evaluated to undefined, skipping`);