Skip to content
Draft
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ node_modules/
.wrangler/
.hermes/
*.log
.env
.env.*
38 changes: 38 additions & 0 deletions the-unit/api/src/allowlist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { NextFunction, Response } from "express";
import type { AuthedRequest } from "./auth.js";
import { logger } from "./logger.js";

const ALLOWED_EMAILS_RAW = process.env.ALLOWED_EMAILS ?? "";

const allowedEmails: Set<string> = new Set(
ALLOWED_EMAILS_RAW.split(",")
.map((e) => e.trim().toLowerCase())
.filter(Boolean),
);

/**
* Rejects requests from users whose email is not in the allowlist.
* When ALLOWED_EMAILS is unset or empty, ALL authenticated users are blocked
* (fail-closed) to prevent open proxy abuse.
*/
export function enforceAllowlist(
req: AuthedRequest,
res: Response,
next: NextFunction,
): void {
const email = req.email?.toLowerCase() ?? "";

if (allowedEmails.size === 0) {
logger.warn({ uid: req.uid, email }, "allowlist_empty_all_blocked");
res.status(403).json({ error: "service_not_configured" });
return;
}

if (!allowedEmails.has(email)) {
logger.warn({ uid: req.uid, email }, "user_not_in_allowlist");
res.status(403).json({ error: "not_authorized" });
return;
}

next();
}
9 changes: 5 additions & 4 deletions the-unit/api/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import express from "express";
import { pinoHttp } from "pino-http";
import { requireFirebaseAuth } from "./auth.js";
import { enforceAllowlist } from "./allowlist.js";
import { enforceDailyCap } from "./rateLimit.js";
import { logger } from "./logger.js";
import { anthropicRouter } from "./providers/anthropic.js";
Expand All @@ -18,10 +19,10 @@
res.json({ ok: true, service: "gumption-api" });
});

app.use("/v1/anthropic", requireFirebaseAuth, enforceDailyCap, anthropicRouter);
app.use("/v1/openai", requireFirebaseAuth, enforceDailyCap, openaiRouter);
app.use("/v1/xai", requireFirebaseAuth, enforceDailyCap, xaiRouter);
app.use("/v1/gemini", requireFirebaseAuth, enforceDailyCap, geminiRouter);
app.use("/v1/anthropic", requireFirebaseAuth, enforceAllowlist, enforceDailyCap, anthropicRouter);

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.
app.use("/v1/openai", requireFirebaseAuth, enforceAllowlist, enforceDailyCap, openaiRouter);

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.
app.use("/v1/xai", requireFirebaseAuth, enforceAllowlist, enforceDailyCap, xaiRouter);

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.
app.use("/v1/gemini", requireFirebaseAuth, enforceAllowlist, enforceDailyCap, geminiRouter);

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.

app.use((_req, res) => {
res.status(404).json({ error: "not_found" });
Expand Down
3 changes: 2 additions & 1 deletion the-unit/scripts/deploy-gcp.sh
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ for SECRET in anthropic-key:ANTHROPIC_API_KEY openai-key:OPENAI_API_KEY xai-key:
done
SECRETS_ARG="${SECRETS_ARG%,}"

ENV_VARS="GCP_PROJECT=${PROJECT_ID},GCP_REGION=${REGION},DAILY_TOKEN_CAP=${DAILY_TOKEN_CAP}"
ALLOWED_EMAILS="${ALLOWED_EMAILS:-bryan@norcalcarbmobile.com}"
ENV_VARS="GCP_PROJECT=${PROJECT_ID},GCP_REGION=${REGION},DAILY_TOKEN_CAP=${DAILY_TOKEN_CAP},ALLOWED_EMAILS=${ALLOWED_EMAILS}"

CMD=(
gcloud run deploy "$SERVICE"
Expand Down
45 changes: 44 additions & 1 deletion workers/silverback-ai-studio/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import { WebSocketServer, WebSocket } from "ws";
import { createServer as createViteServer } from "vite";
import path from "path";
import { fileURLToPath } from "url";
import { GoogleGenAI } from "@google/genai";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const ALERT_SECRET = process.env.ALERT_SECRET || "";

async function startServer() {
const app = express();
const server = createServer(app);
Expand Down Expand Up @@ -39,8 +42,48 @@ async function startServer() {
});
};

// API to trigger high-severity alerts (for testing/demo)
// Server-side AI analysis — keeps GEMINI_API_KEY off the client
app.post("/api/analyze", async (req, res) => {
const apiKey = process.env.GEMINI_API_KEY;
if (!apiKey) {
res.status(503).json({ error: "gemini_key_not_configured" });
return;
}

const { eventType, description, severity } = req.body;
if (!eventType || !description) {
res.status(400).json({ error: "missing_fields" });
return;
}

try {
const ai = new GoogleGenAI({ apiKey });
const response = await ai.models.generateContent({
model: "gemini-3-flash-preview",
contents: `Analyze this security event clip description and provide a more detailed, professional security assessment.
Event Type: ${eventType}
Initial Description: ${description}
Severity: ${severity || "unknown"}

Provide a detailed breakdown of what might be happening, potential risks, and recommended actions. Keep it concise but professional.`,
});

const analysis = response.text || "No analysis available.";
res.json({ analysis });
} catch (err) {
console.error("AI analysis failed:", err);
res.status(500).json({ error: "analysis_failed" });
}
});

// API to trigger high-severity alerts — requires shared secret
app.post("/api/trigger-alert", (req, res) => {
const authHeader = req.header("x-alert-secret") || "";
if (!ALERT_SECRET || authHeader !== ALERT_SECRET) {
res.status(401).json({ error: "unauthorized" });
return;
}

const { type, description, severity, location } = req.body;

if (severity === 'high' || severity === 'critical') {
Expand Down
25 changes: 14 additions & 11 deletions workers/silverback-ai-studio/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -540,21 +540,24 @@ export default function App() {
setIsAnalyzing(event.id);

try {
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
const response = await ai.models.generateContent({
model: "gemini-3-flash-preview",
contents: `Analyze this security event clip description and provide a more detailed, professional security assessment.
Event Type: ${event.type}
Initial Description: ${event.description}
Severity: ${event.severity}

Provide a detailed breakdown of what might be happening, potential risks, and recommended actions. Keep it concise but professional.`,
const res = await fetch("/api/analyze", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
eventType: event.type,
description: event.description,
severity: event.severity,
}),
});

const analysis = response.text || "No analysis available.";
if (!res.ok) {
throw new Error(`Analysis request failed: ${res.status}`);
}

const { analysis } = await res.json();

const eventRef = doc(db, 'event_logs', event.id);
await updateDoc(eventRef, { aiAnalysis: analysis });
await updateDoc(eventRef, { aiAnalysis: analysis || "No analysis available." });
} catch (error) {
console.error("AI Analysis failed:", error);
handleFirestoreError(error, OperationType.UPDATE, `event_logs/${event.id}`);
Expand Down
8 changes: 2 additions & 6 deletions workers/silverback-ai-studio/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
import path from 'path';
import {defineConfig, loadEnv} from 'vite';
import {defineConfig} from 'vite';

export default defineConfig(({mode}) => {
const env = loadEnv(mode, '.', '');
export default defineConfig(() => {
return {
plugins: [react(), tailwindcss()],
define: {
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY),
},
resolve: {
alias: {
'@': path.resolve(__dirname, '.'),
Expand Down