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
12 changes: 12 additions & 0 deletions .env.testing
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
APP_NAME="Synapse Sentinel Core"
APP_ENV=testing
APP_KEY=base64:uu9NaQgjKWHuLvyg3EZvLNYjVgrkunyitUMq88LDf4c=
APP_DEBUG=true
APP_URL=http://localhost

DB_CONNECTION=sqlite
DB_DATABASE=:memory:

CACHE_STORE=array
QUEUE_CONNECTION=sync
SESSION_DRIVER=array
20 changes: 20 additions & 0 deletions app/Events/CertificationCompleted.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace App\Events;

use Thunk\Verbs\Event;

class CertificationCompleted extends Event
{
public function __construct(
public string $repository,
public string $sha,
public string $verdict,
public ?string $reason = null,
public ?array $checks = null,
public ?string $triggeredBy = null,
public ?int $prNumber = null,
) {}
}
27 changes: 27 additions & 0 deletions app/Http/Controllers/WebhookController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Events\CertificationCompleted;
use App\Http\Requests\GateWebhookRequest;
use Illuminate\Http\JsonResponse;

class WebhookController extends Controller
{
public function gate(GateWebhookRequest $request): JsonResponse
{
CertificationCompleted::fire(
repository: $request->validated('repository'),
sha: $request->validated('sha'),
verdict: $request->validated('verdict'),
reason: $request->validated('reason'),
checks: $request->validated('checks'),
triggeredBy: $request->validated('triggered_by'),
prNumber: $request->validated('pr_number'),
);

return response()->json(['status' => 'accepted']);
}
}
32 changes: 32 additions & 0 deletions app/Http/Requests/GateWebhookRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class GateWebhookRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}

/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'repository' => ['required', 'string'],
'sha' => ['required', 'string'],
'verdict' => ['required', Rule::in(['approved', 'rejected', 'escalate'])],
'reason' => ['nullable', 'string'],
'checks' => ['nullable', 'array'],
'triggered_by' => ['nullable', 'string'],
'pr_number' => ['nullable', 'integer'],
];
Comment on lines +12 to +30
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, verify the file exists and check its actual content
find . -name "GateWebhookRequest.php" -type f

Repository: synapse-sentinel/core

Length of output: 107


🏁 Script executed:

# Check if the file exists at the specified path and show its content
if [ -f "app/Http/Requests/GateWebhookRequest.php" ]; then
  echo "=== File found. Content:"; 
  cat -n "app/Http/Requests/GateWebhookRequest.php"
else
  echo "File not found at app/Http/Requests/GateWebhookRequest.php"
  # Search for it
  find . -name "*.php" -path "*/Requests/*" | grep -i webhook || echo "No webhook request files found"
fi

Repository: synapse-sentinel/core

Length of output: 1086


🏁 Script executed:

# Look for the route definition for this webhook endpoint
# Check common route files
echo "=== Checking routes/web.php:"; 
if [ -f "routes/web.php" ]; then rg -i "gatewebhook|webhook.*gate" routes/web.php -A 5; fi
echo ""
echo "=== Checking routes/api.php:"; 
if [ -f "routes/api.php" ]; then rg -i "gatewebhook|webhook.*gate" routes/api.php -A 5; fi
echo ""
echo "=== Checking all route files:"; 
find routes -name "*.php" -type f 2>/dev/null | xargs rg -i "gatewebhook|webhook.*gate" -A 5 2>/dev/null || echo "No matches found"

Repository: synapse-sentinel/core

Length of output: 374


🏁 Script executed:

# Search for any signature validation or auth mechanisms related to webhooks
echo "=== Searching for signature/HMAC validation:"; 
rg -i "signature|hmac|X-Hub-Signature" --type php | head -20
echo ""
echo "=== Searching for webhook auth patterns:"; 
rg -i "webhook.*auth|webhook.*token|webhook.*secret" --type php | head -20

Repository: synapse-sentinel/core

Length of output: 151


🏁 Script executed:

# Check for middleware that might be protecting the webhook route
echo "=== Looking for webhook-related middleware:"; 
find app/Http/Middleware -name "*.php" -type f 2>/dev/null | xargs rg -l "webhook" 2>/dev/null || echo "No webhook middleware found"
echo ""
echo "=== Searching for VerifyWebhook or similar patterns:"; 
rg "VerifyWebhook|verify.*webhook|webhook.*middleware" --type php

Repository: synapse-sentinel/core

Length of output: 221


🏁 Script executed:

# Search for any controller handling this request to see if there are additional checks
echo "=== Looking for controllers that might handle webhook requests:"; 
find app/Http/Controllers -name "*Webhook*.php" -o -name "*Gate*.php" | head -10

Repository: synapse-sentinel/core

Length of output: 171


🏁 Script executed:

# Check the full route definition with middleware
echo "=== Full routes/api.php around webhook route:"; 
rg -B 5 -A 10 "webhooks/gate" routes/api.php

Repository: synapse-sentinel/core

Length of output: 314


🏁 Script executed:

# Examine WebhookController to see how the webhook is processed
echo "=== WebhookController content:"; 
wc -l app/Http/Controllers/WebhookController.php

Repository: synapse-sentinel/core

Length of output: 141


🏁 Script executed:

# Read the WebhookController
head -100 app/Http/Controllers/WebhookController.php

Repository: synapse-sentinel/core

Length of output: 859


🏁 Script executed:

# Look at the test file to understand expected security behavior
echo "=== WebhookGateTest content (first 150 lines):"; 
head -150 tests/Feature/WebhookGateTest.php

Repository: synapse-sentinel/core

