Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[core.logging] Add RewriteAppender for filtering LogMeta. #91492

Merged
merged 29 commits into from
Feb 24, 2021
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
752bdf1
Add RewriteAppender with a 'meta' rewrite policy.
lukeelmers Feb 16, 2021
71e46ce
Add unit tests.
lukeelmers Feb 16, 2021
e1c6402
Filter sensitive headers using rewrite appender.
lukeelmers Feb 17, 2021
ae5facc
Update generated docs.
lukeelmers Feb 17, 2021
03a0397
Rename policy.transform to policy.rewrite for consistency with log4j.
lukeelmers Feb 17, 2021
61d4450
Address initial feedback.
lukeelmers Feb 18, 2021
0dc0afc
Switch from appenders.update to appenders.addAppender to align with l…
lukeelmers Feb 18, 2021
44d1440
Fix missed transform > rewrite rename.
lukeelmers Feb 18, 2021
524cc31
Shallow clone http request/response headers.
lukeelmers Feb 18, 2021
c72c75b
Test for circular refs between appenders.
lukeelmers Feb 19, 2021
7cbe347
Update README with documentation.
lukeelmers Feb 19, 2021
e9b9bcc
Merge branch 'master' into feat/rewrite-appender
kibanamachine Feb 19, 2021
8fb040e
Take Appender inside addAppender
lukeelmers Feb 19, 2021
88397a5
Clean up policy comments
lukeelmers Feb 19, 2021
a6632ee
Update snapshots
lukeelmers Feb 19, 2021
eabb888
Merge branch 'master' into feat/rewrite-appender
kibanamachine Feb 21, 2021
0516db3
Re-add hardcoded headers filtering to http logs.
lukeelmers Feb 21, 2021
c777305
Use fixed timestamp in meta policy tests.
lukeelmers Feb 21, 2021
e0f4c58
Clean up documentation.
lukeelmers Feb 21, 2021
11b5512
Prevent mutating array headers.
lukeelmers Feb 22, 2021
b46e404
Extract inline types from MetaRewritePolicy.
lukeelmers Feb 22, 2021
f6ae2ea
Clean up unknown appender error message.
lukeelmers Feb 22, 2021
353d8e5
Resolve merge conflicts.
lukeelmers Feb 22, 2021
dcee4b9
Merge branch 'master' into feat/rewrite-appender
lukeelmers Feb 22, 2021
f9f28dd
Remove MetaRewritePolicy 'add' mode.
lukeelmers Feb 23, 2021
3d2a945
Remove default rewrite-appender.
lukeelmers Feb 23, 2021
102d490
Merge branch 'master' into feat/rewrite-appender
kibanamachine Feb 23, 2021
aa5d21e
Merge branch 'master' into feat/rewrite-appender
kibanamachine Feb 24, 2021
fb23cb1
Fix logging system jest tests.
lukeelmers Feb 24, 2021
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
<b>Signature:</b>

```typescript
export declare type AppenderConfigType = ConsoleAppenderConfig | FileAppenderConfig | LegacyAppenderConfig | RollingFileAppenderConfig;
export declare type AppenderConfigType = ConsoleAppenderConfig | FileAppenderConfig | LegacyAppenderConfig | RewriteAppenderConfig | RollingFileAppenderConfig;
```
18 changes: 18 additions & 0 deletions packages/kbn-logging/src/appenders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,24 @@ import { LogRecord } from './log_record';
*/
export interface Appender {
append(record: LogRecord): void;
/**
* Appenders can be "attached" to one another so that they are able to act
* as a sort of middleware by calling `append` on a different appender.
*
* As appenders cannot be attached to each other until they are configured,
* the `addAppender` method can be used to pass in a newly configured appender
* to attach.
*/
addAppender?(appenderRef: string, appender: Appender): void;
/**
* For appenders which implement `addAppender`, they should declare a list of
* `appenderRefs`, which specify the names of the appenders that their configuration
* depends on.
*
* Note that these are the appender key names that the user specifies in their
* config, _not_ the names of the appender types themselves.
*/
appenderRefs?: string[];
}

