# Project export: Gallop

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2026
- Tagline: Stop breaches in real-time. Gallop is a multi-agent, AI-native cybersecurity control platform that detects intrusion activity and converts security signals into immediate defensive actions.
- Devpost: https://devpost.com/software/gallop
- GitHub: https://github.com/Eth007/gallop
- Demo: https://joingallop.com/
- Video: https://www.youtube.com/embed/QRSf0SUPioM?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner ([Elastic] Best end-to-end Agentic system on Elasticsearch (1st Place: $2,000 (split across team members) 2nd Place: $1,000 (split across team members)))
- Team: 1 GitHub contributor(s) — Eth007 (2 commits)

## Devpost submission (written by the team)

### Inspiration

In today’s world of big data, where it becomes increasingly difficult to distinguish between pertinent and irrelevant information, identifying meaningful threats under time-pressure is essential. Many current cybersecurity operations are hindered by tedious manual investigation, fragmented tool usage, and delayed bureaucratic remediation—all of which result in attacker advantages as they seek to exploit the gap between detection and action. Signal overload, where analysts must interpret large volumes of telemetry and alerts under severe time constraints, is one of the most significant burdens for modern security operations. Solution Gallop is a real-time cybersecurity control platform that autonomously converts security signals into immediate defensive actions. By galloping through security data, our tool detects intrusion activity through continuous system telemetry monitoring. Once detected, an appropriate response is quickly deployed as determined by the autonomous multi-agent architecture. Gallop seeks to limit the detection-to-action gap and contain threats in real time. Instead of relying solely on manual investigation workflows, the AI-powered platform builds contextual understanding from live telemetry and validates threats through Elasticsearch-driven signal correlation. Finally, it executes containment and remediation actions in real time. Using Elasticsearch's advanced vector search capabilities with Jina models via Elastic Inference Service (EIS), Gallop utilizes context retrieval through a multi-agent architecture, managed by an orchestrator agent triggered by Elasticsearch workflows. This event-driven workflow automation makes autonomous, human-free decisions. Essentially, Elastic provides the necessary retrieval and sensing system, and Gallop provides the reflexes and operational control plane, compressing detection-to-action cycles from human to operational timescales. Gallop’s specialty and central thesis lie in its continuous decision-making and response optimization, which result in latency compression and system stabilization. Rather than rigid, predefined rules, Gallop’s multi-agent model uses contextual reasoning to autonomously decide on and execute security protocol. How We Built It We dove deep into new tools and programs this weekend. We primarily utilized Elastic’s many services, including Elasticsearch running on Elastic Cloud as our telemetry aggregation and reasoning function. Jina models were used for embeddings generation and vector similarity, in order to better classify events to match with pre-defined threat categories. Using Elastic’s Agent Builder and Workflow functions, we deployed a multi-agent architecture with event-triggering loops. The individual agents running on Gallop-defended machines are powered by the OpenAI API, and the orchestrator is built with Elastic AI Agent Builder, which oversees event-driven workflows and response actions. Data collection is done using an eBPF-based framework, inspired by the execsnoop.bt tool that comes with BPFTrace. We utilized this technology because it allowed us real-time access to what is actually being executed on a computer, allowing us to have significantly more visibility into security-relevant data. To test the efficacy of our project, we also created a simulated intrusion platform to test how quickly Gallop could detect and mitigate exploits. Challenges We Ran Into This was our first time using the Elastic product suite, so familiarizing ourselves with the ecosystem took a significant portion of our attention and time. A primary difficulty in this project was creating the multi-agent infrastructure. Given the fact that many observed behaviors were non-deterministic, ensuring the accuracy of the agents’ autonomous decision-making proved rather difficult during initial tests. Erroneous false positives and false negatives presented themselves during early tests; a significant number of tests and iterations were required to ameliorate these issues and yield far more accurate decision-making results. Accomplishments We're Proud Of Utilized Elasticsearch for telemetry ingestion and low-latency querying. Integrated Jina models for data analysis. Used Elastic Agent Builder to design an autonomous multi-agent architecture Made a dashboard for system visibility What We Learned On the technical front, we gained experience with Elastic’s low-latency querying abilities, vector search, and workflow and agent interactions. This project taught us how embeddings and reranking models can turn massive datasets into a manageable decision-making context. Navigating Elastic API demonstrated the tool’s full abilities and versatility for wide-ranging objectives. We also gained great insight into creating a clean, user-friendly UI whilst integrating real-time updating backend capabilities. Looking Forward We are excited to see Gallop’s future potential. With additional time and resources, we envision creating a more robust framework for Gallop’s risk-awareness and decision-making abilities. These additions would enable Gallop to act with precision and stability across wide-ranging industries—bringing security to our modern and fast-changing world.

## README (from the GitHub repository)

# Gallop Defensive Agents

Multi-agent defense framework composed of three cooperating services:

- `agent/`: Rust + eBPF responder that observes process starts and executes bounded remediation tasks.
- `server/`: Axum control plane that coordinates detections, queues actions, and brokers AI reasoning for agents.
- `frontend/`: Next.js 16 command console that renders the current posture and suggests next actions via Jina embeddings.

## Plain-English Overview
- Mission: shrink detection-to-containment time with cooperating agents.
- Pattern: sense activity → reason with policy/AI → act → verify.
- Role split: control plane plans, host agents execute, console validates.
- Benefit: closed-loop defense that records protection history instead of alert noise.
- Result: higher containment rates with fewer manual touches.

