feat: anonymization command for request/response XML dumps
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Service\BpnXmlAnonymizer;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:bpn:xml-anonymize',
|
||||
description: 'Anonymize personal data in BPN XML request/response dumps',
|
||||
)]
|
||||
class BpnXmlAnonymizeCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private readonly BpnXmlAnonymizer $xmlAnonymizer,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addArgument('infile', InputArgument::REQUIRED, 'Path to the input XML file')
|
||||
->addArgument('outfile', InputArgument::OPTIONAL, 'Path to the output XML file (defaults to infile)')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$inputFile = (string) $input->getArgument('infile');
|
||||
$outputFile = $input->getArgument('outfile');
|
||||
|
||||
if (false === is_file($inputFile)) {
|
||||
$io->error(sprintf('Input file not found: %s', $inputFile));
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
if (false === is_readable($inputFile)) {
|
||||
$io->error(sprintf('Input file is not readable: %s', $inputFile));
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$targetFile = null === $outputFile ? $inputFile : (string) $outputFile;
|
||||
$this->xmlAnonymizer->resetState();
|
||||
|
||||
$processingResult = $this->anonymizeFile($io, $inputFile, $targetFile, false);
|
||||
if (Command::SUCCESS !== $processingResult) {
|
||||
return $processingResult;
|
||||
}
|
||||
|
||||
$pairedInputFile = $this->derivePairedFilename($inputFile);
|
||||
if (null !== $pairedInputFile && true === is_file($pairedInputFile)) {
|
||||
if (false === is_readable($pairedInputFile)) {
|
||||
$io->error(sprintf('Paired file is not readable: %s', $pairedInputFile));
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$pairedTargetFile = $this->derivePairedOutputFilename($targetFile, $inputFile, $pairedInputFile);
|
||||
if (null === $pairedTargetFile) {
|
||||
$io->error(sprintf(
|
||||
'Unable to derive paired output filename from: %s',
|
||||
$targetFile
|
||||
));
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$processingResult = $this->anonymizeFile($io, $pairedInputFile, $pairedTargetFile, true);
|
||||
if (Command::SUCCESS !== $processingResult) {
|
||||
return $processingResult;
|
||||
}
|
||||
|
||||
$io->note(sprintf('Paired file anonymized: %s -> %s', $pairedInputFile, $pairedTargetFile));
|
||||
}
|
||||
|
||||
$io->success(sprintf(
|
||||
'Anonymized %d persons from %s to %s',
|
||||
$this->xmlAnonymizer->getLastIdentityCount(),
|
||||
$inputFile,
|
||||
$targetFile,
|
||||
));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function anonymizeFile(SymfonyStyle $io, string $inputFile, string $targetFile, bool $keepState): int
|
||||
{
|
||||
$xml = file_get_contents($inputFile);
|
||||
if (false === $xml) {
|
||||
$io->error(sprintf('Unable to read input file: %s', $inputFile));
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
try {
|
||||
$anonymizedXml = $this->xmlAnonymizer->anonymize($xml, false === $keepState);
|
||||
} catch (\InvalidArgumentException|\RuntimeException $exception) {
|
||||
$io->error(sprintf('Failed to anonymize %s: %s', $inputFile, $exception->getMessage()));
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$writeResult = file_put_contents($targetFile, $anonymizedXml);
|
||||
if (false === $writeResult) {
|
||||
$io->error(sprintf('Unable to write output file: %s', $targetFile));
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function derivePairedFilename(string $filename): ?string
|
||||
{
|
||||
if (str_ends_with($filename, '_request.xml')) {
|
||||
return substr($filename, 0, -12).'_response.xml';
|
||||
}
|
||||
|
||||
if (str_ends_with($filename, '_response.xml')) {
|
||||
return substr($filename, 0, -13).'_request.xml';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function derivePairedOutputFilename(string $outputFile, string $inputFile, string $pairedInputFile): ?string
|
||||
{
|
||||
if (str_ends_with($inputFile, '_request.xml') && str_ends_with($pairedInputFile, '_response.xml')) {
|
||||
if (false === str_ends_with($outputFile, '_request.xml')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return substr($outputFile, 0, -12).'_response.xml';
|
||||
}
|
||||
|
||||
if (str_ends_with($inputFile, '_response.xml') && str_ends_with($pairedInputFile, '_request.xml')) {
|
||||
if (false === str_ends_with($outputFile, '_response.xml')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return substr($outputFile, 0, -13).'_request.xml';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user