Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
14 changes: 13 additions & 1 deletion docs/@v2/commands/respect.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Use this command to execute API tests described in an Arazzo description.
## Usage

```sh
npx @redocly/cli@latest respect <your-test-file | multiple files | files bash query> [-w | --workflow] [-s | --skip] [-v | --verbose] [-i | --input] [-S | --server] [-H | --har-output] [-J | --json-output] [--max-steps] [--max-fetch-timeout] [--execution-timeout] [--severity]
npx @redocly/cli@latest respect <your-test-file | multiple files | files bash query> [-w | --workflow] [-s | --skip] [-v | --verbose] [-i | --input] [-S | --server] [-H | --har-output] [-J | --json-output] [--max-steps] [--max-fetch-timeout] [--execution-timeout] [--severity] [--secrets-reveal]
```

## Options
Expand Down Expand Up @@ -180,6 +180,18 @@ npx @redocly/cli@latest respect <your-test-file | multiple files | files bash qu

`REDOCLY_CLI_RESPECT_EXECUTION_TIMEOUT=1800000 npx @redocly/cli@latest respect test-file.yaml`

---

- --secrets-reveal
- boolean
- Disables masking of secrets in the output. By default, any sensitive information, such as values described with `format: password`,
as well as tokens and authentication headers from `x-security`, is masked with `********` in both terminal logs and file outputs. When this flag is set to `true`,
the raw (unmasked) data shows in all outputs.

For example, the following command disables sensitive output masking:

`npx @redocly/cli@latest respect test-file.yaml --secrets-reveal`

{% /table %}

## Examples
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ describe('handleRespect', () => {
totalTimeMs: 100,
totalRequests: 1,
globalTimeoutError: false,
secretValues: [],
},
]);

Expand Down
10 changes: 8 additions & 2 deletions packages/cli/src/commands/respect/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export type RespectArgv = {
config?: string;
'max-fetch-timeout': number;
'execution-timeout': number;
'secrets-reveal': boolean;
};

