fix(appdata): Redact sensitive values in AppData logs and add redaction tests
This commit is contained in:
@@ -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<unknown> {
|
||||
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<unknown> {
|
||||
}
|
||||
|
||||
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<any> {
|
||||
// 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<any> {
|
||||
}
|
||||
|
||||
// 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<T = any> {
|
||||
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`);
|
||||
|
Reference in New Issue
Block a user