Skip to content

Commit 9167aef

Browse files
Copilotildyria
andauthored
feat(032): Security Advisories Check – spec, plan, tasks, and config (#4263)
Co-authored-by: ildyria <627094+ildyria@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ildyria <beviguier@gmail.com>
1 parent 76a3f05 commit 9167aef

58 files changed

Lines changed: 2839 additions & 12 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/Actions/Diagnostics/Errors.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
use App\Actions\Diagnostics\Pipes\Checks\OpCacheCheck;
3131
use App\Actions\Diagnostics\Pipes\Checks\PHPVersionCheck;
3232
use App\Actions\Diagnostics\Pipes\Checks\PlaceholderExistsCheck;
33+
use App\Actions\Diagnostics\Pipes\Checks\SecurityAdvisoriesCheck;
3334
use App\Actions\Diagnostics\Pipes\Checks\SmallMediumExistsCheck;
3435
use App\Actions\Diagnostics\Pipes\Checks\StatisticsIntegrityCheck;
3536
use App\Actions\Diagnostics\Pipes\Checks\SupporterCheck;
@@ -77,6 +78,7 @@ class Errors
7778
WatermarkerEnabledCheck::class,
7879
StatisticsIntegrityCheck::class,
7980
WebshopCheck::class,
81+
SecurityAdvisoriesCheck::class,
8082
];
8183

8284
/**
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
/**
4+
* SPDX-License-Identifier: MIT
5+
* Copyright (c) 2017-2018 Tobias Reich
6+
* Copyright (c) 2018-2026 LycheeOrg.
7+
*/
8+
9+
namespace App\Actions\Diagnostics\Pipes\Checks;
10+
11+
use App\Assets\Features;
12+
use App\Contracts\DiagnosticPipe;
13+
use App\DTO\DiagnosticData;
14+
use App\Models\User;
15+
use App\Services\SecurityAdvisoriesService;
16+
use Illuminate\Support\Facades\Auth;
17+
18+
/**
19+
* Diagnostic pipe that reports known security vulnerabilities affecting the
20+
* currently installed Lychee version.
21+
*
22+
* Only runs when:
23+
* - the `vulnerability-check` feature flag is enabled, and
24+
* - the currently authenticated user is an administrator.
25+
*
26+
* Vulnerability data is never disclosed to non-admin users.
27+
*/
28+
class SecurityAdvisoriesCheck implements DiagnosticPipe
29+
{
30+
public function __construct(
31+
private SecurityAdvisoriesService $service,
32+
) {
33+
}
34+
35+
/**
36+
* {@inheritDoc}
37+
*/
38+
public function handle(array &$data, \Closure $next): array
39+
{
40+
if (Features::inactive('vulnerability-check')) {
41+
return $next($data);
42+
}
43+
44+
/** @var User|null */
45+
$user = Auth::user();
46+
47+
if ($user?->may_administrate !== true) {
48+
return $next($data);
49+
}
50+
51+
$advisories = $this->service->getMatchingAdvisories();
52+
53+
foreach ($advisories as $advisory) {
54+
$identifier = $advisory->cve_id ?? $advisory->ghsa_id;
55+
$score = $advisory->cvss_score !== null
56+
? number_format($advisory->cvss_score, 1)
57+
: '(no CVSS score)';
58+
59+
$data[] = DiagnosticData::error(
60+
'Security vulnerability: ' . $identifier . ' (CVSS ' . $score . ')',
61+
self::class,
62+
[$advisory->summary],
63+
);
64+
}
65+
66+
return $next($data);
67+
}
68+
}

