diff --git a/backend/Dockerfile b/backend/Dockerfile index 662136a..b80452d 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -6,6 +6,7 @@ WORKDIR /app RUN apt-get update && apt-get install -y \ gcc \ libpq-dev \ + curl \ && rm -rf /var/lib/apt/lists/* # Copy requirements first for better caching @@ -15,8 +16,11 @@ RUN pip install --no-cache-dir -r requirements.txt # Copy application code COPY . . +# Make entrypoint executable +RUN chmod +x /app/entrypoint.sh + # Expose port EXPOSE 8000 -# Default command -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] +# Default command (migrations + seed + uvicorn) +CMD ["sh", "/app/entrypoint.sh"] diff --git a/backend/entrypoint.sh b/backend/entrypoint.sh new file mode 100644 index 0000000..7923f32 --- /dev/null +++ b/backend/entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -e + +echo "=== Running Alembic migrations ===" +alembic upgrade head + +echo "=== Seeding admin user ===" +python -m app.seed + +echo "=== Starting uvicorn ===" +exec uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload diff --git a/docker-compose.yml b/docker-compose.yml index f22bec9..d420b6f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -84,7 +84,7 @@ services: condition: service_started volumes: - ./backend:/app - command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload + command: sh /app/entrypoint.sh healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/health"] interval: 10s diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2aacb6c..e34b111 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,6 +6,7 @@ import TechniqueDetailPage from "./pages/TechniqueDetailPage"; import TestsPage from "./pages/TestsPage"; import TestCreatePage from "./pages/TestCreatePage"; import TestDetailPage from "./pages/TestDetailPage"; +import TestCatalogPage from "./pages/TestCatalogPage"; import SystemPage from "./pages/SystemPage"; import UsersPage from "./pages/UsersPage"; import AuditLogPage from "./pages/AuditLogPage"; @@ -32,6 +33,8 @@ export default function App() { } /> } /> } /> + } /> + } /> void; +} + +// ── Component ────────────────────────────────────────────────────── + +export default function TestFromTemplateForm({ + templateId, + techniqueId: propTechniqueId, + onClose, +}: TestFromTemplateFormProps) { + const navigate = useNavigate(); + + // ── Load template details ────────────────────────────────────── + + const { + data: template, + isLoading, + error, + } = useQuery({ + queryKey: ["test-template", templateId], + queryFn: () => getTemplateById(templateId), + enabled: !!templateId, + }); + + // ── Form state (pre-filled from template) ────────────────────── + + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [technique, setTechnique] = useState(propTechniqueId || ""); + const [platform, setPlatform] = useState(""); + const [procedureText, setProcedureText] = useState(""); + const [toolUsed, setToolUsed] = useState(""); + const [expectedDetection, setExpectedDetection] = useState(""); + + // Hydrate form when template loads + useEffect(() => { + if (template) { + setName(template.name || ""); + setDescription(template.description || ""); + setTechnique(propTechniqueId || template.mitre_technique_id || ""); + setPlatform(template.platform || ""); + setProcedureText(template.attack_procedure || ""); + setToolUsed(template.tool_suggested || ""); + setExpectedDetection(template.expected_detection || ""); + } + }, [template, propTechniqueId]); + + // ── Submit ───────────────────────────────────────────────────── + + const createMutation = useMutation({ + mutationFn: () => createTestFromTemplate(templateId, technique), + onSuccess: (test) => { + navigate(`/tests/${test.id}`); + }, + }); + + const canSubmit = name.trim().length > 0 && technique.trim().length > 0; + + // ── Render ───────────────────────────────────────────────────── + + return ( +
+
+ {/* Header */} +
+
+ +

Create Test from Template

+
+ +
+ + {/* Body */} + {isLoading ? ( +
+ +
+ ) : error ? ( +
+ +

Failed to load template

+
+ ) : ( +
+ {/* Template info banner */} +
+

+ Pre-filled from template: {template?.name} + {template?.source && ( + + {template.source} + + )} +

+
+ + {/* Name */} +
+ + setName(e.target.value)} + className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 placeholder-gray-500 focus:border-cyan-500 focus:outline-none focus:ring-1 focus:ring-cyan-500" + placeholder="Test name" + /> +
+ + {/* Description */} +
+ +