Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
43 changes: 26 additions & 17 deletions src/Command/GenerateTopSecurityCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -62,38 +63,44 @@ protected function execute(InputInterface $input, OutputInterface $output): int
* @param array<array{ghsa_id:string|null, credits:array<array{login:string, avatar_url:string|null, html_url:string|null, type:string|null}>}> $advisories
* @param array<string, mixed> $contributors
*
* @return array{updatedAt: string, items: array<array{rank:int, login:string, name:string, avatar_url:string, html_url:string, count:int}>}
* @return array{updatedAt: string, items: array<array{rank:int, login:string, name:string, avatar_url:string, html_url:string, count:int, research:int, remediation:int}>}
*/
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'] ?? '',
'html_url' => $credit['html_url'] ?? 'https://github.com/' . $login,
];
}
}
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 = [];
Expand All @@ -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;
}
Expand Down