diff --git a/CONTRIBUTING_GUIDE_GSSOC.md b/CONTRIBUTING_GUIDE_GSSOC.md index 386a1ed..2c99514 100644 --- a/CONTRIBUTING_GUIDE_GSSOC.md +++ b/CONTRIBUTING_GUIDE_GSSOC.md @@ -32,45 +32,6 @@ Maintainers may ask you to confirm: If the maintainer asks for a smaller first step, follow that direction before expanding the change. - -## GSSoC Labels & Scoring (Project-specific) - -Maintainers use the following labels and scoring rules to award points for merged PRs. Apply the `gssoc:approved` label once a contribution meets the project standards (tests, style, and review). - -- **gssoc:approved** : +50 pts (base for every approved PR) - -Difficulty (pick one): - -- **level:beginner** : +20 pts -- **level:intermediate** : +35 pts -- **level:advanced** : +55 pts -- **level:critical** : +80 pts - -Quality multiplier (optional): - -- **quality:clean** : ×1.2 -- **quality:exceptional** : ×1.5 - -Type bonus (optional, stackable): - -- **type:docs** : +5 -- **type:bug** : +10 -- **type:feature** : +10 -- **type:testing** : +10 -- **type:design** : +10 -- **type:refactor** : +10 -- **type:accessibility** : +15 -- **type:performance** : +15 -- **type:devops** : +15 -- **type:security** : +20 - -Formula (recommended): - -``` -50 + (difficulty × quality) + type bonus -``` - -Requirements to award points: ## Difficulty Expectations ### Easy @@ -163,3 +124,48 @@ Requirements to award points: - PR must carry the **gssoc:approved** label (added by maintainers) Maintainers may adjust final points based on actual impact and implementation quality. + +## GSSoC Labels & Scoring (Project-specific) + +Maintainers use the following labels and scoring rules to award points for merged PRs. Apply the `gssoc:approved` label once a contribution meets the project standards (tests, style, and review). + +- **gssoc:approved** : +50 pts (base for every approved PR) + +Difficulty (pick one): + +- **level:beginner** : +20 pts +- **level:intermediate** : +35 pts +- **level:advanced** : +55 pts +- **level:critical** : +80 pts + +Quality multiplier (optional): + +- **quality:clean** : ×1.2 +- **quality:exceptional** : ×1.5 + +Type bonus (optional, stackable): + +- **type:docs** : +5 +- **type:bug** : +10 +- **type:feature** : +10 +- **type:testing** : +10 +- **type:design** : +10 +- **type:refactor** : +10 +- **type:accessibility** : +15 +- **type:performance** : +15 +- **type:devops** : +15 +- **type:security** : +20 + +Formula (recommended): + +``` +50 + (difficulty × quality) + type bonus +``` + +Requirements to award points: + +- PR must be **merged** +- At least **one review** approved +- PR must carry the **gssoc:approved** label (added by maintainers) + +Maintainers may adjust final points based on actual impact and implementation quality. diff --git a/GOOD_FIRST_ISSUE.md b/GOOD_FIRST_ISSUE.md index ffbafc8..e84a2e9 100644 --- a/GOOD_FIRST_ISSUE.md +++ b/GOOD_FIRST_ISSUE.md @@ -1,81 +1,643 @@ -# Good First Issues for SecDev +# SecDev Audit Report -These starter issues are tailored to SecDev's current codebase and its Next.js, TypeScript, E2B, Neon, and Inngest architecture. They are intentionally scoped so a new contributor can learn the project without needing to change the entire system. +## Console Pages Found -## 1. Improve the SecDev Placeholder Screen +- Account +- API Testing +- Attack Pipeline +- Billing +- Dashboard +- Deployments +- Deployment Detail +- Env Vars +- GitHub +- Logs +- Notifications +- Performance +- Projects +- Sandboxes +- Security +- Security Agent +- Settings +- Test Suite +- Vibetest -**Problem**: Several console surfaces still rely on the shared placeholder UI, and the current message is generic. +## File: app/api/env-vars/route.ts -**Expected outcome**: Make the placeholder more informative for SecDev by adding context-specific copy and a clearer call to action for each page that uses it. +## [CRITICAL] Unauthenticated env-var read/write endpoint -**Skills needed**: React components, Tailwind CSS, basic Next.js App Router structure. +**File:** `app/api/env-vars/route.ts` +**Line(s):** `14–83` +**Console Page (if applicable):** `Env Vars` +**Severity:** `Critical` -**Difficulty**: Easy +### Description -**Relevant files or folders**: +This route exposes plaintext secret storage and mutation without any authentication or authorization checks. `GET` can reveal plaintext values with `reveal=1`, and `POST`/`DELETE` can overwrite or remove values for any `project` supplied by the caller. -- `components/console/not-implemented.tsx` -- `app/console/*/page.tsx` +### Evidence (from code) -## 2. Make the Console Sidebar More Accessible +```ts +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const project = searchParams.get("project"); + const reveal = searchParams.get("reveal") === "1"; -**Problem**: The console sidebar is functional, but the collapse and group-toggle interactions can be improved for keyboard and screen-reader users. + if (!project) { + return NextResponse.json( + { error: "project query param required" }, + { status: 400 }, + ); + } -**Expected outcome**: Add better accessibility labels, clearer focus behavior, and more descriptive toggle states while keeping the current design. + if (reveal) { + const vars = await getEnvVars(project); + return NextResponse.json({ + ok: true, + vars: Object.entries(vars).map(([key, value]) => ({ key, value })), + }); + } -**Skills needed**: React event handling, accessibility basics, Lucide icons, Tailwind CSS. + const masked = await listEnvVars(project); + return NextResponse.json({ ok: true, vars: masked }); + } catch (err: unknown) { + const message = err instanceof Error ? err.message : String(err); + return NextResponse.json({ ok: false, error: message }, { status: 500 }); + } +} +``` + +### Suggested Fix + +Require a valid session before any read or write, then restrict each `project` to the authenticated user's resources. For secret reads, avoid a plaintext reveal flag entirely or gate it behind a stricter admin-only policy. -**Difficulty**: Easy +## File: lib/env-store.ts -**Relevant files or folders**: +## [HIGH] Hardcoded fallback key weakens encrypted env vars -- `components/console/sidebar.tsx` -- `app/console/layout.tsx` +**File:** `lib/env-store.ts` +**Line(s):** `20` +**Console Page (if applicable):** `Env Vars` +**Severity:** `High` -## 3. Remove Direct Console Logging from the Email Service +### Description -**Problem**: The email service still emits an info-level `console.log` path, which does not match the project's production logging expectations. +If `NEXTAUTH_SECRET` is missing, secret encryption falls back to a hardcoded development string. That means env-var values can be decrypted with a predictable key instead of a deployment-specific secret. -**Expected outcome**: Replace direct console logging with the project's structured logging approach so email events are handled consistently. +### Evidence (from code) -**Skills needed**: TypeScript, async/await, service-layer refactoring, logging hygiene. +```ts +function getKey(): Buffer { + const raw = process.env.NEXTAUTH_SECRET ?? "default-dev-secret-32-chars-long"; + return createHash("sha256").update(raw).digest(); +} +``` -**Difficulty**: Easy +### Suggested Fix -**Relevant files or folders**: +Fail fast when `NEXTAUTH_SECRET` is absent in non-development environments. Do not derive encryption keys from a hardcoded fallback string. -- `lib/email/mail-service.ts` -- `lib/email/brevo.ts` -- `lib/email/notifications.ts` +## File: lib/deployer.ts -## 4. Add Better Empty-State Copy to the Logs Page +## [CRITICAL] Shell command injection in sandbox deployment pipeline -**Problem**: The console logs view should guide users more clearly when no sandbox is selected or no log lines are available yet. +**File:** `lib/deployer.ts` +**Line(s):** `236, 266–272` +**Console Page (if applicable):** `Deployments` +**Severity:** `Critical` -**Expected outcome**: Show helpful empty and error states so contributors can understand what to do next instead of seeing a blank or confusing view. +### Description -**Skills needed**: React state rendering, conditional UI, Next.js data fetching. +User-controlled `repoUrl`, `branch`, and environment-variable data are interpolated directly into shell command strings. A crafted repository URL or branch can change the `git clone` command, and env-var content is concatenated into the install command without shell escaping. -**Difficulty**: Easy +### Evidence (from code) -**Relevant files or folders**: +```ts +const cloneResult = await sandbox.commands.run( + `git clone --depth 1 --branch ${branch} ${repoUrl} /home/user/repo 2>&1`, + { onStdout, onStderr, timeoutMs: 120_000 }, +); -- `app/console/logs/page.tsx` -- `components/console/*` +const envStr = envVars + ? Object.entries(envVars) + .map(([k, v]) => `${k}=${JSON.stringify(v)}`) + .join(" ") + : ""; -## 5. Improve Repository Browser Copy and CTA Clarity +const installCmd = `cd /home/user/repo && ${envStr} ${pkgMgr} install 2>&1`; +``` -**Problem**: The repository browser is one of the first user-facing flows in SecDev, and its cards/tables can explain the deploy action more clearly. +### Suggested Fix -**Expected outcome**: Refine the copy in the repository browser so users immediately understand what each repository state means and what happens when they click deploy. +Stop building shell commands with string interpolation. Use argument arrays or a shell-escaping library, and validate `repoUrl`, `branch`, and env-var values against strict allowlists before they reach the sandbox. -**Skills needed**: React components, UX copywriting, conditional rendering, TypeScript. +## File: app/api/dashboard/stats/route.ts -**Difficulty**: Medium +## [HIGH] Dashboard stats leak legacy rows across users -**Relevant files or folders**: +**File:** `app/api/dashboard/stats/route.ts` +**Line(s):** `29, 36, 43, 84` +**Console Page (if applicable):** `Dashboard` +**Severity:** `High` -- `components/console/repository-card.tsx` -- `components/console/repository-table.tsx` -- `components/console/repository-list.tsx` -- `app/console/projects/page.tsx` +### Description + +Each stats query includes rows where `user_id = ''`, so any authenticated user can see records that were created without an owner. That leaks deployment, test-run, and security-agent counts across accounts and makes the dashboard totals untrustworthy. + +### Evidence (from code) + +```ts + sql` + SELECT status, COUNT(*) AS count + FROM deployments + WHERE user_id = ${userId} OR user_id = '' + GROUP BY status + `, + sql` + SELECT type, status, COUNT(*) AS count + FROM test_runs + WHERE user_id = ${userId} OR user_id = '' + GROUP BY type, status + `, + sql` + SELECT status, COUNT(*) AS count + FROM security_agent_runs + WHERE user_id = ${userId} OR user_id = '' + GROUP BY status + `, +... + WHERE user_id = ${userId} OR user_id = '' +``` + +### Suggested Fix + +Remove the `user_id = ''` fallback and backfill/repair legacy rows before exposing them. If legacy rows must remain accessible, migrate them to a real owner or gate them behind a separate admin-only path. + +## File: app/api/deploy/[id]/route.ts + +## [HIGH] Empty-owner deployments bypass the ownership check + +**File:** `app/api/deploy/[id]/route.ts` +**Line(s):** `17–18` +**Console Page (if applicable):** `Deployments` +**Severity:** `High` + +### Description + +The ownership check only rejects a deployment when `record.userId` is truthy and different from the session user. Any deployment row with an empty `userId` skips the check entirely, so authenticated users can read, restart, refresh, or delete those rows. + +### Evidence (from code) + +```ts +if (record.userId && record.userId !== session.user.id) { + return { + error: NextResponse.json( + { ok: false, error: "Forbidden" }, + { status: 403 }, + ), + }; +} +``` + +### Suggested Fix + +Treat missing ownership as forbidden unless the record is explicitly marked public. Enforce ownership on insert and backfill existing rows so an empty `userId` cannot bypass access control. + +## File: app/api/tests/run/route.ts + +## [HIGH] Test-run history leaks and accepts legacy unowned rows + +**File:** `app/api/tests/run/route.ts` +**Line(s):** `47–52, 93` +**Console Page (if applicable):** `Test Suite` +**Severity:** `High` + +### Description + +The POST path only blocks access when a deployment row exists and has a non-empty `user_id`. If the deployment row is missing or still has `user_id = ''`, the route creates a test run anyway. The GET path has the same `user_id = ''` fallback, so run history from those legacy rows is visible across users. + +### Evidence (from code) + +```ts + const dep = await sql`SELECT user_id FROM deployments WHERE sandbox_id = ${sandboxId} LIMIT 1`; + if (dep.length > 0 && dep[0].user_id && dep[0].user_id !== userId) { + return NextResponse.json({ ok: false, error: "Forbidden" }, { status: 403 }); + } +... + rows = await sql`SELECT * FROM test_runs WHERE (user_id = ${userId} OR user_id = '') ORDER BY created_at DESC LIMIT 100`; +``` + +### Suggested Fix + +Require an owned deployment row before starting a run, and stop exposing `user_id = ''` rows in history. If legacy runs must be retained, migrate them to a real owner or hide them from normal users. + +## File: app/api/tests/ai-plan/route.ts + +## [HIGH] AI planning trusts arbitrary sandbox IDs + +**File:** `app/api/tests/ai-plan/route.ts` +**Line(s):** `79–92` +**Console Page (if applicable):** `Security Agent` +**Severity:** `High` + +### Description + +The route accepts any `sandboxId`, parses its routes, and sends the result to OpenAI without proving that the sandbox belongs to the authenticated user. A caller can therefore request AI planning for a sandbox they do not own, which misattributes analysis and can expose route structure. + +### Evidence (from code) + +```ts +const { sandboxId } = (await request.json()) as { sandboxId?: string }; +if (!sandboxId) { + return NextResponse.json( + { ok: false, error: "sandboxId is required" }, + { status: 400 }, + ); +} + +// Discover routes +const routes = await parseApiRoutes(sandboxId); +if (routes.length === 0) { + return NextResponse.json( + { ok: false, error: "No API routes found in this deployment" }, + { status: 404 }, + ); +} + +const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY! }); +``` + +### Suggested Fix + +Look up the deployment by `sandboxId` and verify ownership before parsing routes. Also guard `OPENAI_API_KEY` explicitly so the route fails with a clear configuration error instead of a runtime exception. + +## File: app/api/attack-pipeline/route.ts + +## [MEDIUM] Attack-pipeline runs are accepted for arbitrary sandbox IDs + +**File:** `app/api/attack-pipeline/route.ts` +**Line(s):** `36–60` +**Console Page (if applicable):** `Attack Pipeline` +**Severity:** `Medium` + +### Description + +The POST handler stores and executes a run for whatever `sandboxId` the caller supplies, but it never checks that the sandbox belongs to the signed-in user. That allows a caller to misattribute runs to other sandbox IDs and pollute the attack-pipeline history. + +### Evidence (from code) + +```ts +const { baseUrl, sandboxId, useAi = false, includePerformance = false } = body; + +if (!baseUrl || typeof baseUrl !== "string") { + return NextResponse.json( + { ok: false, error: "baseUrl is required" }, + { status: 400 }, + ); +} + +// Basic URL validation — must start with http(s):// +if (!/^https?:\/\/.+/i.test(baseUrl)) { + return NextResponse.json( + { ok: false, error: "baseUrl must start with http:// or https://" }, + { status: 400 }, + ); +} + +await ensureTables(); +const sql = getDb(); + +const runId = `ap_${randomBytes(8).toString("hex")}`; + +// Insert run record immediately so history shows it +await sql` + INSERT INTO attack_pipeline_runs + (id, user_id, base_url, sandbox_id, status, created_at) + VALUES + (${runId}, ${userId}, ${baseUrl}, ${sandboxId ?? null}, 'running', ${Date.now()}) + `; +``` + +### Suggested Fix + +Verify the sandbox ownership before inserting the run record. If the sandbox does not belong to the authenticated user, return `403` and do not persist or execute the scan. + +## File: app/console/dashboard/page.tsx + +## [MEDIUM] Dashboard swallows stats fetch failures + +**File:** `app/console/dashboard/page.tsx` +**Line(s):** `53–64` +**Console Page (if applicable):** `Dashboard` +**Severity:** `Medium` + +### Description + +The page fetches dashboard stats on mount, but the `catch` handler does nothing and the component never tracks an error state. If `/api/dashboard/stats` fails, the page quietly shows stale or empty metrics with no feedback to the user. + +### Evidence (from code) + +```ts +const fetchStats = async () => { + try { + const res = await fetch("/api/dashboard/stats"); + const data = await res.json(); + if (data.ok) setStats(data); + } catch { + /* ignore */ + } + setLoading(false); +}; + +useEffect(() => { + fetch("/api/dashboard/stats") + .then((r) => r.json()) + .then((data) => { + if (data.ok) setStats(data); + }) + .catch(() => {}) + .finally(() => setLoading(false)); +}, []); +``` + +### Suggested Fix + +Store a visible error state and render an inline failure message or retry control when the stats request fails. Avoid maintaining two separate fetch paths for the same mount-time data. + +## File: app/console/settings/page.tsx + +## [MEDIUM] Save Changes is only local UI state + +**File:** `app/console/settings/page.tsx` +**Line(s):** `43–180` +**Console Page (if applicable):** `Settings` +**Severity:** `Medium` + +### Description + +The settings page presents editable deployment settings and a `Save Changes` button, but the save handler only flips local state and never persists anything. Users can interact with the form, but none of the values are written to the backend. + +### Evidence (from code) + +```ts + const save = () => { + setSaved(true); + setTimeout(() => setSaved(false), 2000); + }; +... +
{hint}
} -Manage your profile and security settings
-Profile Picture
-Click to upload a new avatar (PNG, JPG)
-{s.device}
-{s.loc}
-- Permanently delete your account and all associated data. This action cannot be undone. -
-+ Connect GitHub once, then browse repositories and deploy directly from SecDev. +
++ GitHub is not connected yet. Use the button below to authorize GitHub for this account. +
++ Use email and password to sign in. After login, you can connect GitHub from your account page. +
{/* Sign up link */}diff --git a/app/register/page.tsx b/app/register/page.tsx index 60cab69..f1b70f3 100644 --- a/app/register/page.tsx +++ b/app/register/page.tsx @@ -3,9 +3,6 @@ import React, { useState } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import { Eye, EyeOff } from "lucide-react"; -import { signUpWithEmail } from "@/lib/firebase"; -import { signIn } from "next-auth/react"; -import { Github } from "lucide-react"; export default function RegisterPage() { const [email, setEmail] = useState(""); @@ -20,8 +17,19 @@ export default function RegisterPage() { setLoading(true); setError(null); try { - await signUpWithEmail(email, password); - router.push("/"); + const response = await fetch("/api/auth/register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }), + }); + + const payload = (await response.json()) as { ok?: boolean; error?: string; detail?: string }; + + if (!response.ok || !payload.ok) { + throw new Error(payload.error ?? payload.detail ?? "Registration failed"); + } + + router.push("/login"); } catch (err: unknown) { setError(err instanceof Error ? err.message : "Registration failed"); } finally { @@ -47,6 +55,12 @@ export default function RegisterPage() { )} + {error === "Email already in use" && ( +
+ That account already exists. Use Sign In instead. +
+ )} + {/* Form */}