From 17c32f27fbcaa17ad843ab2ab599b3a54aa6cba7 Mon Sep 17 00:00:00 2001 From: Jonathan Danse Date: Wed, 8 Jul 2026 10:33:25 +0200 Subject: [PATCH] feat: expose research and remediation credit counts in top_security.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The top_security.json output now carries per-login `research` and `remediation` counts alongside the total `count`. The ranking still sorts by total (research + remediation together), so the front end can display both families in separate columns while keeping a single leaderboard. Remediation credits are a valuable signal on their own: the fixers of PrestaShop vulnerabilities push their commits through the private security repository, get their work republished by the core team, and therefore never appear as the author on any public PR — so this is the only structured source that surfaces them. Co-Authored-By: Claude Opus 4.7 --- README.md | 2 +- src/Command/GenerateTopSecurityCommand.php | 43 +++++++++++++--------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index a74db1a..6718209 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ The authentication use a Github Token. $ php bin/console traces:fetch:security-advisories ## A file gh_security_advisories.json is generated - # 10- Generate the security researchers leaderboard (research credits only) + # 10- Generate the security contributors leaderboard (research + remediation) $ php bin/console traces:generate:topsecurity --config="config.yml" ## A file top_security.json is generated ``` diff --git a/src/Command/GenerateTopSecurityCommand.php b/src/Command/GenerateTopSecurityCommand.php index 45d98a3..0326fe5 100644 --- a/src/Command/GenerateTopSecurityCommand.php +++ b/src/Command/GenerateTopSecurityCommand.php @@ -8,17 +8,18 @@ class GenerateTopSecurityCommand extends AbstractCommand { - // Only research-side credits are ranked here. Remediation credits - // (remediation_developer / reviewer / verifier) go to core developers who - // are already surfaced in the PR and commit leaderboards; counting them - // again would double-credit them and bury the security researchers this - // board exists to highlight. + // Credits split into two families. `count` sums both — the leaderboard + // ranks all credited security contributors together, and the front end + // shows the per-family breakdown next to it so the two kinds of work + // remain distinguishable. protected const RESEARCH_TYPES = ['reporter', 'finder', 'analyst', 'coordinator']; + protected const REMEDIATION_TYPES = ['remediation_developer', 'remediation_reviewer', 'remediation_verifier']; + protected function configure(): void { $this->setName('traces:generate:topsecurity') - ->setDescription('Generate the security researchers leaderboard from fetched advisory credits') + ->setDescription('Generate the security contributors leaderboard from fetched advisory credits') ->addOption( 'ghtoken', null, @@ -62,23 +63,27 @@ protected function execute(InputInterface $input, OutputInterface $output): int * @param array}> $advisories * @param array $contributors * - * @return array{updatedAt: string, items: array} + * @return array{updatedAt: string, items: array} */ public function buildRanking(array $advisories, array $contributors): array { - // Count, per login, the distinct advisories where they hold a research - // credit. Remediation-only contributors are intentionally left out (see - // RESEARCH_TYPES). + // Per login and per advisory, tag whether that person shows up in a + // research role, a remediation role, or both. Then per login, count + // the distinct advisories in each family; `count` totals both. $counts = []; $identities = []; foreach ($advisories as $advisory) { - $researchLogins = []; + $perLogin = []; foreach ($advisory['credits'] as $credit) { - if (!in_array($credit['type'] ?? '', self::RESEARCH_TYPES, true)) { + $type = $credit['type'] ?? ''; + $isResearch = in_array($type, self::RESEARCH_TYPES, true); + $isRemediation = in_array($type, self::REMEDIATION_TYPES, true); + if (!$isResearch && !$isRemediation) { continue; } $login = $credit['login']; - $researchLogins[$login] = true; + $perLogin[$login]['research'] = ($perLogin[$login]['research'] ?? false) || $isResearch; + $perLogin[$login]['remediation'] = ($perLogin[$login]['remediation'] ?? false) || $isRemediation; if (!isset($identities[$login])) { $identities[$login] = [ 'avatar_url' => $credit['avatar_url'] ?? '', @@ -86,14 +91,16 @@ public function buildRanking(array $advisories, array $contributors): array ]; } } - foreach (array_keys($researchLogins) as $login) { - $counts[$login] = ($counts[$login] ?? 0) + 1; + foreach ($perLogin as $login => $families) { + $counts[$login]['count'] = ($counts[$login]['count'] ?? 0) + 1; + $counts[$login]['research'] = ($counts[$login]['research'] ?? 0) + ($families['research'] ? 1 : 0); + $counts[$login]['remediation'] = ($counts[$login]['remediation'] ?? 0) + ($families['remediation'] ? 1 : 0); } } $logins = array_keys($counts); usort($logins, function (string $a, string $b) use ($counts): int { - return ($counts[$b] <=> $counts[$a]) ?: strcmp($a, $b); + return ($counts[$b]['count'] <=> $counts[$a]['count']) ?: strcmp($a, $b); }); $items = []; @@ -109,7 +116,9 @@ public function buildRanking(array $advisories, array $contributors): array 'name' => (string) ($contributor['name'] ?? $login), 'avatar_url' => (string) ($contributor['avatar_url'] ?? $identities[$login]['avatar_url']), 'html_url' => (string) ($contributor['html_url'] ?? $identities[$login]['html_url']), - 'count' => $counts[$login], + 'count' => $counts[$login]['count'], + 'research' => $counts[$login]['research'], + 'remediation' => $counts[$login]['remediation'], ]; ++$rank; }