Skip to content

Commit c392c3e

Browse files
authored
Merge pull request #27 from dgtlss/26-false-positive-laravelhorizon-classified-as-a-development-package
26 false positive laravelhorizon classified as a development package
2 parents 92cf628 + 039442e commit c392c3e

7 files changed

Lines changed: 453 additions & 7 deletions

File tree

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"notifications",
1111
"CVE"
1212
],
13-
"version": "1.5.1",
13+
"version": "1.5.3",
1414
"license": "MIT",
1515
"autoload": {
1616
"psr-4": {

readme.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,19 @@ WARDEN_SCHEDULE_TIME=03:00
166166
WARDEN_SCHEDULE_TIMEZONE=UTC
167167
```
168168

169+
### Ignoring Accepted Findings
170+
171+
If your team has reviewed a finding and wants to suppress it without forking the package, add an `ignore_findings` rule to `config/warden.php`.
172+
173+
```php
174+
'ignore_findings' => [
175+
['source' => 'debug-mode', 'package' => 'laravel/horizon'],
176+
['source' => 'debug-mode', 'title' => 'Testing routes*'],
177+
],
178+
```
179+
180+
All provided keys in a rule must match for the finding to be ignored. String values support wildcard matching.
181+
169182
---
170183

171184
## 🔍 Security Audits
@@ -604,4 +617,4 @@ If you find Warden useful for your organization's security needs, please conside
604617

605618
[⭐ Star on GitHub](https://github.com/dgtlss/warden) | [📦 Packagist](https://packagist.org/packages/dgtlss/warden) | [🐦 Follow Updates](https://twitter.com/nlangerdev)
606619

607-
</div>
620+
</div>

src/Commands/WardenAuditCommand.php

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Illuminate\Support\Facades\Http;
77
use Illuminate\Support\Facades\Mail;
88
use Illuminate\Support\Collection;
9+
use Illuminate\Support\Str;
910
use Carbon\Carbon;
1011
use Symfony\Component\Process\Process;
1112
use Dgtlss\Warden\Services\Audits\ComposerAuditService;
@@ -193,6 +194,7 @@ protected function runSequentialAudits(bool $isMachineOutput = false): int
193194

194195
protected function processResults(array $allFindings, array $abandonedPackages, bool $hasFailures): int
195196
{
197+
$allFindings = $this->filterIgnoredFindings($allFindings);
196198
$totalBeforeFilter = count($allFindings);
197199
$severityOption = null;
198200

@@ -232,6 +234,75 @@ protected function processResults(array $allFindings, array $abandonedPackages,
232234
return $hasFailures ? 2 : 0;
233235
}
234236

237+
/**
238+
* Remove findings that match configured ignore rules.
239+
*
240+
* @param array<array<string, mixed>> $findings
241+
* @return array<array<string, mixed>>
242+
*/
243+
protected function filterIgnoredFindings(array $findings): array
244+
{
245+
$ignoreRules = config('warden.ignore_findings', []);
246+
247+
if (!is_array($ignoreRules) || $ignoreRules === []) {
248+
return $findings;
249+
}
250+
251+
return array_values(array_filter(
252+
$findings,
253+
fn (array $finding): bool => !$this->shouldIgnoreFinding($finding, $ignoreRules)
254+
));
255+
}
256+
257+
/**
258+
* @param array<string, mixed> $finding
259+
* @param array<mixed> $ignoreRules
260+
*/
261+
protected function shouldIgnoreFinding(array $finding, array $ignoreRules): bool
262+
{
263+
foreach ($ignoreRules as $rule) {
264+
if (!is_array($rule) || !$this->findingMatchesIgnoreRule($finding, $rule)) {
265+
continue;
266+
}
267+
268+
return true;
269+
}
270+
271+
return false;
272+
}
273+
274+
/**
275+
* @param array<string, mixed> $finding
276+
* @param array<mixed> $rule
277+
*/
278+
protected function findingMatchesIgnoreRule(array $finding, array $rule): bool
279+
{
280+
$matchedKey = false;
281+
282+
foreach ($rule as $key => $expectedValue) {
283+
if (!is_string($key) || $key === '' || !array_key_exists($key, $finding)) {
284+
return false;
285+
}
286+
287+
$matchedKey = true;
288+
289+
if (!$this->findingValueMatchesRule($finding[$key], $expectedValue)) {
290+
return false;
291+
}
292+
}
293+
294+
return $matchedKey;
295+
}
296+
297+
protected function findingValueMatchesRule(mixed $actualValue, mixed $expectedValue): bool
298+
{
299+
if (is_string($actualValue) && is_string($expectedValue)) {
300+
return Str::is($expectedValue, $actualValue);
301+
}
302+
303+
return $actualValue === $expectedValue;
304+
}
305+
235306
/**
236307
* Display the current version of Warden.
237308
*/

src/Services/Audits/DebugModeAuditService.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ class DebugModeAuditService extends AbstractAuditService
77
private array $devPackages = [
88
'barryvdh/laravel-debugbar',
99
'laravel/telescope',
10-
'laravel/horizon',
1110
'beyondcode/laravel-dump-server',
1211
'laravel/dusk',
1312
];
@@ -124,7 +123,6 @@ private function hasExposedTestingRoutes(): bool
124123
// Check other testing routes that should never be exposed in production
125124
$testingRoutes = [
126125
'telescope',
127-
'horizon',
128126
'_dusk',
129127
];
130128

src/config/warden.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,25 @@
8888
],
8989
],
9090

91+
/*
92+
|--------------------------------------------------------------------------
93+
| Ignored Findings
94+
|--------------------------------------------------------------------------
95+
|
96+
| Suppress accepted or context-specific findings without forking the package.
97+
| Each rule is matched against the final finding payload and all provided
98+
| keys must match for the finding to be ignored.
99+
|
100+
| Examples:
101+
| - ['source' => 'debug-mode', 'package' => 'laravel/horizon']
102+
| - ['source' => 'debug-mode', 'title' => 'Testing routes*']
103+
|
104+
*/
105+
106+
'ignore_findings' => [
107+
// ['source' => 'debug-mode', 'package' => 'laravel/horizon'],
108+
],
109+
91110
/*
92111
|--------------------------------------------------------------------------
93112
| Custom Audits

tests/Commands/WardenAuditCommandTest.php

Lines changed: 143 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@
22

33
namespace Dgtlss\Warden\Tests\Commands;
44

5-
use Dgtlss\Warden\Commands\WardenAuditCommand;
6-
use Illuminate\Support\Facades\Artisan;
7-
use Orchestra\Testbench\TestCase;
85
use Dgtlss\Warden\Providers\WardenServiceProvider;
6+
use Dgtlss\Warden\Services\AuditCacheService;
97
use Dgtlss\Warden\Services\AuditExecutor;
8+
use Dgtlss\Warden\Services\Audits\ComposerAuditService;
9+
use Dgtlss\Warden\Services\Audits\DebugModeAuditService;
10+
use Dgtlss\Warden\Services\Audits\EnvAuditService;
11+
use Dgtlss\Warden\Services\Audits\StorageAuditService;
12+
use Illuminate\Support\Facades\Http;
13+
use Illuminate\Support\Facades\Mail;
1014
use Mockery\MockInterface;
15+
use Orchestra\Testbench\TestCase;
1116

1217
class WardenAuditCommandTest extends TestCase
1318
{
@@ -56,4 +61,139 @@ public function testAuditCommandHandlesFindings(): void
5661
->expectsOutputToContain('1 security issue found.')
5762
->assertExitCode(1);
5863
}
64+
65+
public function testAuditCommandIgnoresConfiguredFindingsBeforeNotifications(): void
66+
{
67+
Http::fake();
68+
Mail::fake();
69+
70+
config([
71+
'warden.webhook_url' => 'https://example.com/webhook',
72+
'warden.email_recipients' => '[email protected]',
73+
'warden.ignore_findings' => [
74+
['source' => 'debug-mode', 'package' => 'laravel/horizon'],
75+
],
76+
]);
77+
78+
$findings = [
79+
[
80+
'source' => 'debug-mode',
81+
'title' => 'Development package detected in production',
82+
'severity' => 'high',
83+
'package' => 'laravel/horizon',
84+
],
85+
];
86+
87+
$this->mock(AuditExecutor::class, function (MockInterface $mock) use ($findings): void {
88+
$mock->shouldReceive('addAudit')->zeroOrMoreTimes();
89+
$mock->shouldReceive('execute')->once()->andReturn([
90+
'debug-mode' => [
91+
'success' => true,
92+
'findings' => $findings,
93+
'service' => new \stdClass(),
94+
],
95+
]);
96+
});
97+
98+
$this->artisan('warden:audit')
99+
->expectsOutputToContain('Warden')
100+
->expectsOutputToContain('No security issues found.')
101+
->assertExitCode(0);
102+
103+
Http::assertNothingSent();
104+
Mail::assertNothingSent();
105+
}
106+
107+
public function testAuditCommandSupportsWildcardIgnoreRulesInJsonOutput(): void
108+
{
109+
config([
110+
'warden.ignore_findings' => [
111+
['source' => 'debug-mode', 'title' => 'Testing routes*'],
112+
],
113+
]);
114+
115+
$findings = [
116+
[
117+
'source' => 'debug-mode',
118+
'title' => 'Testing routes are exposed',
119+
'severity' => 'high',
120+
'package' => 'routes',
121+
],
122+
];
123+
124+
$this->mock(AuditExecutor::class, function (MockInterface $mock) use ($findings): void {
125+
$mock->shouldReceive('addAudit')->zeroOrMoreTimes();
126+
$mock->shouldReceive('execute')->once()->andReturn([
127+
'debug-mode' => [
128+
'success' => true,
129+
'findings' => $findings,
130+
'service' => new \stdClass(),
131+
],
132+
]);
133+
});
134+
135+
$this->artisan('warden:audit', ['--output' => 'json'])
136+
->expectsOutputToContain('"vulnerabilities_found": 0')
137+
->assertExitCode(0);
138+
}
139+
140+
public function testAuditCommandFiltersCachedFindingsInSequentialMode(): void
141+
{
142+
config([
143+
'warden.audits.parallel_execution' => false,
144+
'warden.ignore_findings' => [
145+
['source' => 'debug-mode', 'package' => 'laravel/horizon'],
146+
],
147+
]);
148+
149+
$this->mock(AuditCacheService::class, function (MockInterface $mock): void {
150+
$mock->shouldReceive('hasRecentAudit')
151+
->times(4)
152+
->andReturnUsing(fn (string $auditName): bool => $auditName === 'debug-mode');
153+
154+
$mock->shouldReceive('getCachedResult')
155+
->once()
156+
->with('debug-mode')
157+
->andReturn([
158+
'result' => [
159+
[
160+
'source' => 'debug-mode',
161+
'title' => 'Development package detected in production',
162+
'severity' => 'high',
163+
'package' => 'laravel/horizon',
164+
],
165+
],
166+
'timestamp' => now()->toIso8601String(),
167+
'cached' => true,
168+
]);
169+
});
170+
171+
$this->mock(ComposerAuditService::class, function (MockInterface $mock): void {
172+
$mock->shouldReceive('getName')->once()->andReturn('composer');
173+
$mock->shouldReceive('run')->once()->andReturn(true);
174+
$mock->shouldReceive('getFindings')->once()->andReturn([]);
175+
$mock->shouldReceive('getAbandonedPackages')->once()->andReturn([]);
176+
});
177+
178+
$this->mock(EnvAuditService::class, function (MockInterface $mock): void {
179+
$mock->shouldReceive('getName')->once()->andReturn('environment');
180+
$mock->shouldReceive('run')->once()->andReturn(true);
181+
$mock->shouldReceive('getFindings')->once()->andReturn([]);
182+
});
183+
184+
$this->mock(StorageAuditService::class, function (MockInterface $mock): void {
185+
$mock->shouldReceive('getName')->once()->andReturn('storage');
186+
$mock->shouldReceive('run')->once()->andReturn(true);
187+
$mock->shouldReceive('getFindings')->once()->andReturn([]);
188+
});
189+
190+
$this->mock(DebugModeAuditService::class, function (MockInterface $mock): void {
191+
$mock->shouldReceive('getName')->once()->andReturn('debug-mode');
192+
});
193+
194+
$this->artisan('warden:audit', ['--no-notify' => true])
195+
->expectsOutputToContain('Warden')
196+
->expectsOutputToContain('No security issues found.')
197+
->assertExitCode(0);
198+
}
59199
}

0 commit comments

Comments
 (0)