export async function handleRespect({
Expand All @@ -42,7 +43,7 @@ export async function handleRespect({
let harLogs;

try {
const { run, maskSecrets } = await import('@redocly/respect-core');
const { run, conditionallyMaskSecrets } = await import('@redocly/respect-core');
const workingDir = config.configPath ? dirname(config.configPath) : process.cwd();

if (argv['client-cert'] || argv['client-key'] || argv['ca-cert']) {
Expand Down Expand Up @@ -92,6 +93,7 @@ export async function handleRespect({
envVariables: readEnvVariables(workingDir) || {},
logger,
fetch: customFetch as unknown as typeof fetch,
secretsReveal: argv['secrets-reveal'],
};

if (options.skip && options.workflow) {
Expand Down Expand Up @@ -140,7 +142,11 @@ export async function handleRespect({
if (argv['har-output']) {
// TODO: implement multiple run files HAR output
for (const result of runAllFilesResult) {
const parsedHarLogs = maskSecrets(harLogs, result.ctx.secretFields || new Set());
const parsedHarLogs = conditionallyMaskSecrets({
value: harLogs,
secretsReveal: result.ctx.secretsReveal,
secretsSet: result.ctx.secretsSet || new Set(),
});
writeFileSync(argv['har-output'], jsonStringifyWithArrayBuffer(parsedHarLogs, 2), 'utf-8');
logger.output(blue(`Har logs saved in ${green(argv['har-output'])}`));
logger.printNewLine();
Expand Down
21 changes: 14 additions & 7 deletions packages/cli/src/commands/respect/json-logs.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { maskSecrets, calculateTotals } from '@redocly/respect-core';
import { calculateTotals, conditionallyMaskSecrets } from '@redocly/respect-core';

import type {
TestContext,
Expand All @@ -18,23 +18,30 @@ export function composeJsonLogsFiles(
executedWorkflows: WorkflowExecutionResult[];
ctx: TestContext;
globalTimeoutError: boolean;
secretValues?: string[];
}[]
): JsonLogs['files'] {
const files: JsonLogs['files'] = {};

for (const fileResult of filesResult) {
const { executedWorkflows, globalTimeoutError: fileGlobalTimeoutError } = fileResult;
const { secretFields } = fileResult.ctx;
const {
executedWorkflows,
globalTimeoutError: fileGlobalTimeoutError,
ctx,
secretValues,
} = fileResult;

files[fileResult.file] = maskSecrets(
{
files[fileResult.file] = conditionallyMaskSecrets({
value: {
totalRequests: fileResult.totalRequests,
executedWorkflows: executedWorkflows.map((workflow) => mapJsonWorkflow(workflow)),
totalTimeMs: fileResult.totalTimeMs,
globalTimeoutError: fileGlobalTimeoutError,
secretValues,
},
secretFields || new Set()
);
secretsReveal: ctx.secretsReveal,
secretsSet: ctx.secretsSet || new Set(),
});
}

return files;
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,10 @@ yargs(hideBin(process.argv))
default: 3_600_000,
coerce: validatePositiveNumber('execution-timeout', false),
},
'secrets-reveal': {
describe: 'Do not mask secrets in the output.',
type: 'boolean',
},
});
},
async (argv) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/respect-core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export { generate, type GenerateArazzoOptions } from './generate.js';
export { run, type RespectOptions } from './run.js';
export * from './types.js';
export { maskSecrets } from './modules/logger-output/mask-secrets.js';
export { maskSecrets, conditionallyMaskSecrets } from './modules/logger-output/mask-secrets.js';
export { calculateTotals } from './modules/logger-output/calculate-tests-passed.js';
export { RESET_ESCAPE_CODE } from './modules/logger-output/helpers.js';
export { parseRuntimeExpression } from './modules/runtime-expressions/index.js';
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { TestContext } from '../../../types';

describe('getSecurityParameter', () => {
const ctx = {
secretFields: new Set(),
secretsSet: new Set(),
} as TestContext;

it('should return security parameters for API Key Auth', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ describe('callAPIAndAnalyzeResults', () => {
},
],
$outputs: {},
secretFields: new Set(),
secretsSet: new Set(),
} as unknown as TestContext;

it('should call API and return checks result', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ApiFetcher } from '../../../../utils/api-fetcher.js';
import {
createTestContext,
DEFAULT_SEVERITY_CONFIGURATION,
collectSecretFields,
collectSecretValues,
} from '../../../../modules/flow-runner/index.js';

describe('createTestContext', () => {
Expand Down Expand Up @@ -524,7 +524,7 @@ describe('createTestContext', () => {
},
info: { title: 'API', version: '1.0' },
arazzo: '1.0.1',
secretFields: {},
secretsSet: {},
severity: DEFAULT_SEVERITY_CONFIGURATION,
sourceDescriptions: [
{ name: 'cats', type: 'openapi', url: '../../__tests__/respect/cat-fact-api/cats.yaml' },
Expand Down Expand Up @@ -762,10 +762,10 @@ describe('createTestContext', () => {
});
});

describe('storeSecretFields', () => {
describe('storeSecretValues', () => {
it('should store secret fields with inputs', () => {
const ctx = {
secretFields: new Set<string>(),
secretsSet: new Set<string>(),
} as unknown as TestContext;

const schema = {
Expand All @@ -788,14 +788,14 @@ describe('storeSecretFields', () => {
},
};

collectSecretFields(ctx, schema, inputs);
collectSecretValues(ctx, schema, inputs);

expect(ctx.secretFields).toEqual(new Set(['password', 'nested-password']));
expect(ctx.secretsSet).toEqual(new Set(['password', 'nested-password']));
});

it('should store secret env fields', () => {
const ctx = {
secretFields: new Set<string>(),
secretsSet: new Set<string>(),
} as unknown as TestContext;

const schema = {
Expand Down Expand Up @@ -825,14 +825,14 @@ describe('storeSecretFields', () => {
},
};

collectSecretFields(ctx, schema, inputs);
collectSecretValues(ctx, schema, inputs);

expect(ctx.secretFields).toEqual(new Set(['password', 'nested-password']));
expect(ctx.secretsSet).toEqual(new Set(['password', 'nested-password']));
});

it('should not store secret fields if not password format', () => {
const ctx = {
secretFields: new Set<string>(),
secretsSet: new Set<string>(),
} as unknown as TestContext;

const schema = {
Expand All @@ -855,14 +855,14 @@ describe('storeSecretFields', () => {
},
};

collectSecretFields(ctx, schema, inputs);
collectSecretValues(ctx, schema, inputs);

expect(ctx.secretFields).toEqual(new Set());
expect(ctx.secretsSet).toEqual(new Set());
});

it('should store secret field with format password in object', () => {
const ctx = {
secretFields: new Set<string>(),
secretsSet: new Set<string>(),
} as unknown as TestContext;

const schema = {
Expand All @@ -874,8 +874,8 @@ describe('storeSecretFields', () => {
tocken: 'secret-token',
};

collectSecretFields(ctx, schema, inputs, ['tocken']);
collectSecretValues(ctx, schema, inputs, ['tocken']);

expect(ctx.secretFields).toEqual(new Set(['secret-token']));
expect(ctx.secretsSet).toEqual(new Set(['secret-token']));
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { Step, RuntimeExpressionContext, TestContext } from 'respect-core/s

describe('resolveXSecurityParameters', () => {
const ctx = {
secretFields: new Set(),
secretsSet: new Set(),
options: {
logger,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
findPotentiallySecretObjectFields,
containsSecret,
maskSecrets,
conditionallyMaskSecrets,
} from '../../logger-output/mask-secrets.js';

describe('findPotentiallySecretObjectFields', () => {
Expand Down Expand Up @@ -128,3 +129,23 @@ describe('maskSecrets', () => {
expect(result.nested.password).toBe('********');
});
});

describe('conditionallyMaskSecrets', () => {
it('should mask secrets if secretsReveal is false', () => {
const result = conditionallyMaskSecrets({
value: 'token123456',
secretsReveal: false,
secretsSet: new Set(['token123456']),
});
expect(result).toEqual('********');
});

it('should preserve secrets if secretsReveal is true', () => {
const result = conditionallyMaskSecrets({
value: 'token123456',
secretsReveal: true,
secretsSet: new Set(['token123456']),
});
expect(result).toEqual('token123456');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -25,31 +25,31 @@ export function getSecurityParameter(
const { username, password } = security.values;
const value = btoa(`${username}:${password}`);

ctx.secretFields.add(value);
ctx.secretsSet.add(value);

return getAuthHeader(`Basic ${value}`, ctx);
}

if (isBearerAuth(security)) {
const { token } = security.values;

ctx.secretFields.add(token);
ctx.secretsSet.add(token);

return getAuthHeader(`Bearer ${token}`, ctx);
}

if (isOpenIdConnectAuth(security)) {
const { accessToken } = security.values;

ctx.secretFields.add(accessToken);
ctx.secretsSet.add(accessToken);

return getAuthHeader(`Bearer ${accessToken}`, ctx);
}

if (isOAuth2Auth(security)) {
const { accessToken } = security.values;

ctx.secretFields.add(accessToken);
ctx.secretsSet.add(accessToken);

return getAuthHeader(`Bearer ${accessToken}`, ctx);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,22 +94,23 @@ export async function createTestContext(
info: testDescription.info || infoSubstitute,
arazzo: testDescription.arazzo || '',
sourceDescriptions: testDescription.sourceDescriptions || [],
secretFields: new Set<string>(),
secretsReveal: options.secretsReveal || false,
secretsSet: new Set<string>(),
severity: resolveSeverityConfiguration(options.severity),
apiClient,
};

// Collect all secret fields from the input schema and the workflow inputs
for (const workflow of testDescription.workflows || []) {
if (workflow.inputs) {
collectSecretFields(ctx, workflow.inputs, ctx.$workflows[workflow.workflowId].inputs);
collectSecretValues(ctx, workflow.inputs, ctx.$workflows[workflow.workflowId].inputs);
}
}

return ctx;
}

export function collectSecretFields(
export function collectSecretValues(
ctx: TestContext,
schema: InputSchema | undefined,
inputs: Record<string, any> | undefined,
Expand All @@ -120,13 +121,13 @@ export function collectSecretFields(
const inputValue = getNestedValue(inputs, path);

if (schema.format === 'password' && inputValue) {
ctx.secretFields.add(inputValue);
ctx.secretsSet.add(inputValue);
}

if (schema.properties) {
Object.entries(schema.properties).forEach(([key, value]: [string, any]) => {
const currentPath = [...path, key];
collectSecretFields(ctx, value, inputs, currentPath);
collectSecretValues(ctx, value, inputs, currentPath);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
handlePayloadReplacements,
} from '../context-parser/index.js';
import { getServerUrl } from './get-server-url.js';
import { createRuntimeExpressionCtx, collectSecretFields } from './context/index.js';
import { createRuntimeExpressionCtx, collectSecretValues } from './context/index.js';
import { evaluateRuntimeExpressionPayload } from '../runtime-expressions/index.js';
import { resolveXSecurityParameters } from './resolve-x-security-parameters.js';

Expand Down Expand Up @@ -168,7 +168,7 @@ export async function prepareRequest(

for (const param of openapiOperation?.parameters || []) {
const { schema, name } = param;
collectSecretFields(ctx, schema, groupParametersValuesByName(parameters, param.in), [name]);
collectSecretValues(ctx, schema, groupParametersValuesByName(parameters, param.in), [name]);
}

const evaluatedBody = evaluateRuntimeExpressionPayload({
Expand All @@ -190,7 +190,7 @@ export async function prepareRequest(
if (contentType && openapiOperation?.requestBody) {
const requestBodySchema = getRequestBodySchema(contentType, openapiOperation);
if (typeof requestBody === 'object') {
collectSecretFields(ctx, requestBodySchema, requestBody);
collectSecretValues(ctx, requestBodySchema, requestBody);
}
}

Expand Down
Loading
Loading