diff --git a/.github/workflows/container-release.yml b/.github/workflows/container-release.yml new file mode 100644 index 0000000..2843130 --- /dev/null +++ b/.github/workflows/container-release.yml @@ -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 + diff --git a/examples/demo_app/Dockerfile b/examples/demo_app/Dockerfile new file mode 100644 index 0000000..d97c641 --- /dev/null +++ b/examples/demo_app/Dockerfile @@ -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"] diff --git a/examples/demo_app/index.php b/examples/demo_app/index.php new file mode 100644 index 0000000..1669307 --- /dev/null +++ b/examples/demo_app/index.php @@ -0,0 +1,120 @@ + 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 + */ +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); +}