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
62 changes: 62 additions & 0 deletions .github/workflows/container-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Publish Containerized Demo

on:
release:
types: [created]
push:
branches:
- main
paths:
- 'examples/**'
- 'src/**'
- 'composer.json'
- 'composer.lock'
- '.github/workflows/container-release.yml'
workflow_dispatch:

jobs:
publish-container:
name: Build and push the containerized demo
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Set version tag
id: set_version
run: |
if [[ "${{ github.event_name }}" == "release" ]]; then
echo "VERSION=${{ github.event.release.tag_name }}" >> "$GITHUB_ENV"
echo "IS_RELEASE=true" >> "$GITHUB_ENV"
else
echo "VERSION=latest" >> "$GITHUB_ENV"
echo "IS_RELEASE=false" >> "$GITHUB_ENV"
fi

- name: Log in to GHCR
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build the demo image
run: |
docker build -f examples/demo_app/Dockerfile -t guard-agent-php-demo .

- name: Tag and push to GHCR
run: |
docker tag guard-agent-php-demo ghcr.io/guard-core/guard-agent-php:latest
docker push ghcr.io/guard-core/guard-agent-php:latest

if [[ "${{ env.IS_RELEASE }}" == "true" ]]; then
docker tag guard-agent-php-demo ghcr.io/guard-core/guard-agent-php:${{ env.VERSION }}
docker push ghcr.io/guard-core/guard-agent-php:${{ env.VERSION }}
fi

33 changes: 33 additions & 0 deletions examples/demo_app/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Build context is the repository root:
# docker build -f examples/demo_app/Dockerfile .
FROM composer:2 AS build

WORKDIR /app

COPY composer.json composer.lock ./
RUN composer install --no-interaction --no-progress

FROM php:8.3-cli

# The agent's transport speaks HTTPS to the ingestion API.
RUN docker-php-ext-install curl && \
useradd --create-home --uid 1000 guard

COPY --from=build /app/vendor /app/vendor
COPY src/ /app/src/
COPY examples/demo_app/ /app/examples/demo_app/

LABEL org.opencontainers.image.title="guard-agent-php demo" \
org.opencontainers.image.description="Containerized Guard Agent demo: a small PHP service streaming security telemetry to a Guard Core ingestion endpoint" \
org.opencontainers.image.source="https://github.com/Guard-Core/guard-agent-php" \
org.opencontainers.image.licenses="MIT"

ENV GUARD_AGENT_ENDPOINT=https://api.guard-core.com \
GUARD_AGENT_PROJECT_ID=demo-project

USER guard
WORKDIR /app

EXPOSE 8080

CMD ["php", "-S", "0.0.0.0:8080", "examples/demo_app/index.php"]
120 changes: 120 additions & 0 deletions examples/demo_app/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?php

declare(strict_types=1);

/**
* demo_app serves the Guard Agent demo container: the PHP built-in server
* hosts a small router that starts guard-agent-php, ships one test event on
* boot, and exposes the agent lifecycle over three routes (/ : service
* info, /health : agent status, POST /events : emit a test event). It
* mirrors the Python agent's examples/demo_app.
*
* PHP is host-driven: flush timers only run when the host calls the agent,
* so the router drives flushIfNeeded() per request and stop() at shutdown.
*
* php -S 0.0.0.0:8080 examples/demo_app/index.php
*/

use RenzoFranceschini\GuardAgent\Config\AgentConfigResolver;
use RenzoFranceschini\GuardAgent\GuardAgent;

require __DIR__ . '/../../vendor/autoload.php';

const DEMO_EVENT_TYPE = 'custom_request_check';

/**
* Boots the agent once per server process; the built-in server keeps the
* process (and this static) alive across requests.
*/
function agent(): GuardAgent
{
static $agent = null;
if ($agent instanceof GuardAgent) {
return $agent;
}

$agent = new GuardAgent(AgentConfigResolver::resolve([
'apiKey' => getenv('GUARD_AGENT_API_KEY') ?: 'demo-api-key-12345',
'endpoint' => getenv('GUARD_AGENT_ENDPOINT') ?: 'https://api.guard-core.com',
'projectId' => getenv('GUARD_AGENT_PROJECT_ID') ?: 'demo-project',
'payloadSigningSecret' => getenv('GUARD_AGENT_SIGNING_SECRET') ?: null,
'guardVersion' => 'demo',
'bufferSize' => 10,
'flushInterval' => 5,
]));

// start() is the only method that may throw (config validation happens
// in the resolver; start() can fail on crash-recovery load).
$agent->start();

// Boot-time test event, like the Python demo's lifespan startup.
$agent->sendEvent(buildTestEvent());

register_shutdown_function(static function () use ($agent): void {
$agent->stop();
});

return $agent;
}

/**
* @return array<string, mixed>
*/
function buildTestEvent(): array
{
return [
'event_type' => DEMO_EVENT_TYPE,
'ip_address' => '192.168.1.100',
'action_taken' => 'logged',
'reason' => 'Guard Agent demo container test event',
'endpoint' => '/demo/test-event',
'method' => 'POST',
'metadata' => ['source' => 'guard-agent-demo-container'],
];
}

/**
* @return never
*/
function respond(array $payload, int $status = 200): void
{
http_response_code($status);
header('Content-Type: application/json');
echo json_encode($payload, JSON_THROW_ON_ERROR), "\n";
exit;
}

$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';

$agent = agent();

switch ([$method, $path]) {
case ['GET', '/']:
respond([
'service' => 'guard-agent-demo',
'endpoint' => getenv('GUARD_AGENT_ENDPOINT') ?: 'https://api.guard-core.com',
'project_id' => getenv('GUARD_AGENT_PROJECT_ID') ?: 'demo-project',
]);
// no break: respond() exits.

case ['GET', '/health']:
// Host-driven flush: timers only advance when the host calls in.
$agent->flushIfNeeded();
respond(['status' => $agent->getStatus()->status]);

case ['POST', '/events']:
$event = buildTestEvent();
// Never throws, never blocks (default drop policy).
$agent->sendEvent($event);
respond(['emitted' => $event['event_type']]);

default:
if (($path === '/' || $path === '/health') && $method !== 'GET') {
respond(['error' => 'GET only'], 405);
}
if ($path === '/events' && $method !== 'POST') {
respond(['error' => 'POST only'], 405);
}
respond(['error' => 'not found'], 404);
}
Loading