Skip to content

Commit 307d83c

Browse files
committed
Add data sanitization processor to logging
Introduces DataSanitizationProcessor to sanitize sensitive data in log records based on configurable rules. Updates QueryActionLogger and QueryFilterLogger to encode requests as arrays for improved processing. Registers the new processor in LoggerService to enhance privacy and compliance.
1 parent ab68db6 commit 307d83c

File tree

4 files changed

+214
-5
lines changed

4 files changed

+214
-5
lines changed

plugins/wpgraphql-logging/src/Events/QueryActionLogger.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,13 +164,14 @@ public function log_before_response_returned(
164164
if ( ! in_array( Events::BEFORE_RESPONSE_RETURNED, $selected_events, true ) ) {
165165
return;
166166
}
167-
$context = [
167+
$encoded_request = wp_json_encode( $request );
168+
$context = [
168169
'response' => $response,
169170
'schema' => $schema,
170171
'operation_name' => $operation,
171172
'query' => $query,
172173
'variables' => $variables,
173-
'request' => $request,
174+
'request' => false !== $encoded_request ? json_decode( $encoded_request, true ) : null,
174175
'query_id' => $query_id,
175176
];
176177
if ( ! $this->should_log_response( $this->config ) ) {

plugins/wpgraphql-logging/src/Events/QueryFilterLogger.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,13 +130,14 @@ public function log_graphql_request_results(
130130
}
131131

132132
/** @var \GraphQL\Server\OperationParams $params */
133-
$params = $request->params;
134-
$context = [
133+
$params = $request->params;
134+
$encoded_request = wp_json_encode( $request );
135+
$context = [
135136
'response' => $response,
136137
'operation_name' => $params->operation,
137138
'query' => $params->query,
138139
'variables' => $params->variables,
139-
'request' => $request,
140+
'request' => false !== $encoded_request ? json_decode( $encoded_request, true ) : null,
140141
'query_id' => $query_id,
141142
];
142143
if ( ! $this->should_log_response( $this->config ) ) {

plugins/wpgraphql-logging/src/Logger/LoggerService.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Monolog\Processor\ProcessorInterface;
1313
use Monolog\Processor\WebProcessor;
1414
use WPGraphQL\Logging\Logger\Handlers\WordPressDatabaseHandler;
15+
use WPGraphQL\Logging\Logger\Processors\DataSanitizationProcessor;
1516
use WPGraphQL\Logging\Logger\Processors\RequestHeadersProcessor;
1617

1718
/**
@@ -225,6 +226,7 @@ public static function get_default_processors(): array {
225226
new WebProcessor(), // Logs web request data. e.g. IP address, request method, URI, etc.
226227
new ProcessIdProcessor(), // Logs the process ID.
227228
new RequestHeadersProcessor(), // Custom processor to capture request headers.
229+
new DataSanitizationProcessor(), // Custom processor to sanitize data in log records.
228230
];
229231

230232
// Filter for users to add their own processors.
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WPGraphQL\Logging\Logger\Processors;
6+
7+
use Monolog\LogRecord;
8+
use Monolog\Processor\ProcessorInterface;
9+
use WPGraphQL\Logging\Admin\Settings\Fields\Tab\Data_Management_Tab;
10+
11+
/**
12+
* This class is responsible for sanitizing data in log records
13+
*
14+
* @package WPGraphQL\Logging
15+
*
16+
* @since 0.0.1
17+
*/
18+
class DataSanitizationProcessor implements ProcessorInterface {
19+
/**
20+
* The data management settings.
21+
*
22+
* @var array<string, string|int|bool|array<string>>
23+
*/
24+
protected array $config;
25+
26+
/**
27+
* DataSanitizationProcessor constructor.
28+
*/
29+
public function __construct() {
30+
$full_config = get_option( WPGRAPHQL_LOGGING_SETTINGS_KEY, [] );
31+
$this->config = $full_config['data_management'] ?? [];
32+
}
33+
34+
/**
35+
* Check if data sanitization is enabled.
36+
*/
37+
protected function is_enabled(): bool {
38+
$is_enabled = (bool) ( $this->config[ Data_Management_Tab::DATA_SANITIZATION_ENABLED ] ?? false );
39+
return apply_filters( 'wpgraphql_logging_data_sanitization_enabled', $is_enabled );
40+
}
41+
42+
/**
43+
* Get the sanitization rules based on the settings.
44+
*
45+
* @return array<string, mixed> The sanitization rules.
46+
*/
47+
protected function get_rules(): array {
48+
$method = $this->config[ Data_Management_Tab::DATA_SANITIZATION_METHOD ] ?? 'none';
49+
50+
51+
if ( 'recommended' === $method ) {
52+
return apply_filters( 'wpgraphql_logging_data_sanitization_rules', $this->get_recommended_rules() );
53+
}
54+
return apply_filters( 'wpgraphql_logging_data_sanitization_rules', $this->get_custom_rules() );
55+
}
56+
57+
/**
58+
* Get the recommended sanitization rules.
59+
*
60+
* @return array<string, mixed> The recommended sanitization rules.
61+
*/
62+
protected function get_recommended_rules(): array {
63+
$rules = [
64+
'request.app_context.viewer.data' => 'remove',
65+
'request.app_context.viewer.allcaps' => 'remove',
66+
'request.app_context.viewer.cap_key' => 'remove',
67+
'request.app_context.viewer.caps' => 'remove',
68+
];
69+
70+
return apply_filters( 'wpgraphql_logging_data_sanitization_recommended_rules', $rules );
71+
}
72+
73+
/**
74+
* Get the custom sanitization rules based on the settings.
75+
*
76+
* @return array<string, mixed> The custom sanitization rules.
77+
*/
78+
protected function get_custom_rules(): array {
79+
80+
$rules = [];
81+
$fields = [
82+
'anonymize' => $this->config[ Data_Management_Tab::DATA_SANITIZATION_CUSTOM_FIELD_ANONYMIZE ] ?? [],
83+
'remove' => $this->config[ Data_Management_Tab::DATA_SANITIZATION_CUSTOM_FIELD_REMOVE ] ?? [],
84+
'truncate' => $this->config[ Data_Management_Tab::DATA_SANITIZATION_CUSTOM_FIELD_TRUNCATE ] ?? [],
85+
];
86+
87+
foreach ( $fields as $action => $field_string ) {
88+
if ( empty( $field_string ) || ! is_string( $field_string ) ) {
89+
continue;
90+
}
91+
92+
$field_string = trim( $field_string );
93+
$field_list = array_filter(
94+
explode( ',', $field_string ),
95+
static function ($value) {
96+
return '' !== $value;
97+
}
98+
);
99+
100+
foreach ( $field_list as $field ) {
101+
$rules[ $field ] = $action;
102+
}
103+
}
104+
return $rules;
105+
}
106+
107+
/**
108+
* Apply a sanitization rule to a specific key in the data array.
109+
*
110+
* @param array<string, mixed> $data The data array to sanitize.
111+
* @param string $key The key to apply the rule to (dot notation for nested keys).
112+
* @param string $rule The sanitization rule ('anonymize', 'remove', 'truncate').
113+
*/
114+
protected function apply_rule(array &$data, string $key, string $rule): void {
115+
if ( empty( $data ) ) {
116+
return;
117+
}
118+
119+
$keys = explode( '.', $key );
120+
$last_key = array_pop( $keys );
121+
$current = &$this->navigate_to_parent( $data, $keys );
122+
123+
if ( null === $current || ! array_key_exists( $last_key, $current ) ) {
124+
return;
125+
}
126+
127+
$this->apply_sanitization_rule( $current, $last_key, $rule );
128+
}
129+
130+
/**
131+
* Navigate to the parent array of the target key.
132+
*
133+
* @param array<string, mixed> $data The data array to navigate.
134+
* @param array<string> $keys The keys to navigate through.
135+
*
136+
* @return array<string, mixed>|null The parent array or null if not found.
137+
*/
138+
protected function &navigate_to_parent(array &$data, array $keys): ?array {
139+
$current = &$data;
140+
foreach ( $keys as $segment ) {
141+
if ( ! is_array( $current ) || ! isset( $current[ $segment ] ) ) {
142+
$null = null;
143+
return $null;
144+
}
145+
$current = &$current[ $segment ];
146+
}
147+
return $current;
148+
}
149+
150+
/**
151+
* Apply the sanitization rule to the target value.
152+
*
153+
* @param array<string, mixed> $current The current array containing the target key.
154+
* @param string $key The key to sanitize.
155+
* @param string $rule The sanitization rule to apply.
156+
*
157+
* @phpcs:disable Generic.Metrics.NestingLevel.TooHigh
158+
*/
159+
protected function apply_sanitization_rule(array &$current, string $key, string $rule): void {
160+
switch ( $rule ) {
161+
case 'anonymize':
162+
$current[ $key ] = '***';
163+
break;
164+
case 'remove':
165+
unset( $current[ $key ] );
166+
break;
167+
case 'truncate':
168+
if ( is_string( $current[ $key ] ) ) {
169+
$current[ $key ] = substr( $current[ $key ], 0, 47 ) . '...';
170+
}
171+
break;
172+
}
173+
}
174+
175+
/**
176+
* This method is called for each log record. It adds the captured
177+
* request headers to the record's 'extra' array.
178+
*
179+
* @param \Monolog\LogRecord $record The log record to process.
180+
*
181+
* @return \Monolog\LogRecord The processed log record.
182+
*/
183+
public function __invoke( LogRecord $record ): LogRecord {
184+
185+
if ( ! $this->is_enabled() ) {
186+
return $record;
187+
}
188+
189+
$rules = $this->get_rules();
190+
191+
if ( empty( $rules ) ) {
192+
return $record;
193+
}
194+
195+
$context = $record['context'] ?? [];
196+
$extra = $record['extra'] ?? [];
197+
foreach ( $rules as $key => $rule ) {
198+
$this->apply_rule( $context, $key, $rule );
199+
$this->apply_rule( $extra, $key, $rule );
200+
}
201+
202+
$record = $record->with( context: $context, extra: $extra );
203+
return apply_filters( 'wpgraphql_logging_data_sanitization_record', $record );
204+
}
205+
}

0 commit comments

Comments
 (0)