From 0fc48ccf58be02b6fa70f9516a12602860fb12f3 Mon Sep 17 00:00:00 2001 From: Niels Keurentjes Date: Fri, 17 Nov 2023 13:00:57 +0100 Subject: [PATCH] Prepare 0.1.4 --- CHANGELOG.md | 13 ++++++++- src/Command/StatisticsCommand.php | 47 ++++++++++++++++++++++++++---- src/Utility/StringCounter.php | 48 +++++++++++++++++++++++++++++++ tests/Unit/UtilityTest.php | 40 ++++++++++++++++++++++++++ 4 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 src/Utility/StringCounter.php create mode 100644 tests/Unit/UtilityTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index c8042de..a98dd83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +## [0.1.4] - 2023-11-17 +### Added + - Implement ResetInterface for proper adaptation to long running servers (#10) + - Implement basic quarantine functionality + - Add PSR-compliant application logging + - Add basic antispam::stats command + +### Fixed + - Stealth behavior in embedded forms should now be correct + ## [0.1.3] - 2023-11-15 ### Added - All caught spam is now put into a quarantine folder @@ -29,7 +39,8 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## 0.1.0 - 2023-11-10 First public release. -[Unreleased]: https://github.com/omines/antispam-bundle/compare/0.1.3...master +[Unreleased]: https://github.com/omines/antispam-bundle/compare/0.1.4...master +[0.1.4]: https://github.com/omines/antispam-bundle/compare/0.1.3...0.1.4 [0.1.3]: https://github.com/omines/antispam-bundle/compare/0.1.2...0.1.3 [0.1.2]: https://github.com/omines/antispam-bundle/compare/0.1.1...0.1.2 [0.1.1]: https://github.com/omines/antispam-bundle/compare/0.1.0...0.1.1 diff --git a/src/Command/StatisticsCommand.php b/src/Command/StatisticsCommand.php index d98a120..558f420 100644 --- a/src/Command/StatisticsCommand.php +++ b/src/Command/StatisticsCommand.php @@ -13,9 +13,13 @@ namespace Omines\AntiSpamBundle\Command; use Omines\AntiSpamBundle\AntiSpam; +use Omines\AntiSpamBundle\Utility\StringCounter; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Helper\TableCell; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Finder\Finder; use Symfony\Component\Yaml\Yaml; @@ -31,6 +35,7 @@ public function __construct(private readonly AntiSpam $antiSpam) protected function configure(): void { $this + ->addOption('limit', 'l', InputOption::VALUE_OPTIONAL, 'Number of results to show in rankings. Defaults to number of days in quarantine.') ->setHelp(<<<'EOF' The %command.name% command lists general statistics from the file based anti-spam quarantine. @@ -49,13 +54,21 @@ protected function execute(InputInterface $input, OutputInterface $output) return self::FAILURE; } - $output->writeln(sprintf('Gathering data from quarantine folder at %s', $config['dir'])); + $output->writeln(sprintf('Analyzing data from quarantine folder at %s', $config['dir'])); + + $limit = $input->getOption('limit'); + $limit = (is_string($limit) ? intval($limit) : 0) ?: $config['max_days'] ?: 25; $finder = (new Finder()) ->files() ->name('*.yaml') ->in($config['dir']) + ->sortByName() ; + + $ips = new StringCounter(); + $causes = new StringCounter(); + $dates = new StringCounter(); foreach ($finder as $file) { $items = Yaml::parse($file->getContents()); if (!is_array($items)) { @@ -63,13 +76,37 @@ protected function execute(InputInterface $input, OutputInterface $output) continue; } foreach ($items as $item) { - $output->writeln(''); - $output->writeln(sprintf('Time: %s', $item['time'])); - $output->writeln(sprintf('Message: %s', $item['antispam'][0]['message'])); - $output->writeln(sprintf('Cause: %s', $item['antispam'][0]['cause'])); + if (array_key_exists('request', $item)) { + $ips->add($item['request']['client_ip']); + } + $dates->add((new \DateTimeImmutable($item['time']))->format('Y-m-d')); + $causes->add($item['antispam'][0]['cause']); } } + $table = new Table($output); + $table->setHeaders([ + [new TableCell('By date', ['colspan' => 2]), new TableCell('By IP', ['colspan' => 2]), new TableCell('By cause', ['colspan' => 2])], + ['Date', '#', 'IP', '#', 'Cause', '#'], + ]); + + $dates = $dates->getScores(); + $ips = $ips->getRanking($limit); + $causes = $causes->getRanking($limit); + $max = max(count($dates), count($ips), count($causes)); + + for ($i = 0; $i < $max; ++$i) { + @$table->addRow([ + $dates[$i][0], + $dates[$i][1], + $ips[$i][0], + $ips[$i][1], + $causes[$i][0], + $causes[$i][1], + ]); + } + $table->render(); + return self::SUCCESS; } } diff --git a/src/Utility/StringCounter.php b/src/Utility/StringCounter.php new file mode 100644 index 0000000..700645f --- /dev/null +++ b/src/Utility/StringCounter.php @@ -0,0 +1,48 @@ + */ + private array $scores = []; + + public function add(string $string): void + { + array_key_exists($string, $this->scores) ? $this->scores[$string]++ : ($this->scores[$string] = 1); + } + + /** + * @return array{string, int}[] + */ + public function getScores(bool $sortByKey = true): array + { + if ($sortByKey) { + ksort($this->scores); + } + + return array_map(fn ($k, $v) => [$k, $v], array_keys($this->scores), $this->scores); + } + + /** + * @return array{string, int}[] + */ + public function getRanking(int $max = null): array + { + arsort($this->scores, SORT_NUMERIC); + + $slice = array_slice($this->scores, 0, $max); + + return array_map(fn ($k, $v) => [$k, $v], array_keys($slice), $slice); + } +} diff --git a/tests/Unit/UtilityTest.php b/tests/Unit/UtilityTest.php new file mode 100644 index 0000000..e22a932 --- /dev/null +++ b/tests/Unit/UtilityTest.php @@ -0,0 +1,40 @@ +add('foo'); + $test->add('bar'); + $test->add('bar'); + $test->add('baz'); + $test->add('baz'); + $test->add('baz'); + + $unsorted = $test->getScores(false); + $this->assertSame(['foo', 1], $unsorted[0]); + + $sorted = $test->getScores(); + $this->assertSame(['bar', 2], $sorted[0]); + + $ranking = $test->getRanking(2); + $this->assertCount(2, $ranking); + $this->assertSame(['baz', 3], $ranking[0]); + } +}