diff --git a/.env.local.example b/.env.local.example index 2e38651..db34e85 100644 --- a/.env.local.example +++ b/.env.local.example @@ -3,6 +3,8 @@ JIRA_HOST=yourorg.atlassian.net JIRA_EMAIL=service-account@yourorg.com JIRA_API_TOKEN=your-jira-api-token JIRA_SECOPS_PROJECT_KEY=SECOPS +# Optional full JQL override. The default finds open issues whose summary contains "User-reported phishing". +# JIRA_PHISHING_JQL=project = "SECOPS" AND labels = phishing-intake AND statusCategory != Done ORDER BY created DESC # Google Workspace Configuration GOOGLE_CLIENT_EMAIL=service-account@your-project.iam.gserviceaccount.com @@ -17,8 +19,14 @@ VIRUSTOTAL_API_KEY=your-virustotal-api-key OPENROUTER_API_KEY=your-openrouter-api-key OPENROUTER_MODEL=anthropic/claude-3.5-sonnet -# Authentication (Future Use) -NEXTAUTH_SECRET=your-nextauth-secret +# SecureCatch access control +# Generate the password hash with: printf '%s' 'a-long-password' | npm run auth:hash-password +SECURECATCH_ADMIN_PASSWORD_HASH=scrypt$16384$8$1$replace-with-salt$replace-with-hash +# Generate a random secret with: openssl rand -base64 48 +SECURECATCH_SESSION_SECRET=replace-with-at-least-32-random-characters + +# Safety gate. Keep false until authentication is configured and a dry run has been reviewed. +SECURECATCH_ENABLE_DESTRUCTIVE_ACTIONS=false # Database DATABASE_URL=file:./dev.db diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c2cc175 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: CI + +on: + push: + branches: [securecatch-base, main] + pull_request: + branches: [securecatch-base, main] + +jobs: + quality: + runs-on: ubuntu-latest + permissions: + contents: read + env: + DATABASE_URL: file:./dev.db + SECURECATCH_SESSION_SECRET: ci-only-session-secret-with-at-least-32-characters + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm run lint + - run: npm run typecheck + - run: npm test + - run: npm run build diff --git a/AGENTS.md b/AGENTS.md index 8bd0e39..643577d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,9 @@ + # This is NOT the Next.js you know -This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices. +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. + +This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. + diff --git a/README.md b/README.md index e215bc4..b807a3e 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,109 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# SecureCatch -## Getting Started +SecureCatch is a human-in-the-loop phishing triage console for Google Workspace and Jira. It gathers reported-email evidence, enriches indicators with VirusTotal, asks a configured OpenRouter model for a structured classification, and presents the result for analyst review. -First, run the development server: +Remediation is deliberately gated. A signed administrator session is required for every page and API route, state transitions are claimed atomically, domain-wide deletion is disabled by default, and an analyst must type the Jira ticket key before a purge can begin. + +## What it does + +1. Ingests open phishing reports from a Jira project. +2. Resolves the corresponding message through Google Workspace Alert Center and Gmail. +3. Extracts headers, message text, sender data, and links. +4. Enriches domains, IPs, and URLs with VirusTotal. +5. Produces a validated `Phishing`, `Spam`, or `Safe` assessment through OpenRouter. +6. Pauses in `AWAITING_REVIEW` for an analyst decision. +7. Optionally trashes the matching RFC 2822 message across Workspace accounts and updates Jira. + +The model never acts on its own. Its output is advisory, must match a strict JSON schema, and cannot bypass the review and confirmation controls. + +## Safety model + +- **Private operator access:** an administrator password creates an eight-hour, HMAC-signed, `HttpOnly`, `SameSite=Strict` session. +- **Protected APIs:** route-level authorization remains in place even if the Next.js proxy is changed accidentally. +- **CSRF resistance:** state-changing endpoints reject requests without a same-origin signal. +- **Fail-closed remediation:** `SECURECATCH_ENABLE_DESTRUCTIVE_ACTIONS` defaults to `false`. +- **Explicit confirmation:** a domain-wide purge requires the exact Jira ticket key. +- **Single-action claims:** an atomic database transition prevents two operators from starting the same action. +- **Untrusted evidence boundaries:** email content and threat-intelligence fields are marked as untrusted in the model prompt. +- **Safe parse failures:** malformed model output is rejected instead of being stored as analysis. + +SecureCatch is designed as an internal, single-administrator tool. Put deployed instances behind a private network or identity-aware access proxy as an additional boundary. Review Google Workspace delegation scopes before enabling remediation. + +## Stack + +- Next.js 16 and React 19 +- TypeScript and Tailwind CSS +- Prisma with SQLite for a local or single-instance deployment +- Google Workspace APIs: Gmail, Alert Center, and Admin SDK +- Jira REST API +- VirusTotal API +- OpenRouter-compatible chat completions + +## Quick start + +Prerequisites: Node.js 20 or newer; npm; and credentials for the integrations you plan to use. + +```bash +git clone https://github.com/cjcsecurity/SecureCatch.git +cd SecureCatch +npm ci +cp .env.local.example .env.local +``` + +Generate the two access-control values and place them in `.env.local`: + +```bash +printf '%s' 'choose-a-long-unique-password' | npm run auth:hash-password +openssl rand -base64 48 +``` + +Then initialize and run the app: ```bash +npm run db:migrate npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +Open [http://localhost:3000](http://localhost:3000) and sign in. Keep `SECURECATCH_ENABLE_DESTRUCTIVE_ACTIONS=false` while validating ingestion and analysis with your environment. -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +The complete Google Workspace, Jira, and API configuration is in [SETUP.md](SETUP.md). -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +## Development checks -## Learn More +```bash +npm run lint +npm run typecheck +npm test +npm run build +``` + +`npm run check` runs all four. Pull requests run the same checks in GitHub Actions. -To learn more about Next.js, take a look at the following resources: +The tests cover signed-session validation, remediation policy gates, and strict model-response parsing. External services are not contacted during the test suite. + +## Workflow states + +```text +PENDING -> INGESTED -> ANALYSIS_RUNNING -> AWAITING_REVIEW + | | + ANALYSIS_FAILED +------------------+ + | | + CLOSING REMEDIATING + | | + CLOSED REMEDIATED + \ / + +-- ACTION_FAILED +``` -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +An `ACTION_FAILED` result means the operator should inspect server logs and verify external-system state before retrying or taking manual action. A failure may occur after an external API has completed part of a multi-step operation. -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +## Operational limitations -## Deploy on Vercel +- SQLite is appropriate for local use or one application instance with persistent storage. A multi-instance deployment needs a shared database and a reviewed Prisma migration strategy. +- Domain-wide remediation uses delegated Gmail access and performs one mailbox operation at a time. Test with a constrained Workspace environment first. +- Jira workflow transition IDs vary by project. Confirm the configured transition behavior before using it against production tickets. +- OpenRouter and VirusTotal receive the evidence sent by their respective integration code. Apply your organization’s data-handling rules before connecting real mail. -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +## Security reports -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +Please avoid posting exploitable details in a public issue. Use the repository’s private vulnerability-reporting option under the **Security** tab when available. diff --git a/SETUP.md b/SETUP.md index 413c320..ed69185 100644 --- a/SETUP.md +++ b/SETUP.md @@ -1,237 +1,219 @@ -# SecureCatch — Setup Guide +# SecureCatch setup -## Prerequisites +This guide configures a local or single-instance SecureCatch deployment. Complete the access-control setup before adding third-party credentials. -| Requirement | Version | -|-------------|---------| -| Node.js | v18+ (v20+ recommended) | -| npm | v9+ | -| Google Workspace | Admin with Domain-Wide Delegation access | -| Jira | Atlassian Cloud or Server with API token | -| VirusTotal | API key (free tier works) | -| OpenRouter | API key + model access | +## Prerequisites ---- +| Requirement | Supported configuration | +| --- | --- | +| Node.js | 20 or newer; use an active LTS release for deployment | +| npm | Version bundled with the selected Node release | +| Google Workspace | Administrator able to configure domain-wide delegation | +| Jira | Atlassian account with API-token access to the target project | +| VirusTotal | API key; the free tier works for low-volume testing | +| OpenRouter | API key and access to the selected model | -## 1. Clone & Install Dependencies +## 1. Install ```bash -git clone +git clone https://github.com/cjcsecurity/SecureCatch.git cd SecureCatch -npm install +npm ci +cp .env.local.example .env.local ``` ---- +Use `npm ci` to install the reviewed lockfile exactly. -## 2. Configure Environment Variables +## 2. Configure operator access -Copy the template and fill in your real credentials: +Create a long, unique password. The command reads it from standard input and prints only the scrypt hash that belongs in `.env.local`: ```bash -cp .env.local.example .env.local +printf '%s' 'choose-a-long-unique-password' | npm run auth:hash-password ``` -Then edit `.env.local`: +Generate a separate signing secret: ```bash -# Jira Configuration -JIRA_HOST=yourorg.atlassian.net # e.g. snapdocs.atlassian.net -JIRA_EMAIL=service-account@yourorg.com # Jira account email -JIRA_API_TOKEN=your-jira-api-token # From: id.atlassian.com → Security → API tokens -JIRA_SECOPS_PROJECT_KEY=SECOPS # Your Jira project key +openssl rand -base64 48 +``` -# Google Workspace Configuration -GOOGLE_CLIENT_EMAIL=service-account@your-project.iam.gserviceaccount.com -GOOGLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n" -GOOGLE_SUBJECT_EMAIL=admin@yourorg.com # Email to impersonate (must have Gmail + Alert Center access) -GOOGLE_ADMIN_EMAIL=admin@yourorg.com # Super Admin email for Directory API +Set the results: -# VirusTotal -VIRUSTOTAL_API_KEY=your-virustotal-api-key +```dotenv +SECURECATCH_ADMIN_PASSWORD_HASH=scrypt$16384$8$1$... +SECURECATCH_SESSION_SECRET=replace-with-the-generated-random-secret +``` -# OpenRouter -OPENROUTER_API_KEY=your-openrouter-api-key -OPENROUTER_MODEL=anthropic/claude-3.5-sonnet +The password itself is never stored. Sessions expire after eight hours. Changing the signing secret invalidates every existing session. -# Database (SQLite — no changes needed for local dev) -DATABASE_URL=file:./dev.db +Keep the remediation kill switch off during setup: + +```dotenv +SECURECATCH_ENABLE_DESTRUCTIVE_ACTIONS=false ``` ---- +## 3. Configure Jira -## 3. Google Workspace Service Account Setup +Create an API token from the Atlassian account-security page. Use a dedicated account with only the project permissions SecureCatch needs. -SecureCatch uses a **Google Service Account with Domain-Wide Delegation** to access Gmail, Alert Center, and the Admin SDK on behalf of users. +```dotenv +JIRA_HOST=yourorg.atlassian.net +JIRA_EMAIL=service-account@yourorg.com +JIRA_API_TOKEN=your-jira-api-token +JIRA_SECOPS_PROJECT_KEY=SECOPS +``` -### 3a. Create a Service Account +By default, ingestion finds open issues in that project whose summary contains `User-reported phishing`. If your intake uses a label, issue type, reporter, or custom field, set a complete query: -1. Go to [Google Cloud Console](https://console.cloud.google.com/) → **IAM & Admin → Service Accounts** -2. Click **Create Service Account** -3. Give it a name (e.g., `securecatch-soar`) -4. Click **Done** (no roles needed at this level) -5. Click on the new service account → **Keys** tab → **Add Key → Create new key → JSON** -6. Save the downloaded JSON file — you'll extract `client_email` and `private_key` from it +```dotenv +JIRA_PHISHING_JQL=project = "SECOPS" AND labels = phishing-intake AND statusCategory != Done ORDER BY created DESC +``` -### 3b. Enable Domain-Wide Delegation +The expected issue description includes `Actor:`, `Reported by:`, and, optionally, `Activity date:` lines. Jira workflows differ, so validate the project’s closing transition in a test ticket before enabling production use. -1. In the service account detail page, click **Edit** → check **Enable Google Workspace Domain-wide Delegation** -2. Save, then note the **Client ID** (numeric ID shown on the service account page) +## 4. Configure Google Workspace -### 3c. Grant OAuth Scopes in Google Admin +SecureCatch uses a Google service account with domain-wide delegation to read reported messages and, when separately enabled, trash an identified message from user mailboxes. -1. Go to [admin.google.com](https://admin.google.com) → **Security → API Controls → Domain-wide delegation** -2. Click **Add new** and enter: - - **Client ID:** (the numeric Client ID from step 3b) - - **OAuth Scopes:** - ``` - https://www.googleapis.com/auth/gmail.readonly, - https://www.googleapis.com/auth/gmail.modify, - https://www.googleapis.com/auth/admin.directory.user.readonly, - https://www.googleapis.com/auth/apps.alerts - ``` +### Create the service account -### 3d. Enable Required APIs +1. In Google Cloud Console, open **IAM & Admin > Service Accounts**. +2. Create a service account without project roles. +3. Open the account, select **Keys > Add key > Create new key**, and download the JSON key. +4. Edit the service account and enable Google Workspace domain-wide delegation. +5. Record its numeric OAuth client ID. -In Google Cloud Console → **APIs & Services → Enable APIs**: -- Gmail API -- Google Workspace Alert Center API -- Admin SDK API +Store the private key outside the repository. Never commit `.env.local` or the downloaded JSON file. -### 3e. Set Environment Variables +### Grant OAuth scopes -From the downloaded JSON key file: +In Google Admin, open **Security > API controls > Domain-wide delegation** and add the service account client ID with these scopes: -```bash -GOOGLE_CLIENT_EMAIL= -GOOGLE_PRIVATE_KEY= -GOOGLE_SUBJECT_EMAIL= -GOOGLE_ADMIN_EMAIL= +```text +https://www.googleapis.com/auth/gmail.readonly, +https://www.googleapis.com/auth/gmail.modify, +https://www.googleapis.com/auth/admin.directory.user.readonly, +https://www.googleapis.com/auth/apps.alerts ``` -> **Note:** When pasting `GOOGLE_PRIVATE_KEY` into `.env.local`, keep it on one line with `\n` as literal characters (not real newlines), and wrap the whole value in double quotes. +`gmail.modify` enables destructive mailbox actions. If you are evaluating only the interface or analysis flow, use a test Workspace and leave the SecureCatch remediation kill switch disabled. ---- +Enable these APIs in the Google Cloud project: -## 4. Jira API Token - -1. Go to [id.atlassian.com](https://id.atlassian.com) → **Security → API tokens → Create API token** -2. Copy the token and set it as `JIRA_API_TOKEN` -3. Set `JIRA_EMAIL` to the email of the Atlassian account the token belongs to -4. Set `JIRA_HOST` to your Atlassian subdomain (e.g., `yourorg.atlassian.net`) -5. Set `JIRA_SECOPS_PROJECT_KEY` to your SECOPS board project key +- Gmail API +- Google Workspace Alert Center API +- Admin SDK API ---- +Add the credentials to `.env.local`: -## 5. VirusTotal API Key +```dotenv +GOOGLE_CLIENT_EMAIL=service-account@your-project.iam.gserviceaccount.com +GOOGLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n" +GOOGLE_SUBJECT_EMAIL=admin@yourorg.com +GOOGLE_ADMIN_EMAIL=admin@yourorg.com +``` -1. Create an account at [virustotal.com](https://www.virustotal.com) -2. Go to **Profile → API Key** -3. Copy the key and set it as `VIRUSTOTAL_API_KEY` +Keep `GOOGLE_PRIVATE_KEY` on one line, preserve literal `\n` sequences, and wrap the value in double quotes. -> **Free tier limit:** 4 requests/minute, 500/day. The app automatically rate-limits URL checks. +## 5. Configure analysis services ---- +Create a VirusTotal API key and an OpenRouter API key, then set: -## 6. OpenRouter API Key +```dotenv +VIRUSTOTAL_API_KEY=your-virustotal-api-key +OPENROUTER_API_KEY=your-openrouter-api-key +OPENROUTER_MODEL=anthropic/claude-3.5-sonnet +``` -1. Create an account at [openrouter.ai](https://openrouter.ai) -2. Go to **Keys → Create key** -3. Set it as `OPENROUTER_API_KEY` -4. Set `OPENROUTER_MODEL` to your preferred model: - - `anthropic/claude-3.5-sonnet` (recommended) - - `openai/gpt-4o` - - `google/gemini-flash-1.5` +Choose any OpenRouter model that supports JSON-object responses. VirusTotal’s public API is rate-limited; SecureCatch spaces URL lookups, but your account’s current quota remains authoritative. ---- +Email evidence is sent to the configured model provider for classification, and indicators are sent to VirusTotal for enrichment. Confirm that this fits your organization’s data-handling rules. -## 7. Initialize the Database +## 6. Initialize the database -The SQLite database is automatically created. Run migrations to set up the schema: +The included schema uses SQLite: -```bash -npx prisma migrate deploy +```dotenv +DATABASE_URL=file:./dev.db ``` -To reset the database during development: +Apply the committed migration and generate the Prisma client: ```bash -npx prisma migrate reset +npm run db:migrate +npm run db:generate ``` -To inspect the database with a GUI: +For local inspection: ```bash npx prisma studio ``` ---- +SQLite requires persistent local storage and one application instance. Moving to PostgreSQL is a schema and migration change; do not point the existing SQLite migration at a PostgreSQL URL. -## 8. Start the Application +## 7. Validate before connecting production data ```bash -# Development mode (with hot reload) -npm run dev +npm run check +``` + +Start the application: -# OR use the startup script (starts dev server + opens browser) -./start.sh +```bash +npm run dev ``` -The app will be available at **http://localhost:3000** +Open [http://localhost:3000](http://localhost:3000), sign in, and validate this sequence: ---- +1. Run ingestion against a test Jira project. +2. Open an alert and run analysis. +3. Review the headers, message, indicators, classification, and confidence. +4. Use **Mark as safe / close** only after confirming the Jira transition behavior. +5. Verify logs and database state after every external action. -## Workflow +## 8. Enable remediation only after a dry run -``` -1. Open the dashboard at http://localhost:3000 -2. Click "Run Ingestion" — fetches unprocessed Jira SECOPS tickets -3. Click any alert row to open the detail view -4. Click "Run Analysis" — runs VirusTotal OSINT + AI classification -5. Review: AI reasoning, OSINT scores, raw email headers/body/links -6. Choose an action: - └── "Approve & Remediate" → domain-wide email purge + close Jira ticket - └── "Mark as Safe / Close" → false positive, close Jira ticket -``` +Domain-wide remediation can alter every mailbox in the delegated Workspace domain. Before enabling it: ---- +- use a test Workspace or a tightly controlled test message; +- confirm the RFC 2822 Message-ID resolution; +- review the service account scopes and impersonated administrator; +- confirm operator authentication and HTTPS at the deployment boundary; +- back up the SQLite database; +- verify Jira comments and closing transitions. -## Production Deployment +Then set the server-side kill switch and restart the application: -For production, switch from SQLite to PostgreSQL: +```dotenv +SECURECATCH_ENABLE_DESTRUCTIVE_ACTIONS=true +``` -1. Update `DATABASE_URL` in your environment: - ``` - DATABASE_URL=postgresql://user:password@host:5432/securecatch - ``` +The UI still requires an authenticated analyst, an alert in `AWAITING_REVIEW`, completed analysis, and exact entry of the Jira ticket key. -2. Update `prisma/schema.prisma` datasource provider: - ```prisma - datasource db { - provider = "postgresql" - } - ``` +## Production boundary -3. Run migrations: - ```bash - npx prisma migrate deploy - ``` +Build and start the application with: -4. Build and start: - ```bash - npm run build - npm run start - ``` +```bash +npm run build +npm run start +``` ---- +Deploy it only on an internal network or behind an identity-aware access proxy. Terminate TLS before traffic reaches the app, keep all secrets in the platform’s secret manager, persist and back up the database, and restrict outbound access to the integration endpoints you use. ## Troubleshooting -| Issue | Solution | -|-------|----------| -| `Missing Jira configuration` | Ensure `JIRA_HOST`, `JIRA_EMAIL`, `JIRA_API_TOKEN` are set in `.env.local` | -| `Missing Google credentials` | Ensure `GOOGLE_CLIENT_EMAIL` and `GOOGLE_PRIVATE_KEY` are set | -| `Alert Center API failed` | Ensure the service account has the `apps.alerts` scope granted in Google Admin | -| `Gmail API failed` | Ensure Domain-Wide Delegation is enabled and `gmail.modify` scope is granted | -| `No tickets found` | Check that your Jira project key is correct and the filter matches your ticket format | -| `AI parse error` | The model returned malformed JSON — try a different `OPENROUTER_MODEL` | -| `Database errors` | Run `npx prisma migrate deploy` to ensure schema is up to date | +| Symptom | Check | +| --- | --- | +| `Authentication is not configured` | Password hash and signing secret are present; signing secret is at least 32 characters | +| `Invalid credentials` | The plaintext password matches the value used to generate the scrypt hash | +| `Missing Jira configuration` | Jira host, email, token, and project key are set | +| `Missing Google credentials` | Service account email and escaped private key are set | +| Alert Center request fails | Alert Center API is enabled and `apps.alerts` was delegated | +| Gmail request fails | Gmail API is enabled and required Gmail scopes were delegated | +| No tickets are found | Project key and the ticket format expected by the ingestion parser match your test issue | +| Model response is rejected | Selected model supports JSON-object output and returned the required classification schema | +| Remediation is disabled | Keep it disabled during setup; otherwise check the server-side kill switch after completing the dry-run checklist | +| Alert is `ACTION_FAILED` | Inspect server logs and both external systems before retrying; an earlier step may already have succeeded | diff --git a/app/alerts/[id]/page.tsx b/app/alerts/[id]/page.tsx index 1d6bb0a..ff85a74 100644 --- a/app/alerts/[id]/page.tsx +++ b/app/alerts/[id]/page.tsx @@ -65,7 +65,10 @@ export default function AlertDetailPage({ params }: PageProps) { async function runAnalysis() { setAnalyzing(true); try { - const res = await fetch(`/api/analyze/${id}`, { method: "POST" }); + const res = await fetch(`/api/analyze/${id}`, { + method: "POST", + headers: { "X-SecureCatch-Request": "1" }, + }); const data = await res.json() as { message?: string; error?: string }; if (!res.ok) { toast.error(data.error ?? "Analysis failed"); @@ -106,9 +109,11 @@ export default function AlertDetailPage({ params }: PageProps) { ); } - const isActionable = - alert.status === "AWAITING_REVIEW" || alert.status === "INGESTED" || alert.status === "ANALYZING"; + const isActionable = alert.status === "AWAITING_REVIEW"; const isCompleted = alert.status === "REMEDIATED" || alert.status === "CLOSED"; + const canAnalyze = ["INGESTED", "ANALYZING", "ANALYSIS_FAILED", "AWAITING_REVIEW"].includes( + alert.status + ); return (
@@ -141,7 +146,7 @@ export default function AlertDetailPage({ params }: PageProps) { View in Jira )} - {!isCompleted && ( + {canAnalyze && ( + + + + + ); +} diff --git a/components/dashboard-header.tsx b/components/dashboard-header.tsx index 2516baa..308d228 100644 --- a/components/dashboard-header.tsx +++ b/components/dashboard-header.tsx @@ -1,9 +1,10 @@ "use client"; import { useState } from "react"; -import { ShieldAlert, RefreshCw, Play } from "lucide-react"; +import { ShieldAlert, RefreshCw, Play, LogOut } from "lucide-react"; import { Button } from "@/components/ui/button"; import { toast } from "sonner"; +import { useRouter } from "next/navigation"; interface DashboardHeaderProps { onRefresh: () => void; @@ -11,11 +12,15 @@ interface DashboardHeaderProps { export function DashboardHeader({ onRefresh }: DashboardHeaderProps) { const [ingesting, setIngesting] = useState(false); + const router = useRouter(); async function runIngestion() { setIngesting(true); try { - const res = await fetch("/api/ingest/start", { method: "POST" }); + const res = await fetch("/api/ingest/start", { + method: "POST", + headers: { "X-SecureCatch-Request": "1" }, + }); const data = await res.json() as { message?: string; error?: string; processed?: number; newAlerts?: string[] }; if (!res.ok) { @@ -39,6 +44,15 @@ export function DashboardHeader({ onRefresh }: DashboardHeaderProps) { } } + async function signOut() { + await fetch("/api/auth/logout", { + method: "POST", + headers: { "X-SecureCatch-Request": "1" }, + }); + router.replace("/login"); + router.refresh(); + } + return (
@@ -56,6 +70,10 @@ export function DashboardHeader({ onRefresh }: DashboardHeaderProps) { Refresh +