Skip to content
Merged
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
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,21 @@ Headers listed in `excluded_request_headers` are stored with their value replace

When `'*'` or a connector/request class matches, every header is dropped from the recording except Barstool's own `X-Barstool-UUID` correlation header.

Header names are matched case-insensitively, so `authorization` is caught by an `Authorization` exclusion.

### Redacting sensitive response headers

Response headers work the same way via `excluded_response_headers` — matching values are stored as `REDACTED`. The `Set-Cookie` header is redacted by default so session cookies never land in your logs:

```php
'excluded_response_headers' => [
'Set-Cookie',
// '*', // redact every response header
// SensitiveConnector::class, // redact all response headers for a connector
// SensitiveRequest::class, // or a single request
],
```

### Excluding response bodies

Response bodies for sensitive endpoints can be kept out of the database entirely — they are stored as `REDACTED`:
Expand Down
13 changes: 13 additions & 0 deletions config/barstool.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,19 @@
// SensitiveConnector::class // Exclude `token` header for this request
],

/*
* Any response headers that should be excluded from the recording.
* Will replace the header value with `REDACTED`.
* Use '*' or a connector/request class name to redact all headers
* for every response or for that connector/request.
*/
'excluded_response_headers' => [
'Set-Cookie',
// '*', // All headers
// SensitiveConnector::class,
// SensitiveRequest::class,
],

/*
* Queue configuration for recording.
* When enabled, recordings will be dispatched as queued jobs
Expand Down
30 changes: 29 additions & 1 deletion src/Barstool.php
Original file line number Diff line number Diff line change
Expand Up @@ -182,13 +182,41 @@ private static function getResponseData(Response $response): array

return [
'url' => $response->getPsrRequest()->getUri(),
'response_headers' => $response->headers()->all(),
'response_headers' => self::getResponseHeaders($response),
'response_body' => $responseBody,
'response_status' => $response->status(),
'successful' => $response->successful(),
];
}

/**
* @return array<string, mixed>
*/
public static function getResponseHeaders(Response $response): array
{
$excludedHeaders = config('barstool.excluded_response_headers', []);
$headers = collect($response->headers()->all());

// '*' or a listed connector/request class redacts every header value
if (in_array('*', $excludedHeaders)
|| in_array(get_class($response->getConnector()), $excludedHeaders)
|| in_array(get_class($response->getRequest()), $excludedHeaders)) {
return $headers->map(fn () => 'REDACTED')->toArray();
}

// Header names are matched case-insensitively, consistent with the
// request-side exclusions.
$excludedHeaders = array_map(mb_strtolower(...), $excludedHeaders);

return $headers->map(function ($value, $key) use ($excludedHeaders) {
if (in_array(mb_strtolower($key), $excludedHeaders)) {
$value = 'REDACTED';
}

return $value;
})->toArray();
}

/**
* @return array{
* url: UriInterface,
Expand Down
43 changes: 43 additions & 0 deletions tests/BarstoolTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1118,3 +1118,46 @@

assertDatabaseCount('barstools', 0);
});

it('redacts the Set-Cookie response header by default', function () {
config()->set('barstool.enabled', true);

MockClient::global([
SoloUserRequest::class => MockResponse::make(
body: ['data' => 'ok'],
status: 200,
headers: ['set-cookie' => 'session=secret-value', 'token' => 'abc123'],
),
]);

(new SoloUserRequest)->send();

$barstool = Barstool::sole();

expect($barstool->response_headers['set-cookie'])->toBe('REDACTED');
expect($barstool->response_headers['token'])->toBe('abc123');
});

it('can redact all response headers or by connector or request class', function ($excluded) {
config()->set('barstool.enabled', true);
config()->set('barstool.excluded_response_headers', [$excluded]);

MockClient::global([
SoloUserRequest::class => MockResponse::make(
body: ['data' => 'ok'],
status: 200,
headers: ['token' => 'abc123', 'other' => 'value'],
),
]);

(new SoloUserRequest)->send();

$barstool = Barstool::sole();

expect($barstool->response_headers['token'])->toBe('REDACTED');
expect($barstool->response_headers['other'])->toBe('REDACTED');
})->with([
'*',
NullConnector::class,
SoloUserRequest::class,
]);