## Quick Start
1) Launch Elasticsearch and note the endpoint plus an API key.
2) Start the control plane: `cd server && cargo run`. It exposes the APIs on port 8080 and an interactive `cmd>` prompt for issuing agent instructions.
3) Build and run an agent on a Linux host with eBPF support: `cd agent && LIBBPF_SYS_USE_SYSTEM=1 cargo run --release` (usually needs `sudo`).
4) Bring up the console: `cd frontend && pnpm install && pnpm dev`, then open `http://localhost:3000`.

## Control Plane (`server/`)
- **Purpose:** Receives process docs from agents at `/v1/ingest/processes`, forwards them to Elasticsearch, maintains a command queue (`/v1/agent/commands`), accepts agent results, and exposes `/v1/openai/chat` as a reasoning broker.
- **Environment:**
  - `ELASTIC_URL` (required) – Elasticsearch node URL
  - `ELASTIC_API_KEY` (required) – API key for the cluster
  - `PROCESS_INDEX` (default `logs-processstart`) – index to write into
  - `OPENAI_API_KEY` (optional) – enables `/v1/openai/chat`
  - `OPENAI_MODEL` / `OPENAI_BASE` (optional) – override model or base URL
- **Run:** `make run` or `cargo run`. The CLI accepts `list` (show agent IDs) and `<agent_id> <instruction>` to enqueue work.

## Agent (`agent/`)
- **Purpose:** Attaches the `bpf/execsnoop.bpf.c` program to `sys_enter_execve`, buffers process metadata, and batches it to the server. It polls `/v1/agent/commands`, runs scoped tasks (5s timeout), and reports structured outcomes.
- **Environment:**
  - `SERVER_URL` (default `https://api.joingallop.com`) – control plane base URL
  - `OPENAI_MODEL` (optional) – model name passed to the server’s OpenAI proxy
  - Agent ID is the host name; ensure it matches commands you enqueue.
- **Prereqs:** Linux with eBPF enabled, kernel headers, `clang`/`llvm`, `libbpf` (build expects `LIBBPF_SYS_USE_SYSTEM=1`), and root privileges to load the BPF program.
- **Run:** `cd agent && LIBBPF_SYS_USE_SYSTEM=1 cargo run --release` (or `make run`).

## Console (`frontend/`)
- **Purpose:** Reads Elasticsearch threat data, renders current defenses, lists agents, and provides `/api/jina/insight` for AI judgment on commands.
- **Environment (.env.local):**
  - `ELASTIC_URL` – Elasticsearch endpoint
  - `ELASTIC_API_KEY` – API key
  - `PROCESS_INDEX` – index to query (should match server)
  - `JINA_API_KEY` / `JINA_MODEL_ID` (optional) – enable Jina-based command analysis
- **Run:** `pnpm install && pnpm dev` (or `pnpm build && pnpm start`).

## Coordination Flow
1. Agents capture exec events via eBPF and post batches to `POST /v1/ingest/processes`.
2. Control plane enriches/forwards events into Elasticsearch and tracks active agent IDs.
3. Console API routes query Elasticsearch for posture summaries, threat charts, and agent status.
4. Use the server CLI to enqueue instructions; agents fetch them, optionally call the OpenAI proxy, execute bounded actions, and report results to `/v1/agent/result`.

## Detection and Response
1. Activity lands in Elasticsearch with host/user/process context.
2. Detection rules, IOC matches, and Jina v3 embeddings (via Elastic Inference Service) label events as malicious/suspicious/benign.
3. A threat score merges AI confidence, rule severity, heuristics (e.g., netcat reverse shell, `curl | bash`), and asset context.
4. Policy maps score + criticality + allowlist/denylist into actions: investigate, contain, kill process, quarantine host, or log-only.
5. Agents receive scoped plans, execute bounded steps, and emit structured results.
6. Control plane verifies remediation by rechecking Elasticsearch state and closes or escalates.

## Workflow Loop
1) Events flow into Elasticsearch
2) Detection rule fires
3) Workflow triggers automatically
4) Agent is invoked with a scoped plan
5) Agent returns structured action/result
6) Workflow executes deterministic response (kill proc, isolate host, notify, ticket)
7) Verification re-reads ES and closes or retries

## Why Agents (instead of alert-only)
- Shrinks detection-to-action latency and cuts alert fatigue.
- Runs repeatable, policy-checked remediation without external SOAR glue.
- Consolidates automation in one control plane (Elasticsearch + agents + workflows).
- Focuses on protection history (what was remediated, when, how) instead of just scan history.

## Product Narrative
- User signs up and gets a console backed by Elasticsearch; credentials stay in their environment.
- They deploy agents on hosts; each streams exec context and enforces actions locally.
- 14-day Elastic Cloud Serverless trial can power ingestion without new infra.
- Jina models via EIS generate embeddings and rerank search results to improve detections.
- Elastic Workflows and Agent Builder wire detections to actions; custom tools can be added for environment-specific remediation.

## Development Notes
- Rust toolchain 1.75+ recommended for `agent/` and `server/`.
- Node 20+ with `pnpm` for the dashboard.
- Formatting helpers: `make fmt` in Rust projects; `next lint` available in `frontend/`.
- When running agents on production hosts, review and harden command execution policies before enabling remote instructions.


## Detected evidence (automated analysis)