app/DTO/SecurityAdvisory.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
/**
4+
* SPDX-License-Identifier: MIT
5+
* Copyright (c) 2017-2018 Tobias Reich
6+
* Copyright (c) 2018-2026 LycheeOrg.
7+
*/
8+
9+
namespace App\DTO;
10+
11+
/**
12+
* Immutable DTO representing a single GitHub Security Advisory that
13+
* affects the running Lychee version.
14+
*/
15+
class SecurityAdvisory
16+
{
17+
public function __construct(
18+
public readonly ?string $cve_id,
19+
public readonly string $ghsa_id,
20+
public readonly string $summary,
21+
public readonly ?float $cvss_score,
22+
public readonly ?string $cvss_vector,
23+
public readonly string $affected_version_range,
24+
) {
25+
}
26+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
/**
4+
* SPDX-License-Identifier: MIT
5+
* Copyright (c) 2017-2018 Tobias Reich
6+
* Copyright (c) 2018-2026 LycheeOrg.
7+
*/
8+
9+
namespace App\Http\Controllers\Admin;
10+
11+
use App\Assets\Features;
12+
use App\Http\Requests\Admin\SecurityAdvisories\IndexSecurityAdvisoriesRequest;
13+
use App\Http\Resources\Models\SecurityAdvisoryResource;
14+
use App\Services\SecurityAdvisoriesService;
15+
use Illuminate\Routing\Controller;
16+
17+
/**
18+
* Admin controller that exposes the list of security advisories matching
19+
* the currently installed Lychee version.
20+
*
21+
* Only administrators may call this endpoint. Non-admins receive 403.
22+
* When the vulnerability-check feature is disabled the endpoint returns an
23+
* empty array.
24+
*/
25+
class SecurityAdvisoriesController extends Controller
26+
{
27+
public function __construct(
28+
private SecurityAdvisoriesService $service,
29+
) {
30+
}
31+
32+
/**
33+
* Return the list of advisories that affect the running Lychee version.
34+
*
35+
* @param IndexSecurityAdvisoriesRequest $_request
36+
*
37+
* @return SecurityAdvisoryResource[]
38+
*/
39+
public function index(IndexSecurityAdvisoriesRequest $_request): array
40+
{
41+
if (Features::inactive('vulnerability-check')) {
42+
return [];
43+
}
44+
45+
return array_map(
46+
fn ($advisory) => SecurityAdvisoryResource::fromAdvisory($advisory),
47+
$this->service->getMatchingAdvisories(),
48+
);
49+
}
50+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
/**
4+
* SPDX-License-Identifier: MIT
5+
* Copyright (c) 2017-2018 Tobias Reich
6+
* Copyright (c) 2018-2026 LycheeOrg.
7+
*/
8+
9+
namespace App\Http\Requests\Admin\SecurityAdvisories;
10+
11+
use App\Http\Requests\AbstractEmptyRequest;
12+
use App\Models\User;
13+
use Illuminate\Support\Facades\Auth;
14+
15+
/**
16+
* FormRequest for the Security Advisories index endpoint.
17+
*
18+
* Only administrators may retrieve advisory data.
19+
* Unauthenticated requests receive 401 (Laravel default for failed authorize).
20+
* Authenticated non-admin requests receive 403.
21+
*/
22+
class IndexSecurityAdvisoriesRequest extends AbstractEmptyRequest
23+
{
24+
/**
25+
* Only allow administrators to call this endpoint.
26+
*/
27+
public function authorize(): bool
28+
{
29+
/** @var User|null */
30+
$user = Auth::user();
31+
32+
return $user?->may_administrate === true;
33+
}
34+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
/**
4+
* SPDX-License-Identifier: MIT
5+
* Copyright (c) 2017-2018 Tobias Reich
6+
* Copyright (c) 2018-2026 LycheeOrg.
7+
*/
8+
9+
namespace App\Http\Resources\Models;
10+
11+
use App\DTO\SecurityAdvisory;
12+
use Spatie\LaravelData\Data;
13+
use Spatie\TypeScriptTransformer\Attributes\TypeScript;
14+
15+
/**
16+
* API resource for a single security advisory that affects the running
17+
* Lychee version.
18+
*/
19+
#[TypeScript()]
20+
class SecurityAdvisoryResource extends Data
21+
{
22+
public function __construct(
23+
public readonly ?string $cve_id,
24+
public readonly string $ghsa_id,
25+
public readonly string $summary,
26+
public readonly ?float $cvss_score,
27+
public readonly ?string $cvss_vector,
28+
) {
29+
}
30+
31+
/**
32+
* Build a resource from a SecurityAdvisory DTO.
33+
*
34+
* @param SecurityAdvisory $advisory
35+
*
36+
* @return self
37+
*/
38+
public static function fromAdvisory(SecurityAdvisory $advisory): self
39+
{
40+
return new self(
41+
cve_id: $advisory->cve_id,
42+
ghsa_id: $advisory->ghsa_id,
43+
summary: $advisory->summary,
44+
cvss_score: $advisory->cvss_score,
45+
cvss_vector: $advisory->cvss_vector,
46+
);
47+
}
48+
}

app/Metadata/Cache/RouteCacheManager.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,9 @@ public function __construct()
160160

161161
'api/v2/Contact' => false,
162162
'api/v2/Contact::Init' => new RouteCacheConfig(tag: CacheTag::SETTINGS, user_dependant: false),
163+
164+
// No need to cache this.
165+
'api/v2/Security/Advisories' => false,
163166
];
164167
}
165168

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
/**
4+
* SPDX-License-Identifier: MIT
5+
* Copyright (c) 2017-2018 Tobias Reich
6+
* Copyright (c) 2018-2026 LycheeOrg.
7+
*/
8+
9+
namespace App\Metadata\Json;
10+
11+
use Illuminate\Support\Facades\Config;
12+
13+
/**
14+
* HTTP request class for the GitHub Security Advisories API.
15+
*
16+
* Extends JsonRequestFunctions to send the required
17+
* "Accept: application/vnd.github+json" header.
18+
*/
19+
class AdvisoriesRequest extends JsonRequestFunctions
20+
{
21+
/**
22+
* We just override the constructor.
23+
* The rest is handled directly by the parent class.
24+
*/
25+
public function __construct()
26+
{
27+
parent::__construct(
28+
Config::get('urls.advisories.api_url'),
29+
Config::get('urls.advisories.cache_ttl'),
30+
['Accept: application/vnd.github+json'],
31+
);
32+
}
33+
}

app/Metadata/Json/ExternalRequestFunctions.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,15 @@ class ExternalRequestFunctions implements ExternalRequest
1919
{
2020
protected ?string $data = null;
2121

22+
/**
23+
* @param string $url URL to fetch
24+
* @param int $ttl_in_days cache TTL in days
25+
* @param string[] $extra_headers additional HTTP headers to send (e.g. ['Accept: application/json'])
26+
*/
2227
public function __construct(
2328
private string $url,
2429
private int $ttl_in_days,
30+
private array $extra_headers = [],
2531
) {
2632
}
2733

@@ -114,9 +120,10 @@ private function fetchFromServer(): string
114120
'http' => [
115121
'method' => 'GET',
116122
'timeout' => 1,
117-
'header' => [
118-
'User-Agent: ' . ini_get('user_agent'),
119-
],
123+
'header' => array_merge(
124+
['User-Agent: ' . ini_get('user_agent')],
125+
$this->extra_headers,
126+
),
120127
],
121128
];
122129
$context = stream_context_create($opts);

0 commit comments

Comments
 (0)