Skip to content
Open
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
50 changes: 50 additions & 0 deletions src/cli/commands/invoke/__tests__/redact-sensitive-text.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { redactSensitiveText } from '../command.js';
import { describe, expect, it } from 'vitest';

describe('redactSensitiveText', () => {
it('redacts Bearer tokens', () => {
expect(redactSensitiveText('Authorization: Bearer eyJhbGciOiJSUzI1NiJ9.payload.sig')).toBe(
'Authorization: Bearer [REDACTED]'
);
});

it('redacts Bearer tokens in JSON', () => {
expect(redactSensitiveText('{"header":"Bearer eyJhbGciOiJSUzI1NiJ9.payload.sig"}')).toBe(
'{"header":"Bearer [REDACTED]"}'
);
});

it('redacts client_secret in key=value form', () => {
expect(redactSensitiveText('client_secret=abc123def')).toBe('client_secret=[REDACTED]');
});

it('redacts client_secret in JSON form', () => {
expect(redactSensitiveText('{"client_secret":"abc123def"}')).toBe('{"client_secret":"[REDACTED]"}');
});

it('redacts token in key=value form', () => {
expect(redactSensitiveText('token=eyJhbGciOiJSUzI1NiJ9')).toBe('token=[REDACTED]');
});

it('redacts token in JSON form', () => {
expect(redactSensitiveText('{"token":"eyJhbGciOiJSUzI1NiJ9"}')).toBe('{"token":"[REDACTED]"}');
});

it('redacts access_token in JSON form', () => {
expect(redactSensitiveText('{"access_token":"jwt.token.here"}')).toBe('{"access_token":"[REDACTED]"}');
});

it('redacts client-secret with hyphen', () => {
expect(redactSensitiveText('client-secret=mysecret')).toBe('client-secret=[REDACTED]');
});

it('does not modify text without sensitive content', () => {
const input = 'Agent responded successfully with 200 OK';
expect(redactSensitiveText(input)).toBe(input);
});

it('handles multiple sensitive values in one string', () => {
const input = 'Bearer abc123 and client_secret=xyz789';
expect(redactSensitiveText(input)).toBe('Bearer [REDACTED] and client_secret=[REDACTED]');
});
});
25 changes: 17 additions & 8 deletions src/cli/commands/invoke/command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
if (isPreviewEnabled() && options.harnessArn) {
const region = options.region ?? process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION;
if (!region) {
const msg = '--region is required with --harness-arn (or set AWS_REGION)';
const msg = redactSensitiveText('--region is required with --harness-arn (or set AWS_REGION)');
if (options.json) {
console.log(JSON.stringify({ success: false, error: msg }));
} else {
Expand Down Expand Up @@ -85,13 +85,22 @@
}
throw err;
}
}

Check warning on line 88 in src/cli/commands/invoke/command.tsx

View workflow job for this annotation

GitHub Actions / lint

Unsafe Regular Expression

export function redactSensitiveText(value: string): string {
return value
.replace(/(bearer\s+)[a-z0-9\-._~+/]+=*/gi, '$1[REDACTED]')
.replace(/(client[_-]?secret["']?\s*[:=]\s*["']?)([^"',\s}]+)/gi, '$1[REDACTED]')
.replace(/((?:access[_-]?)?token["']?\s*[:=]\s*["']?)([^"',\s}]+)/gi, '$1[REDACTED]');
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No tests for redactSensitiveText.

This is a security-sensitive helper that's easy to get subtly wrong (see the JSON-shape comment above — a unit test would have caught it immediately). A small table-driven test in src/cli/commands/invoke/__tests__/ covering at least:

  • Bearer <jwt> in a plain string and inside a JSON-stringified body
  • client_secret=... and "client_secret":"..."
  • token=... and "access_token":"..."
  • A negative case (no sensitive content → unchanged)

would be enough to lock down the behavior and prevent regressions like the one this PR is fixing.


function printInvokeResult(result: InvokeResult, options: InvokeOptions): void {
if (options.json) {
console.log(JSON.stringify(serializeResult(result)));
const serialized = serializeResult(result);
if (typeof serialized.response === 'string') serialized.response = redactSensitiveText(serialized.response);
if (typeof serialized.error === 'string') serialized.error = redactSensitiveText(serialized.error);
console.log(JSON.stringify(serialized));
} else if (options.stream) {
// Streaming already wrote to stdout, just show session and log path
if (result.sessionId) {
console.error(`\nSession: ${result.sessionId}`);
console.error(`To resume: agentcore invoke --session-id ${result.sessionId}`);
Expand All @@ -100,11 +109,10 @@
console.error(`Log: ${result.logFilePath}`);
}
} else {
// Non-streaming, non-json: print provider info and response or error
if (result.success && result.response) {
console.log(result.response);
console.log(redactSensitiveText(result.response));
} else if (!result.success && result.error) {
console.error(result.error.message);
console.error(redactSensitiveText(result.error.message));
}
if (result.sessionId) {
console.error(`\nSession: ${result.sessionId}`);
Expand Down Expand Up @@ -344,10 +352,11 @@
});
}
} catch (error) {
const msg = redactSensitiveText(getErrorMessage(error));
if (cliOptions.json) {
console.log(JSON.stringify({ success: false, error: getErrorMessage(error) }));
console.log(JSON.stringify({ success: false, error: msg }));
} else {
render(<Text color="red">Error: {getErrorMessage(error)}</Text>);
render(<Text color="red">Error: {msg}</Text>);
}
process.exit(1);
}
Expand Down
Loading