Length of output: 2335


🏁 Script executed:

# Search for any signature validation or custom middleware that might be applied
echo "=== Searching for signature or verification in config or middleware:"; 
find app/Http/Middleware -name "*.php" -type f 2>/dev/null | xargs cat 2>/dev/null

Repository: synapse-sentinel/core

Length of output: 133


🏁 Script executed:

# Check if there's a bootstrap or service provider that might register middleware
echo "=== Checking for webhook verification in service providers:"; 
rg -i "webhook|signature|verify" app/Providers --type php -A 3

Repository: synapse-sentinel/core

Length of output: 124


Add authentication to the webhook endpoint; currently any caller can submit certification verdicts

The authorize() method returns true unconditionally, the route has no middleware or auth guard, and no signature/HMAC validation exists anywhere in the codebase. This means any caller with network access can submit "approved" verdicts for any repository and commit, which is a critical security risk if this endpoint drives production gating decisions.

Implement one of:

  • Signature verification (e.g., validate X-Hub-Signature HMAC against a shared secret in middleware before the route is reached).
  • API token / bearer token authentication via an auth guard.
  • If network isolation is already in place (firewall restricting access to trusted infrastructure only), document this assumption explicitly in code or a security note.
🤖 Prompt for AI Agents
app/Http/Requests/GateWebhookRequest.php lines 12-30: authorize() currently
returns true allowing any caller to submit verdicts; add authentication by
verifying a shared secret signature or bearer token before the controller
handles the request. Implement middleware that reads the incoming request body
and the signature header (e.g., X-Hub-Signature / X-Signature) or Authorization
bearer token, compute an HMAC using a secret stored in env/config, compare it
using a timing-safe comparison, and abort with 401/403 on mismatch; register
that middleware on the webhook route (or wire it into route middleware group)
and keep authorize() conservative (return false or only true when the request
user/context is authenticated) or remove unconditional true so only
authenticated requests proceed; alternatively, if network isolation is
guaranteed, add a documented comment and a runtime check that ensures the
expected network constraint or env flag is set.

}
}
48 changes: 0 additions & 48 deletions app/Models/User.php

This file was deleted.

1 change: 1 addition & 0 deletions bootstrap/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
Expand Down
44 changes: 0 additions & 44 deletions database/factories/UserFactory.php

This file was deleted.

49 changes: 0 additions & 49 deletions database/migrations/0001_01_01_000000_create_users_table.php

This file was deleted.

283 changes: 9 additions & 274 deletions resources/views/welcome.blade.php

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions routes/api.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

use App\Http\Controllers\WebhookController;
use Illuminate\Support\Facades\Route;

Route::post('/webhooks/gate', [WebhookController::class, 'gate'])
->name('webhooks.gate');
16 changes: 13 additions & 3 deletions tests/Feature/ExampleTest.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
<?php

test('the application returns a successful response', function () {
$response = $this->get('/');
declare(strict_types=1);

$response->assertStatus(200);
describe('Application', function () {
it('returns a successful response for the homepage', function () {
$response = $this->get('/');

$response->assertOk();
});

it('has a health check endpoint', function () {
$response = $this->get('/up');

$response->assertOk();
});
});
72 changes: 72 additions & 0 deletions tests/Feature/WebhookGateTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

declare(strict_types=1);

describe('POST /api/webhooks/gate', function () {
it('accepts valid gate certification payload', function () {
$payload = [
'repository' => 'conduit-ui/pr',
'sha' => 'abc123def456',
'verdict' => 'approved',
'reason' => 'All checks passed',
'checks' => [
'tests' => ['status' => 'pass', 'coverage' => 100],
'security' => ['status' => 'pass'],
'syntax' => ['status' => 'pass'],
],
'triggered_by' => 'pull_request',
'pr_number' => 42,
];

$response = $this->postJson('/api/webhooks/gate', $payload);

$response->assertSuccessful();
$response->assertJson(['status' => 'accepted']);
});

it('rejects payload missing required repository field', function () {
$payload = [
'sha' => 'abc123',
'verdict' => 'approved',
];

$response = $this->postJson('/api/webhooks/gate', $payload);

$response->assertUnprocessable();
$response->assertJsonValidationErrors(['repository']);
});

it('rejects payload with invalid verdict value', function () {
$payload = [
'repository' => 'conduit-ui/pr',
'sha' => 'abc123',
'verdict' => 'invalid_verdict',
];

$response = $this->postJson('/api/webhooks/gate', $payload);

$response->assertUnprocessable();
$response->assertJsonValidationErrors(['verdict']);
});

it('stores certification event via Laravel Verbs', function () {
$payload = [
'repository' => 'conduit-ui/pr',
'sha' => 'abc123def456',
'verdict' => 'approved',
'reason' => 'All checks passed',
'checks' => [
'tests' => ['status' => 'pass', 'coverage' => 100],
],
'triggered_by' => 'push',
];

$response = $this->postJson('/api/webhooks/gate', $payload);

$response->assertSuccessful();

$this->assertDatabaseHas('verb_events', [
'type' => 'App\Events\CertificationCompleted',
]);
});
});
2 changes: 1 addition & 1 deletion tests/Pest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
*/

pest()->extend(Tests\TestCase::class)
// ->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
->in('Feature');

/*
Expand Down
8 changes: 6 additions & 2 deletions tests/Unit/ExampleTest.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<?php

test('that true is true', function () {
expect(true)->toBeTrue();
declare(strict_types=1);

describe('Unit', function () {
it('confirms basic assertions work', function () {
expect(true)->toBeTrue();
});
});