Indexed codebase: 108 recognized source files, 751 KB.
- C (language) — detected in the code
- CSS (language) — detected in the code
- Next.js (technology) — detected in the code
- React (technology) — detected in the code
- Rust (language) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- OpenAI (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (120 of 122)

```
.env.example
.gitignore
.gitmodules
agent/.gitignore
agent/bpf/execsnoop.bpf.c
agent/build.rs
agent/Cargo.lock
agent/Cargo.toml
agent/Makefile
agent/src/bpf_skel.rs
agent/src/main.rs
frontend/app/(auth)/login/page.tsx
frontend/app/(auth)/signup/page.tsx
frontend/app/api/dashboard/agents/route.ts
frontend/app/api/dashboard/charts/route.ts
frontend/app/api/dashboard/overview/route.ts
frontend/app/api/dashboard/threats/route.ts
frontend/app/api/elastic/health/route.ts
frontend/app/api/jina/insight/route.ts
frontend/app/dashboard/agents/page.tsx
frontend/app/dashboard/download/page.tsx
frontend/app/dashboard/layout.tsx
frontend/app/dashboard/page.tsx
frontend/app/dashboard/scans/page.tsx
frontend/app/dashboard/threats/page.tsx
frontend/app/globals.css
frontend/app/layout.tsx
frontend/app/page.tsx
frontend/components.json
frontend/components/dashboard/active-threats-card.tsx
frontend/components/dashboard/agent-card.tsx
frontend/components/dashboard/health-score-gauge-card.tsx
frontend/components/dashboard/recent-alerts-table.tsx
frontend/components/dashboard/sidebar.tsx
frontend/components/dashboard/stat-card.tsx
frontend/components/dashboard/threat-breakdown-chart.tsx
frontend/components/dashboard/threat-chart.tsx
frontend/components/landing/cta-footer.tsx
frontend/components/landing/features.tsx
frontend/components/landing/hero-scroll.tsx
frontend/components/landing/hero.tsx
frontend/components/landing/how-it-works.tsx
frontend/components/landing/navbar.tsx
frontend/components/theme-provider.tsx
frontend/components/theme-toggle.tsx
frontend/components/ui/accordion.tsx
frontend/components/ui/alert-dialog.tsx
frontend/components/ui/alert.tsx
frontend/components/ui/aspect-ratio.tsx
frontend/components/ui/avatar.tsx
frontend/components/ui/badge.tsx
frontend/components/ui/breadcrumb.tsx
frontend/components/ui/button.tsx
frontend/components/ui/calendar.tsx
frontend/components/ui/card.tsx
frontend/components/ui/carousel.tsx
frontend/components/ui/chart.tsx
frontend/components/ui/checkbox.tsx
frontend/components/ui/collapsible.tsx
frontend/components/ui/command.tsx
frontend/components/ui/container-scroll-animation.tsx
frontend/components/ui/container-scroll-demo.tsx
frontend/components/ui/context-menu.tsx
frontend/components/ui/dialog.tsx
frontend/components/ui/drawer.tsx
frontend/components/ui/dropdown-menu.tsx
frontend/components/ui/form.tsx
frontend/components/ui/glow-button.tsx
frontend/components/ui/glowing-effect-demo.tsx
frontend/components/ui/glowing-effect.tsx
frontend/components/ui/hover-card.tsx
frontend/components/ui/input-otp.tsx
frontend/components/ui/input.tsx
frontend/components/ui/label.tsx
frontend/components/ui/menubar.tsx
frontend/components/ui/navigation-menu.tsx
frontend/components/ui/pagination.tsx
frontend/components/ui/popover.tsx
frontend/components/ui/progress.tsx
frontend/components/ui/radio-group.tsx
frontend/components/ui/resizable.tsx
frontend/components/ui/scroll-area.tsx
frontend/components/ui/select.tsx
frontend/components/ui/separator.tsx
frontend/components/ui/sheet.tsx
frontend/components/ui/sidebar.tsx
frontend/components/ui/skeleton.tsx
frontend/components/ui/slider.tsx
frontend/components/ui/sonner.tsx
frontend/components/ui/switch.tsx
frontend/components/ui/table.tsx
frontend/components/ui/tabs.tsx
frontend/components/ui/textarea.tsx
frontend/components/ui/toast.tsx
frontend/components/ui/toaster.tsx
frontend/components/ui/toggle-group.tsx
frontend/components/ui/toggle.tsx
frontend/components/ui/tooltip.tsx
frontend/components/ui/use-mobile.tsx
frontend/components/ui/use-toast.ts
frontend/hooks/use-mobile.tsx
frontend/hooks/use-toast.ts
frontend/lib/auth-context.tsx
frontend/lib/mock-data.ts
frontend/lib/server/dashboard-data.ts
frontend/lib/server/elastic.ts
frontend/lib/server/env.ts
frontend/lib/server/jina.ts
frontend/lib/types/security.ts
frontend/lib/utils.ts
frontend/next-env.d.ts
frontend/next.config.mjs
frontend/package.json
frontend/postcss.config.mjs
frontend/styles/globals.css
frontend/tailwind.config.ts
frontend/tsconfig.json
README.md
server/Cargo.lock
server/Cargo.toml
[2 more files omitted for size]
```

### Dependencies

- agent/Cargo.toml: anyhow@1.0, chrono@0.4, hostname@0.3, inotify@0.10, libbpf-cargo@0.26, libbpf-rs@0.26, once_cell@1.19, regex@1, reqwest@0.11, serde@1.0, serde_json@1.0, tokio@1.35, tracing@0.1, tracing-subscriber@0.3, uuid@1.6
- frontend/package.json: @elastic/elasticsearch@^9.3.1, @hookform/resolvers@^3.9.1, @radix-ui/react-accordion@1.2.2, @radix-ui/react-alert-dialog@1.1.4, @radix-ui/react-aspect-ratio@1.1.1, @radix-ui/react-avatar@1.1.2, @radix-ui/react-checkbox@1.1.3, @radix-ui/react-collapsible@1.1.2, @radix-ui/react-context-menu@2.2.4, @radix-ui/react-dialog@1.1.4, @radix-ui/react-dropdown-menu@2.1.4, @radix-ui/react-hover-card@1.1.4, @radix-ui/react-label@2.1.1, @radix-ui/react-menubar@1.1.4, @radix-ui/react-navigation-menu@1.2.3, @radix-ui/react-popover@1.1.4, @radix-ui/react-progress@1.1.1, @radix-ui/react-radio-group@1.2.2, @radix-ui/react-scroll-area@1.2.2, @radix-ui/react-select@2.1.4, @radix-ui/react-separator@1.1.1, @radix-ui/react-slider@1.2.2, @radix-ui/react-slot@1.1.1, @radix-ui/react-switch@1.1.2, @radix-ui/react-tabs@1.1.2, @radix-ui/react-toast@1.2.4, @radix-ui/react-toggle@1.1.1, @radix-ui/react-toggle-group@1.1.1, @radix-ui/react-tooltip@1.1.6, @tailwindcss/postcss@^4.1.13, @types/node@^22, @types/react@19.2.7, @types/react-dom@19.2.3, autoprefixer@^10.4.20, class-variance-authority@^0.7.1, clsx@^2.1.1, cmdk@1.1.1, date-fns@4.1.0, embla-carousel-react@8.5.1, framer-motion@^12.34.0, input-otp@1.4.1, lucide-react@^0.544.0, motion@^12.34.0, next@16.1.6, next-themes@^0.4.6, postcss@^8.5, react@19.2.3, react-day-picker@8.10.1, react-dom@19.2.3, react-hook-form@^7.54.1, react-resizable-panels@^2.1.7, recharts@2.15.0, sonner@^1.7.1, tailwind-merge@^2.5.5, tailwindcss@^3.4.17, tailwindcss-animate@^1.0.7, typescript@5.7.3, vaul@^1.1.2, zod@^3.24.1
- server/Cargo.toml: anyhow@1.0, axum@0.7, chrono@0.4, dotenvy@0.15, once_cell@1.19, reqwest@0.11, serde@1.0, serde_json@1.0, thiserror@1.0, tokio@1.35, tracing@0.1, tracing-subscriber@0.3, uuid@1.6

### Recent commits (newest first)

- chore: docs and ai improvements
- feat: add agents and backend
- chore: added additional AI features
- chore: add interactive action button
- feat: push frontend

## Key source files (fetched from GitHub, selected and truncated for size)

### server/Cargo.toml

```
[package]
name = "gallop-server"
version = "0.1.0"
edition = "2021"

[dependencies]
axum = { version = "0.7", features = ["macros", "json"] }
tokio = { version = "1.35", features = ["macros", "rt-multi-thread", "io-std"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
once_cell = "1.19"
reqwest = { version = "0.11", features = ["json", "rustls-tls"] }
chrono = { version = "0.4", features = ["clock", "serde"] }
uuid = { version = "1.6", features = ["v4", "serde"] }
thiserror = "1.0"
anyhow = "1.0"
dotenvy = "0.15"

```

### agent/Cargo.toml

```
[package]
name = "gallop-agent"
version = "0.1.0"
edition = "2021"
build = "build.rs"

[dependencies]
tokio = { version = "1.35", features = ["macros", "rt-multi-thread", "time", "process"] }
reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
once_cell = "1.19"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
uuid = { version = "1.6", features = ["v4", "serde"] }
hostname = "0.3"
chrono = { version = "0.4", features = ["clock", "serde"] }
anyhow = "1.0"
inotify = "0.10"
regex = "1"


[dependencies.libbpf-rs]
version = "0.26"

[build-dependencies]
libbpf-cargo = { version = "0.26" }

```

### frontend/package.json

```
{
  "name": "my-project",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev --turbo",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@elastic/elasticsearch": "^9.3.1",
    "@hookform/resolvers": "^3.9.1",
    "@radix-ui/react-accordion": "1.2.2",
    "@radix-ui/react-alert-dialog": "1.1.4",
    "@radix-ui/react-aspect-ratio": "1.1.1",
    "@radix-ui/react-avatar": "1.1.2",
    "@radix-ui/react-checkbox": "1.1.3",
    "@radix-ui/react-collapsible": "1.1.2",
    "@radix-ui/react-context-menu": "2.2.4",
    "@radix-ui/react-dialog": "1.1.4",
    "@radix-ui/react-dropdown-menu": "2.1.4",
    "@radix-ui/react-hover-card": "1.1.4",
    "@radix-ui/react-label": "2.1.1",
    "@radix-ui/react-menubar": "1.1.4",
    "@radix-ui/react-navigation-menu": "1.2.3",
    "@radix-ui/react-popover": "1.1.4",
    "@radix-ui/react-progress": "1.1.1",
    "@radix-ui/react-radio-group": "1.2.2",
    "@radix-ui/react-scroll-area": "1.2.2",
    "@radix-ui/react-select": "2.1.4",
    "@radix-ui/react-separator": "1.1.1",
    "@radix-ui/react-slider": "1.2.2",
    "@radix-ui/react-slot": "1.1.1",
    "@radix-ui/react-switch": "1.1.2",
    "@radix-ui/react-tabs": "1.1.2",
    "@radix-ui/react-toast": "1.2.4",
    "@radix-ui/react-toggle": "1.1.1",
    "@radix-ui/react-toggle-group": "1.1.1",
    "@radix-ui/react-tooltip": "1.1.6",
    "autoprefixer": "^10.4.20",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "cmdk": "1.1.1",
    "date-fns": "4.1.0",
    "embla-carousel-react": "8.5.1",
    "framer-motion": "^12.34.0",
    "input-otp": "1.4.1",
    "lucide-react": "^0.544.0",
    "motion": "^12.34.0",
    "next": "16.1.6",
    "next-themes": "^0.4.6",
    "react": "19.2.3",
    "react-day-picker": "8.10.1",
    "react-dom": "19.2.3",
    "react-hook-form": "^7.54.1",
    "react-resizable-panels": "^2.1.7",
    "recharts": "2.15.0",
    "sonner": "^1.7.1",
    "tailwind-merge": "^2.5.5",
    "tailwindcss-animate": "^1.0.7",
    "vaul": "^1.1.2",
    "zod": "^3.24.1"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4.1.13",
    "@types/node": "^22",
    "@types/react": "19.2.7",
    "@types/react-dom": "19.2.3",
    "postcss": "^8.5",
    "tailwindcss": "^3.4.17",
    "typescript": "5.7.3"
  },
  "pnpm": {
    "overrides": {
      "@types/react": "19.2.7",
      "@types/react-dom": "19.2.3"
    }
  }
}

```

### frontend/app/page.tsx

```typescript
import { Navbar } from "@/components/landing/navbar"
import { HeroScrollSection } from "@/components/landing/hero-scroll"
import { Features } from "@/components/landing/features"
import { CTASection, Footer } from "@/components/landing/cta-footer"

export default function Page() {
  return (
    <div className="min-h-screen bg-transparent">
      <Navbar />
      <main>
        <HeroScrollSection />
        <Features />
        <CTASection />
      </main>
      <Footer />
    </div>
  )
}

```

### frontend/app/layout.tsx

```typescript
import type { Metadata, Viewport } from 'next'
import { Geist, Geist_Mono } from 'next/font/google'
import { AuthProvider } from '@/lib/auth-context'
import { ThemeProvider } from '@/components/theme-provider'
import { ThemeToggle } from '@/components/theme-toggle'

import './globals.css'

const geistSans = Geist({ subsets: ['latin'], variable: '--font-geist-sans' })
const geistMono = Geist_Mono({
  subsets: ['latin'],
  variable: '--font-geist-mono',
})

export const metadata: Metadata = {
  title: 'Gallop - AI-Powered Cybersecurity for Linux',
  description:
    'Protect your Linux systems with AI-powered threat detection. Real-time monitoring, intelligent alerts, and centralized security management powered by Elasticsearch and Jina AI.',
}

export const viewport: Viewport = {
  themeColor: '#8b5e34',
}

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode
}>) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body className={`${geistSans.variable} ${geistMono.variable} font-sans antialiased`}>
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
          disableTransitionOnChange
        >
          <AuthProvider>
            <ThemeToggle />
            {children}
          </AuthProvider>
        </ThemeProvider>
      </body>
    </html>
  )
}

```

### server/src/main.rs

```rust
use anyhow::Result;
use axum::{
    extract::State,
    http::StatusCode,
    routing::{get, post},
    Json, Router,
};
use dotenvy::from_filename;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::io::{stdin, stdout, Write};
use std::{collections::VecDeque, net::SocketAddr, sync::Arc};
use tokio::io::{stdin as tokio_stdin, AsyncBufReadExt, BufReader};
use tokio::net::TcpListener;
use tokio::sync::Mutex;
use tracing::{error, info, warn};

static ELASTIC_URL: Lazy<Option<String>> = Lazy::new(|| std::env::var("ELASTIC_URL").ok());
static ELASTIC_API_KEY: Lazy<Option<String>> = Lazy::new(|| std::env::var("ELASTIC_API_KEY").ok());
static PROCESS_INDEX: Lazy<String> = Lazy::new(|| {
    std::env::var("PROCESS_INDEX").unwrap_or_else(|_| "logs-processstart".to_string())
});
static OPENAI_API_KEY: Lazy<Option<String>> = Lazy::new(|| std::env::var("OPENAI_API_KEY").ok());
static OPENAI_BASE: Lazy<String> = Lazy::new(|| {
    std::env::var("OPENAI_BASE").unwrap_or_else(|_| "https://api.openai.com/v1".to_string())
});
static OPENAI_MODEL: Lazy<String> =
    Lazy::new(|| std::env::var("OPENAI_MODEL").unwrap_or_else(|_| "gpt-4o-mini".to_string()));

#[derive(Clone, Default)]
struct AppState {
    commands: Arc<Mutex<VecDeque<AgentCommand>>>,
    agents: Arc<Mutex<AgentSet>>,
}

#[derive(Default)]
struct AgentSet {
    ids: Vec<String>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
struct ProcessDoc {
    #[serde(rename = "@timestamp")]
    timestamp: String,
    host: Host,
    process: Process,
    event: Event,
    user: User,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
struct Host {
    id: String,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
struct Process {
    pid: i32,
    ppid: i32,
    #[serde(default)]
    tid: i32,
    #[serde(default)]
    pid_ns: i32,
    #[serde(default)]
    ppid_ns: i32,
    #[serde(default)]
    pid_host: i32,
    #[serde(default)]
    ppid_host: i32,
    name: String,
    command_line: String,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
struct Event {
    kind: String,
    category: Vec<String>,
    action: String,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
struct User {
    id: u32,
    gid: u32,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
struct AgentCommand {
    id: uuid::Uuid,
    agent_id: String,
    instruction: String,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
struct AgentCommandResponse {
    id: uuid::Uuid,
    agent_id: String,
    instruction: String,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
struct AgentResult {
    id: uuid::Uuid,
    agent_id: String,
    output: String,
    status: String,
}

#[derive(Debug, Serialize, Deserialize)]
struct OpenAIChatRequest {
    messages: serde_json::Value,
    #[serde(default = "default_model")]
    model: String,
}

#[derive(Debug, Serialize, Deserialize)]
struct OpenAIChatResponse {
    response: serde_json::Value,
}

fn default_model() -> String {
    OPENAI_MODEL.clone()
}

#[tokio::main]
async fn main() -> Result<()> {
    tracing_subscriber::fmt()
        .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
        .init();

    let _ = from_filename(".env");

    let state = AppState::default();
    let cli_state = state.clone();
    let app = Router::new()
        .route("/health", get(|| async { StatusCode::OK }))
        .route("/v1/ingest/processes", post(ingest_processes))
        .route("/v1/agent/commands", get(next_command))
        .route("/v1/agent/commands", post(enqueue_command))
        .route("/v1/agent/list", get(list_agents))
        .route("/v1/agent/result", post(agent_result))
        .route("/v1/openai/chat", post(openai_chat))
        .with_state(state);

    let addr = SocketAddr::from(([0, 0, 0, 0], 8080));
    info!("server listening on {addr}");
    let listener = TcpListener::bind(addr).await?;

    // CLI task for interactive commands
    let cli = tokio::spawn(async move {
        let stdin = BufReader::new(tokio_stdin());
        let mut lines = stdin.lines();
        loop {
            eprint!("cmd> ");
            let _ = stdout().flush();
            if let Ok(Some(line)) = lines.next_line().await {
                let cmd = line.trim();
                if cmd.is_empty() {
                    continue;
                }
                if cmd == "list" {
                    let agents = cli_state.agents.lock().await;
                    eprintln!("Agents: {:?}", agents.ids);
                    continue;
                }
                let mut parts = cmd.splitn(2, ' ');
                let agent = parts.next().unwrap_or("").trim();
                let instr = parts.next().unwrap_or("").trim();
                if agent.is_empty() || instr.is_empty() {
                    eprintln!("Usage: <agent_id> <instruction>  |  list");
                    continue;
                }
                {
                    let mut agents = cli_state.agents.lock().await;
                    if !agents.ids.contains(&agent.to_string()) {
                        agents.ids.push(agent.to_string());
                    }
                }
                let mut queue = cli_state.commands.lock().await;
                let cmd_obj = AgentCommand {
                    id: uuid::Uuid::new_v4(),
                    agent_id: agent.to_string(),
                    instruction: instr.to_string(),
                };
                queue.push_back(cmd_obj.clone());
                info!(
                    "queued command for agent={} instruction=\"{}\"",
                    agent, instr
                );
            } else {
                break;
            }
        }
    });

    let server = axum::serve(listener, app);
    let _ = tokio::join!(server, cli);
    Ok(())
}

async fn ingest_processes(
    State(state): State<AppState>,
    Json(payload): Json<Vec<ProcessDoc>>,
) -> Result<StatusCode, (StatusCode, String)> {
    info!("ingest received {} docs", payload.len());
    if payload.is_empty()
[truncated — 5280 more characters]
```

### agent/src/main.rs

```rust
use anyhow::Result;
use libbpf_rs::RingBufferBuilder;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::env;
use std::fs;
use std::os::raw::c_char;
use std::sync::Mutex;
use std::time::Instant;
use tokio::process::Command as TokioCommand;
use tokio::sync::mpsc;
use tokio::time::timeout;
use tokio::time::{sleep, Duration};
use tracing::{error, info};

mod bpf_skel;
use bpf_skel::ExecsnoopSkelBuilder;
use libbpf_rs::skel::OpenSkel;
use libbpf_rs::skel::Skel;
use libbpf_rs::skel::SkelBuilder;

static SERVER_URL: Lazy<String> = Lazy::new(|| {
    env::var("SERVER_URL").unwrap_or_else(|_| "https://api.joingallop.com".to_string())
});
static HOST_ID: Lazy<String> = Lazy::new(|| {
    hostname::get()
        .unwrap_or_default()
        .to_string_lossy()
        .to_string()
});
static MODEL: Lazy<String> =
    Lazy::new(|| env::var("OPENAI_MODEL").unwrap_or_else(|_| "gpt-4o-mini".to_string()));
static SUPPRESS_UNTIL: Lazy<Mutex<Option<Instant>>> = Lazy::new(|| Mutex::new(None));

#[derive(Serialize, Clone, Debug)]
struct ProcessDoc {
    #[serde(rename = "@timestamp")]
    timestamp: String,
    host: Host,
    process: Process,
    event: Event,
    user: User,
}

#[derive(Serialize, Clone, Debug)]
struct Host {
    id: String,
}

#[derive(Serialize, Clone, Debug)]
struct Process {
    pid: i32,  // host pid (killable on host)
    ppid: i32, // host ppid
    tid: i32,
    pid_ns: i32,
    ppid_ns: i32,
    pid_host: i32,
    ppid_host: i32,
    name: String,
    command_line: String,
}

#[derive(Serialize, Clone, Debug)]
struct Event {
    kind: String,
    category: Vec<String>,
    action: String,
}

#[derive(Serialize, Clone, Debug)]
struct User {
    id: u32,
    gid: u32,
}

#[derive(Debug, Deserialize)]
struct AgentCommand {
    id: uuid::Uuid,
    agent_id: String,
    instruction: String,
}

#[derive(Debug, Serialize)]
struct AgentResultPayload {
    id: uuid::Uuid,
    agent_id: String,
    output: String,
    status: String,
}

async fn bulk_ingest(docs: &[ProcessDoc]) -> Result<()> {
    if docs.is_empty() {
        return Ok(());
    }
    let client = reqwest::Client::new();
    let mut req = client
        .post(format!("{}/v1/ingest/processes", *SERVER_URL))
        .json(&docs);
    let resp = req.send().await?;
    let status = resp.status();
    if !status.is_success() {
        let text = resp.text().await.unwrap_or_default();
        anyhow::bail!("bulk ingest failed: {} {}", status, text);
    }
    Ok(())
}

async fn process_ingest_loop(mut rx: mpsc::Receiver<ProcessDoc>) {
    let mut batch: Vec<ProcessDoc> = Vec::with_capacity(64);
    let mut last_flush = tokio::time::Instant::now();
    loop {
        let timeout = tokio::time::sleep(Duration::from_millis(200));
        tokio::pin!(timeout);
        tokio::select! {
            maybe_doc = rx.recv() => {
                if let Some(doc) = maybe_doc {
                    info!("process event pid={} name={} cmdline=\"{}\" ppid={} uid={} gid={}",
                        doc.process.pid,
                        doc.process.name,
                        doc.process.command_line,
                        doc.process.ppid,
                        doc.user.id,
                        doc.user.gid,
                    );
                    info!("host_pid={} host_ppid={} ns_pid={} ns_ppid={}",
                        doc.process.pid_host,
                        doc.process.ppid_host,
                        doc.process.pid,
                        doc.process.ppid,
                    );
                    batch.push(doc);
                    if batch.len() >= 50 {
                        if let Err(e) = bulk_ingest(&batch).await {
                            error!("bulk ingest error: {e:?}");
                        }
                        batch.clear();
                        last_flush = tokio::time::Instant::now();
                    }
                    continue;
                } else {
                    // channel closed: flush remaining and exit
                    if let Err(e) = bulk_ingest(&batch).await {
                        error!("bulk ingest error on shutdown: {e:?}");
                    }
                    break;
                }
            }
            _ = &mut timeout => {
                if !batch.is_empty() && last_flush.elapsed() >= Duration::from_millis(200) {
                    if let Err(e) = bulk_ingest(&batch).await {
                        error!("bulk ingest error: {e:?}");
                    }
                    batch.clear();
                    last_flush = tokio::time::Instant::now();
                }
            }
        }
    }
}

async fn codex_connector() {
    loop {
        info!("codex connector heartbeat");
        sleep(Duration::from_secs(15)).await;
    }
}

async fn command_loop() {
    let mut running = true;
    while running {
        match fetch_commands().await {
            Ok(cmds) => {
                for cmd in cmds.into_iter().filter(|c| c.agent_id == *HOST_ID) {
                    info!(
                        "command received id={} agent={} instr=\"{}\"",
                        cmd.id, cmd.agent_id, cmd.instruction
                    );
                    let output = process_instruction(&cmd.instruction).await;
                    if let Err(e) = send_result(&cmd.id, &cmd.agent_id, &output).await {
                        error!("send result error: {e:?}");
                    }
                    if should_terminate(&cmd.instruction) {
                        info!("termination instruction received; stopping command loop");
                        running = false;
                        break;
                    }
                }
            }
            Err(e) => {
                error!("command fetch error: {e:?}");
            }
        }
        if running {
            sleep(Duration::from_secs(5)).await;
        }
    }
}

async fn process_instruction(instr: &str) -> String {
    info!("processing inst
[truncated — 10480 more characters]
```

### frontend/app/dashboard/layout.tsx

```typescript
"use client"

import { DashboardSidebar } from "@/components/dashboard/sidebar"

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div className="flex min-h-screen bg-background">
      <DashboardSidebar />
      <main className="flex-1 overflow-auto pt-14 lg:pt-0">
        <div className="mx-auto max-w-7xl p-4 md:p-6 lg:p-8">{children}</div>
      </main>
    </div>
  )
}

```

### frontend/app/dashboard/page.tsx

```typescript
"use client"

import { useEffect, useMemo, useState } from "react"
import { Server, ScanSearch, Database } from "lucide-react"
import { StatCard } from "@/components/dashboard/stat-card"
import { ActiveThreatsCard } from "@/components/dashboard/active-threats-card"
import { HealthScoreGaugeCard } from "@/components/dashboard/health-score-gauge-card"
import { ThreatChart } from "@/components/dashboard/threat-chart"
import { ThreatBreakdownChart } from "@/components/dashboard/threat-breakdown-chart"
import { RecentAlertsTable } from "@/components/dashboard/recent-alerts-table"
import { AgentCard } from "@/components/dashboard/agent-card"
import type { AgentSummary, OverviewStats, ThreatAlert } from "@/lib/types/security"

const EMPTY_OVERVIEW: OverviewStats = {
  totalAgents: 0,
  onlineAgents: 0,
  activeThreats: 0,
  scansToday: 0,
  healthScore: 100,
}

export default function DashboardPage() {
  const [overview, setOverview] = useState<OverviewStats>(EMPTY_OVERVIEW)
  const [alerts, setAlerts] = useState<ThreatAlert[]>([])

  useEffect(() => {
    let cancelled = false

    async function load() {
      const [overviewRes, alertsRes] = await Promise.all([
        fetch("/api/dashboard/overview", { cache: "no-store" }),
        fetch("/api/dashboard/threats?limit=200", { cache: "no-store" }),
      ])

      if (!overviewRes.ok || !alertsRes.ok || cancelled) return

      const overviewJson = (await overviewRes.json()) as OverviewStats
      const alertsJson = (await alertsRes.json()) as { alerts?: ThreatAlert[] }

      if (!cancelled) {
        setOverview({
          totalAgents: overviewJson.totalAgents ?? 0,
          onlineAgents: overviewJson.onlineAgents ?? 0,
          activeThreats: overviewJson.activeThreats ?? 0,
          scansToday: overviewJson.scansToday ?? 0,
          healthScore: overviewJson.healthScore ?? 100,
        })
        setAlerts(alertsJson.alerts ?? [])
      }
    }

    load()
    const timer = window.setInterval(load, 15000)
    return () => {
      cancelled = true
      window.clearInterval(timer)
    }
  }, [])

  const agents = useMemo<AgentSummary[]>(() => {
    const map = new Map<string, AgentSummary>()
    for (const alert of alerts) {
      const existing = map.get(alert.agentId)
      if (!existing) {
        map.set(alert.agentId, {
          id: alert.agentId,
          hostname: alert.agentName,
          status: "offline",
          lastSeen: alert.timestamp,
          threatCount: 1,
        })
        continue
      }

      existing.threatCount += 1
      if (new Date(alert.timestamp).getTime() > new Date(existing.lastSeen).getTime()) {
        existing.lastSeen = alert.timestamp
      }
    }

    const now = Date.now()
    for (const agent of map.values()) {
      const age = now - new Date(agent.lastSeen).getTime()
      agent.status = age <= 5 * 60 * 1000 ? "online" : "offline"
    }

    return [...map.values()]
      .sort((a, b) => b.threatCount - a.threatCount)
      .slice(0, 6)
  }, [alerts])

  return (
    <div className="flex flex-col gap-6">
      <div>
        <div className="flex flex-wrap items-center gap-3">
          <h1 className="text-2xl font-bold text-foreground">Security Overview</h1>
          <div className="inline-flex items-center gap-2 rounded-full border border-emerald-500/30 bg-emerald-500/10 px-2.5 py-1 text-xs text-emerald-300">
            <span className="relative flex h-2 w-2">
              <span className="absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75 animate-ping" />
              <span className="relative inline-flex h-2 w-2 rounded-full bg-emerald-400" />
            </span>
            <Database className="h-3.5 w-3.5" />
            Elasticsearch Live
          </div>
        </div>
        <p className="text-sm text-muted-foreground">
          Real-time malicious telemetry across your infrastructure.
        </p>
      </div>

      <div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
        <StatCard
          title="Total Agents"
          value={overview.totalAgents}
          subtitle={`${overview.onlineAgents} online`}
          icon={Server}
          iconColor="text-primary"
        />
        <ActiveThreatsCard count={overview.activeThreats} />
        <StatCard
          title="Detections Today"
          value={overview.scansToday}
          subtitle="Malicious events"
          icon={ScanSearch}
          iconColor="text-warning"
        />
        <HealthScoreGaugeCard score={overview.healthScore} trend={0} />
      </div>

      <div className="grid gap-6 xl:grid-cols-2">
        <ThreatChart />
        <ThreatBreakdownChart />
      </div>

      <RecentAlertsTable />

      <div>
        <h2 className="mb-4 text-lg font-semibold text-foreground">
          Agent Status
        </h2>
        <div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
          {agents.map((agent) => (
            <AgentCard key={agent.id} agent={agent} />
          ))}
        </div>
      </div>
    </div>
  )
}


```

### frontend/app/dashboard/agents/page.tsx

```typescript
"use client"

import Link from "next/link"
import { useEffect, useState } from "react"
import { AgentCard } from "@/components/dashboard/agent-card"
import { GlowButton } from "@/components/ui/glow-button"
import { Download, Server } from "lucide-react"
import type { AgentSummary } from "@/lib/types/security"

export default function AgentsPage() {
  const [agents, setAgents] = useState<AgentSummary[]>([])
  const [totalEvents, setTotalEvents] = useState(0)

  useEffect(() => {
    let cancelled = false

    async function load() {
      const res = await fetch("/api/dashboard/agents?limit=200", { cache: "no-store" })
      if (!res.ok || cancelled) return
      const json = (await res.json()) as {
        agents?: AgentSummary[]
        totalEvents?: number
      }
      if (!cancelled) {
        setAgents(json.agents ?? [])
        setTotalEvents(json.totalEvents ?? 0)
      }
    }

    load()
    const timer = window.setInterval(load, 15000)
    return () => {
      cancelled = true
      window.clearInterval(timer)
    }
  }, [])

  const sortedAgents = [...agents].sort((a, b) => b.threatCount - a.threatCount)

  const online = sortedAgents.filter((a) => a.status === "online").length
  const offline = sortedAgents.filter((a) => a.status === "offline").length

  return (
    <div className="flex flex-col gap-6">
      <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
        <div>
          <h1 className="text-2xl font-bold text-foreground">Agents</h1>
          <p className="text-sm text-muted-foreground">
            {sortedAgents.length} agents seen in malicious telemetry &middot;{" "}
            <span className="text-muted-foreground">{totalEvents} malicious events</span> &middot;{" "}
            <span className="text-success">{online} online</span> &middot;{" "}
            <span className="text-muted-foreground">{offline} offline</span>
          </p>
        </div>
        <GlowButton asChild>
          <Link href="/dashboard/download">
            <Download className="h-4 w-4" />
            Install New Agent
          </Link>
        </GlowButton>
      </div>

      {sortedAgents.length === 0 ? (
        <div className="flex flex-col items-center gap-4 rounded-lg border border-dashed border-border py-16">
          <Server className="h-12 w-12 text-muted-foreground" />
          <div className="text-center">
            <p className="text-lg font-medium text-foreground">
              No malicious agent events yet
            </p>
            <p className="text-sm text-muted-foreground">
              Once malicious process starts are detected, agents will appear here.
            </p>
          </div>
        </div>
      ) : (
        <div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
          {sortedAgents.map((agent) => (
            <AgentCard key={agent.id} agent={agent} />
          ))}
        </div>
      )}
    </div>
  )
}

```

[98 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]