Skip to content
2 changes: 2 additions & 0 deletions bin/console
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

declare(strict_types=1);

use Deviantintegral\Har\Command\SanitizeCommand;
use Deviantintegral\Har\Command\SplitCommand;

require_once __DIR__ . '/../vendor/autoload.php';
Expand All @@ -11,6 +12,7 @@ require_once __DIR__ . '/../vendor/autoload.php';
$application = new Symfony\Component\Console\Application('HAR Tools', '@git-version@ (built: @build-time@)');

$application->add(new SplitCommand());
$application->add(new SanitizeCommand());

// Run it
$application->run();
22 changes: 22 additions & 0 deletions infection.json5
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,28 @@
// so yield $index => $cloned is equivalent to yield $cloned (PHP auto-generates same keys)
"Deviantintegral\\Har\\Har::splitLogEntries"
]
},
"MethodCallRemoval": {
"ignore": [
// Equivalent mutation: params are modified in place via setValue(), and setQueryString
// just sets the same array back - removing it doesn't change behavior
"Deviantintegral\\Har\\HarSanitizer::sanitizeQueryParams"
]
},
"LogicalAnd": {
"ignore": [
// Equivalent mutation: when hasText() is false but isJsonMimeType() is true,
// sanitizeJsonText(null) returns null, so setText is not called - same behavior
"Deviantintegral\\Har\\HarSanitizer::sanitizePostData",
"Deviantintegral\\Har\\HarSanitizer::sanitizeContent"
]
},
"ReturnRemoval": {
"ignore": [
// Equivalent mutation: removing early return for empty string falls through to
// json_decode('') which fails with JSON_ERROR_SYNTAX, then error check returns null anyway
"Deviantintegral\\Har\\HarSanitizer::sanitizeJsonText"
]
}
}
}
99 changes: 99 additions & 0 deletions src/Command/SanitizeCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

declare(strict_types=1);

namespace Deviantintegral\Har\Command;

use Deviantintegral\Har\HarSanitizer;
use Deviantintegral\Har\Serializer;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

/**
* Command to sanitize sensitive data from a HAR file.
*/
class SanitizeCommand extends Command
{
protected function configure(): void
{
$this->setName('har:sanitize')
->setDescription('Sanitize sensitive data from a HAR file')
->setHelp('Redact sensitive values like authorization headers, API keys, and passwords from HAR files.')
->addArgument('har', InputArgument::REQUIRED, 'The source HAR file to sanitize.')
->addArgument('output', InputArgument::OPTIONAL, 'The output file path. Defaults to stdout.')
->addOption('header', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Header name to redact (can be specified multiple times).')
->addOption('query-param', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Query parameter name to redact (can be specified multiple times).')
->addOption('body-field', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Body field name to redact (can be specified multiple times).')
->addOption('case-sensitive', null, InputOption::VALUE_NONE, 'Use case-sensitive matching for field names. Defaults to case-insensitive.');
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$source = $input->getArgument('har');

if (!file_exists($source)) {
$io->error(\sprintf('File not found: %s', $source));

return Command::FAILURE;
}

if (is_dir($source)) {
$io->error(\sprintf('Path is a directory, not a file: %s', $source));

return Command::FAILURE;
}

$contents = file_get_contents($source);
if (false === $contents) {
$io->error(\sprintf('Unable to read file: %s', $source));

Check warning on line 53 in src/Command/SanitizeCommand.php

View workflow job for this annotation

GitHub Actions / Annotate

src/Command/SanitizeCommand.php#L53

This line is not covered by a test

return Command::FAILURE;

Check warning on line 55 in src/Command/SanitizeCommand.php

View workflow job for this annotation

GitHub Actions / Annotate

src/Command/SanitizeCommand.php#L55

This line is not covered by a test
}

$serializer = new Serializer();
$har = $serializer->deserializeHar($contents);

$sanitizer = new HarSanitizer();

if ($input->getOption('case-sensitive')) {
$sanitizer->setCaseSensitive(true);
}

$headers = $input->getOption('header');
if (!empty($headers)) {
$sanitizer->redactHeaders($headers);
}

$queryParams = $input->getOption('query-param');
if (!empty($queryParams)) {
$sanitizer->redactQueryParams($queryParams);
}

$bodyFields = $input->getOption('body-field');
if (!empty($bodyFields)) {
$sanitizer->redactBodyFields($bodyFields);
}

$sanitized = $sanitizer->sanitize($har);
$result = $serializer->serializeHar($sanitized);

$outputPath = $input->getArgument('output');
if (null !== $outputPath) {
if (false === file_put_contents($outputPath, $result)) {
$io->error(\sprintf('Unable to write to file: %s', $outputPath));

Check warning on line 88 in src/Command/SanitizeCommand.php

View workflow job for this annotation

GitHub Actions / Annotate

src/Command/SanitizeCommand.php#L88

This line is not covered by a test

return Command::FAILURE;

Check warning on line 90 in src/Command/SanitizeCommand.php

View workflow job for this annotation

GitHub Actions / Annotate

src/Command/SanitizeCommand.php#L90

This line is not covered by a test
}
$io->success(\sprintf('Sanitized HAR written to %s', $outputPath));
} else {
$output->write($result);
}

return Command::SUCCESS;
}
}
26 changes: 26 additions & 0 deletions src/Entry.php
Original file line number Diff line number Diff line change
Expand Up @@ -177,4 +177,30 @@ public function setInitiator(Initiator $_initiator): self

return $this;
}

/**
* Deep clone all object properties when cloning Entry.
*/
public function __clone(): void
{
if (isset($this->request)) {
$this->request = clone $this->request;
}

if (isset($this->response)) {
$this->response = clone $this->response;
}

if (isset($this->cache)) {
$this->cache = clone $this->cache;
}

if (isset($this->timings)) {
$this->timings = clone $this->timings;
}

if (isset($this->_initiator)) {
$this->_initiator = clone $this->_initiator;
}
}
}
Loading