/**
Expand Down
136 changes: 134 additions & 2 deletions src/core/server/http/integration_tests/logging.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ describe('request logging', () => {
expect(JSON.parse(meta).http.response.headers.bar).toBe('world');
});

it('filters sensitive request headers', async () => {
it('filters sensitive request headers by default', async () => {
const { http } = await root.setup();

http.createRouter('/').post(
Expand Down Expand Up @@ -283,7 +283,139 @@ describe('request logging', () => {
expect(JSON.parse(meta).http.request.headers.authorization).toBe('[REDACTED]');
});

it('filters sensitive response headers', async () => {
it('filters sensitive request headers when RewriteAppender is configured', async () => {
root = kbnTestServer.createRoot({
logging: {
silent: true,
appenders: {
'test-console': {
type: 'console',
layout: {
type: 'pattern',
pattern: '%level|%logger|%message|%meta',
},
},
rewrite: {
type: 'rewrite',
appenders: ['test-console'],
policy: {
type: 'meta',
mode: 'update',
properties: [
{ path: 'http.request.headers.authorization', value: '[REDACTED]' },
],
},
},
},
loggers: [
{
name: 'http.server.response',
appenders: ['rewrite'],
level: 'debug',
},
],
},
plugins: {
initialize: false,
},
});
const { http } = await root.setup();

http.createRouter('/').post(
{
path: '/ping',
validate: {
body: schema.object({ message: schema.string() }),
},
options: {
authRequired: 'optional',
body: {
accepts: ['application/json'],
},
timeout: { payload: 100 },
},
},
(context, req, res) => res.ok({ body: { message: req.body.message } })
);
await root.start();

await kbnTestServer.request
.post(root, '/ping')
.set('content-type', 'application/json')
.set('authorization', 'abc')
.send({ message: 'hi' })
.expect(200);
expect(mockConsoleLog).toHaveBeenCalledTimes(1);
const [, , , meta] = mockConsoleLog.mock.calls[0][0].split('|');
expect(JSON.parse(meta).http.request.headers.authorization).toBe('[REDACTED]');
});

it('filters sensitive response headers by defaut', async () => {
const { http } = await root.setup();

http.createRouter('/').post(
{
path: '/ping',
validate: {
body: schema.object({ message: schema.string() }),
},
options: {
authRequired: 'optional',
body: {
accepts: ['application/json'],
},
timeout: { payload: 100 },
},
},
(context, req, res) =>
res.ok({ headers: { 'set-cookie': ['123'] }, body: { message: req.body.message } })
);
await root.start();

await kbnTestServer.request
.post(root, '/ping')
.set('Content-Type', 'application/json')
.send({ message: 'hi' })
.expect(200);
expect(mockConsoleLog).toHaveBeenCalledTimes(1);
const [, , , meta] = mockConsoleLog.mock.calls[0][0].split('|');
expect(JSON.parse(meta).http.response.headers['set-cookie']).toBe('[REDACTED]');
});

it('filters sensitive response headers when RewriteAppender is configured', async () => {
root = kbnTestServer.createRoot({
logging: {
silent: true,
appenders: {
'test-console': {
type: 'console',
layout: {
type: 'pattern',
pattern: '%level|%logger|%message|%meta',
},
},
rewrite: {
type: 'rewrite',
appenders: ['test-console'],
policy: {
type: 'meta',
mode: 'update',
properties: [{ path: 'http.response.headers.set-cookie', value: '[REDACTED]' }],
},
},
},
loggers: [
{
name: 'http.server.response',
appenders: ['rewrite'],
level: 'debug',
},
],
},
plugins: {
initialize: false,
},
});
const { http } = await root.setup();

http.createRouter('/').post(
Expand Down
9 changes: 7 additions & 2 deletions src/core/server/http/logging/get_response_log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,12 @@ export function getEcsResponseLog(request: Request, log: Logger): LogMeta {

// eslint-disable-next-line @typescript-eslint/naming-convention
const status_code = isBoom(response) ? response.output.statusCode : response.statusCode;
const responseHeaders = isBoom(response) ? response.output.headers : response.headers;

// shallow clone the headers so they are not mutated if filtered by a RewriteAppender
const requestHeaders = { ...request.headers };
const responseHeaders = isBoom(response)
? { ...response.output.headers }
: { ...response.headers };
lukeelmers marked this conversation as resolved.
Show resolved Hide resolved

// borrowed from the hapi/good implementation
const responseTime = (request.info.completed || request.info.responded) - request.info.received;
Expand All @@ -66,7 +71,7 @@ export function getEcsResponseLog(request: Request, log: Logger): LogMeta {
mime_type: request.mime,
referrer: request.info.referrer,
// @ts-expect-error Headers are not yet part of ECS: https://github.com/elastic/ecs/issues/232.
headers: redactSensitiveHeaders(request.headers),
headers: redactSensitiveHeaders(requestHeaders),
},
response: {
body: {
Expand Down
121 changes: 121 additions & 0 deletions src/core/server/logging/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,127 @@ The maximum number of files to keep. Once this number is reached, oldest files w

The default value is `7`

### Rewrite Appender

*This appender is currently considered experimental and is not intended
for public consumption. The API is subject to change at any time.*

Similar to log4j's `RewriteAppender`, this appender serves as a sort of middleware,
modifying the provided log events before passing them along to another
appender.

```yaml
logging:
appenders:
my-rewrite-appender:
type: rewrite
appenders: [console, file] # name of "destination" appender(s)
policy:
# ...
```

The most common use case for the `RewriteAppender` is when you want to
filter or censor sensitive data that may be contained in a log entry.
In fact, with a default configuration, Kibana will automatically redact
any `authorization`, `cookie`, or `set-cookie` headers when logging http
requests & responses.

To configure additional rewrite rules, you'll need to specify a `RewritePolicy`.

#### Rewrite Policies

Rewrite policies exist to indicate which parts of a log record can be
modified within the rewrite appender.

**Meta**

The `meta` rewrite policy can read and modify any data contained in the
`LogMeta` before passing it along to a destination appender.

Meta policies must specify one of three modes, which indicate which action
to perform on the configured properties:
- `add` creates a new property at the provided `path`, skipping properties which already exist.
lukeelmers marked this conversation as resolved.
Show resolved Hide resolved
- `update` updates an existing property at the provided `path` without creating new properties.
- `remove` removes an existing property at the provided `path`.

The `properties` are listed as a `path` and `value` pair, where `path` is
the dot-delimited path to the target property in the `LogMeta` object, and
`value` is the value to add or update in that target property. When using
the `remove` mode, a `value` is not necessary.

Here's an example of how you would replace any `cookie` header values with `[REDACTED]`:

```yaml
lukeelmers marked this conversation as resolved.
Show resolved Hide resolved
logging:
appenders:
my-rewrite-appender:
type: rewrite
appenders: [console]
policy:
type: meta # indicates that we want to rewrite the LogMeta
mode: update # will update an existing property only
properties:
- path: "http.request.headers.cookie" # path to property
value: "[REDACTED]" # value to replace at path
```

Rewrite appenders can even be passed to other rewrite appenders to apply
multiple filter policies/modes, as long as it doesn't create a circular
reference. Each rewrite appender is applied sequentially (one after the other).
```yaml
logging:
appenders:
remove-stuff:
type: rewrite
appenders: [add-stuff] # redirect to the next rewrite appender
policy:
type: meta
mode: remove
properties:
- path: "http.request.headers.authorization"
- path: "http.request.headers.cookie"
- path: "http.request.headers.set-cookie"
add-stuff:
type: rewrite
appenders: [console] # output to console
policy:
type: meta
mode: add
properties:
- path: "hello"
value: "world" # creates { hello: 'world' } at the LogMeta root
lukeelmers marked this conversation as resolved.
Show resolved Hide resolved
```

#### Complete Example
```yaml
logging:
appenders:
console:
type: console
layout:
type: pattern
highlight: true
pattern: "[%date][%level][%logger] %message %meta"
file:
type: file
fileName: ./kibana.log
layout:
type: json
censor:
type: rewrite
appenders: [console, file]
policy:
type: meta
mode: update
properties:
- path: "http.request.headers.cookie"
value: "[REDACTED]"
loggers:
- name: http.server.response
appenders: [censor] # pass these logs to our rewrite appender
level: debug
```

## Configuration

As any configuration in the platform, logging configuration is validated against the predefined schema and if there are
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/core/server/logging/appenders/appenders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import { Layouts } from '../layouts/layouts';
import { ConsoleAppender, ConsoleAppenderConfig } from './console/console_appender';
import { FileAppender, FileAppenderConfig } from './file/file_appender';
import { RewriteAppender, RewriteAppenderConfig } from './rewrite/rewrite_appender';
import {
RollingFileAppender,
RollingFileAppenderConfig,
Expand All @@ -32,6 +33,7 @@ export const appendersSchema = schema.oneOf([
ConsoleAppender.configSchema,
FileAppender.configSchema,
LegacyAppender.configSchema,
RewriteAppender.configSchema,
RollingFileAppender.configSchema,
]);

Expand All @@ -40,6 +42,7 @@ export type AppenderConfigType =
| ConsoleAppenderConfig
| FileAppenderConfig
| LegacyAppenderConfig
| RewriteAppenderConfig
| RollingFileAppenderConfig;

/** @internal */
Expand All @@ -57,6 +60,8 @@ export class Appenders {
return new ConsoleAppender(Layouts.create(config.layout));
case 'file':
return new FileAppender(Layouts.create(config.layout), config.fileName);
case 'rewrite':
return new RewriteAppender(config);
case 'rolling-file':
return new RollingFileAppender(config);
case 'legacy-appender':
Expand Down
Loading