feat: anonymization command for request/response XML dumps

This commit is contained in:
Björn Fromme
2026-03-04 10:47:20 +01:00
parent cd5d353158
commit 90fc66277e
5 changed files with 744 additions and 1 deletions
+158
View File
@@ -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;
}
}