├── .gitignore ├── .idx └── dev.nix ├── .modified ├── .vscode ├── extensions.json └── settings.json ├── CODE_OF_CONDUCT.md ├── CODE_OF_CONDUCT_ES.md ├── CONTRIBUTING.md ├── CONTRIBUTING_ES.md ├── GUIA_USUARIO.md ├── README.es.md ├── README.md ├── SECURITY.md ├── SECURITY_ES.md ├── USER_GUIDE.md ├── components.json ├── ctf_writeup_builder-main.code-workspace ├── docs ├── blueprint.md ├── logo.png └── screenshots │ ├── ai-editor.png │ ├── export.png │ └── main-view.png ├── favicon.ico.png ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── src ├── ai │ ├── dev.ts │ ├── flows │ │ ├── pdf-processor-flow.ts │ │ └── section-suggester-flow.ts │ └── genkit.ts ├── app │ ├── [locale] │ │ ├── layout.tsx │ │ └── page.tsx │ ├── api │ │ └── ai-generate │ │ │ └── route.ts │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── components │ ├── AboutModal.tsx │ ├── ActiveSectionEditor.tsx │ ├── ApiKeyConfigModal.tsx │ ├── AppLayout.tsx │ ├── GeneralInfoPanel.tsx │ ├── HelpModal.tsx │ ├── ImageUploader.tsx │ ├── MarkdownEditor.tsx │ ├── PdfExportModal.tsx │ ├── SectionItemCard.tsx │ ├── SectionsManager.tsx │ ├── TagInput.tsx │ ├── WriteUpPreview.tsx │ ├── WysiwygEditor.tsx │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── card.tsx │ │ ├── chart.tsx │ │ ├── checkbox.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ └── tooltip.tsx ├── contexts │ └── WriteUpContext.tsx ├── hooks │ ├── use-mobile.tsx │ ├── use-toast.ts │ └── useWriteUp.ts ├── lib │ ├── constants.ts │ ├── types.ts │ └── utils.ts ├── locales │ ├── client.ts │ ├── en │ │ └── index.ts │ ├── es │ │ └── index.ts │ ├── i18n.ts │ └── server.ts ├── middleware.ts └── utils │ └── pdfExtractorEnhanced.ts ├── tailwind.config.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | next-env.d.ts 39 | 40 | .genkit/* 41 | .env* 42 | 43 | # firebase 44 | firebase-debug.log 45 | firestore-debug.log -------------------------------------------------------------------------------- /.idx/dev.nix: -------------------------------------------------------------------------------- 1 | # To learn more about how to use Nix to configure your environment 2 | # see: https://firebase.google.com/docs/studio/customize-workspace 3 | {pkgs}: { 4 | # Which nixpkgs channel to use. 5 | channel = "stable-24.11"; # or "unstable" 6 | # Use https://search.nixos.org/packages to find packages 7 | packages = [ 8 | pkgs.nodejs_20 9 | pkgs.zulu 10 | ]; 11 | # Sets environment variables in the workspace 12 | env = {}; 13 | # This adds a file watcher to startup the firebase emulators. The emulators will only start if 14 | # a firebase.json file is written into the user's directory 15 | services.firebase.emulators = { 16 | detect = true; 17 | projectId = "demo-app"; 18 | services = ["auth" "firestore"]; 19 | }; 20 | idx = { 21 | # Search for the extensions you want on https://open-vsx.org/ and use "publisher.id" 22 | extensions = [ 23 | # "vscodevim.vim" 24 | ]; 25 | workspace = { 26 | onCreate = { 27 | default.openFiles = [ 28 | "src/app/page.tsx" 29 | ]; 30 | }; 31 | }; 32 | # Enable previews and customize configuration 33 | previews = { 34 | enable = true; 35 | previews = { 36 | web = { 37 | command = ["npm" "run" "dev" "--" "--port" "$PORT" "--hostname" "0.0.0.0"]; 38 | manager = "web"; 39 | }; 40 | }; 41 | }; 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /.modified: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilanami/ctf_writeup_builder/09d0939aed968f56dcd6fd568b16d7560ebd947a/.modified -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "github.copilot" 4 | ] 5 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IDX.aI.enableInlineCompletion": true, 3 | "IDX.aI.enableCodebaseIndexing": true 4 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | We as members, contributors, and maintainers pledge to make participation in our project and community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 5 | 6 | ## Our Standards 7 | - Be respectful and considerate. 8 | - No harassment, bullying, or discrimination. 9 | - Use inclusive and welcoming language. 10 | - Respect differing viewpoints and experiences. 11 | - Accept constructive criticism gracefully. 12 | - Focus on what is best for the community. 13 | 14 | ## Reporting Issues 15 | If you experience or witness unacceptable behavior, please report it to: **writeup_builder@proton.me** 16 | 17 | ## Enforcement 18 | - Project maintainers are responsible for clarifying standards and enforcing this code of conduct. 19 | - Maintainers have the right to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned with this Code of Conduct. 20 | 21 | ## Attribution 22 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/). -------------------------------------------------------------------------------- /CODE_OF_CONDUCT_ES.md: -------------------------------------------------------------------------------- 1 | # Código de Conducta 2 | 3 | ## Nuestro Compromiso 4 | Como miembros, contribuyentes y mantenedores, nos comprometemos a que la participación en este proyecto y comunidad sea una experiencia libre de acoso para todos, sin importar edad, tamaño corporal, discapacidad visible o invisible, etnia, características sexuales, identidad y expresión de género, nivel de experiencia, educación, estatus socioeconómico, nacionalidad, apariencia personal, raza, religión o identidad y orientación sexual. 5 | 6 | ## Nuestros Estándares 7 | - Sé respetuoso y considerado. 8 | - No se tolera acoso, bullying ni discriminación. 9 | - Usa un lenguaje inclusivo y acogedor. 10 | - Respeta puntos de vista y experiencias diferentes. 11 | - Acepta críticas constructivas con humildad. 12 | - Enfócate en lo mejor para la comunidad. 13 | 14 | ## Reporte de Incidentes 15 | Si experimentas o presencias un comportamiento inaceptable, repórtalo a: **writeup_builder@proton.me** 16 | 17 | ## Aplicación 18 | - Los mantenedores del proyecto son responsables de aclarar y hacer cumplir este código de conducta. 19 | - Los mantenedores pueden eliminar, editar o rechazar comentarios, commits, código, ediciones de wiki, issues y otras contribuciones que no se alineen con este código. 20 | 21 | ## Atribución 22 | Este Código de Conducta está adaptado del [Contributor Covenant](https://www.contributor-covenant.org/). -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | Thank you for your interest in contributing to CTF Write-up Builder! 4 | 5 | ## How to Contribute 6 | - Fork the repository and create your branch from `master`. 7 | - Follow the code style and conventions used in the project. 8 | - Write clear, descriptive commit messages. 9 | - Test your changes locally before submitting a pull request. 10 | - Ensure your code does not introduce security vulnerabilities. 11 | 12 | ## Security Best Practices 13 | - Sanitize all user input and output. Use [DOMPurify](https://github.com/cure53/DOMPurify) for any HTML rendering. 14 | - Never commit secrets, API keys, or credentials to the repository. 15 | - Validate and restrict file uploads and imports. 16 | - Follow the principle of least privilege in all code and configuration. 17 | 18 | ## Reporting Security Issues 19 | If you find a security issue, please report it privately to **writeup_builder@proton.me**. Do not open a public issue. 20 | 21 | ## Responsible Disclosure 22 | - Give us reasonable time to address the issue before public disclosure. 23 | - Provide clear steps to reproduce the vulnerability. 24 | 25 | ## Community Standards 26 | - Follow our [Code of Conduct](./CODE_OF_CONDUCT.md). 27 | - Be respectful and collaborative. 28 | 29 | We appreciate your help in making CTF Write-up Builder better and more secure! -------------------------------------------------------------------------------- /CONTRIBUTING_ES.md: -------------------------------------------------------------------------------- 1 | # Guía de Contribución 2 | 3 | ¡Gracias por tu interés en contribuir a CTF Write-up Builder! 4 | 5 | ## ¿Cómo Contribuir? 6 | - Haz un fork del repositorio y crea tu rama a partir de `master`. 7 | - Sigue el estilo y convenciones de código del proyecto. 8 | - Escribe mensajes de commit claros y descriptivos. 9 | - Prueba tus cambios localmente antes de enviar un pull request. 10 | - Asegúrate de que tu código no introduzca vulnerabilidades de seguridad. 11 | 12 | ## Buenas Prácticas de Seguridad 13 | - Sanitiza todo input y output de usuario. Usa [DOMPurify](https://github.com/cure53/DOMPurify) para cualquier renderizado HTML. 14 | - Nunca subas secretos, API keys o credenciales al repositorio. 15 | - Valida y restringe los archivos subidos e importados. 16 | - Aplica el principio de mínimo privilegio en todo el código y configuración. 17 | 18 | ## Reporte de Problemas de Seguridad 19 | Si encuentras un problema de seguridad, repórtalo de forma privada a **writeup_builder@proton.me**. No abras un issue público. 20 | 21 | ## Divulgación Responsable 22 | - Da un tiempo razonable para que el equipo resuelva el problema antes de divulgarlo públicamente. 23 | - Proporciona pasos claros para reproducir la vulnerabilidad. 24 | 25 | ## Estándares de Comunidad 26 | - Sigue nuestro [Código de Conducta](./CODE_OF_CONDUCT_ES.md). 27 | - Sé respetuoso y colaborativo. 28 | 29 | ¡Agradecemos tu ayuda para mejorar y asegurar CTF Write-up Builder! -------------------------------------------------------------------------------- /GUIA_USUARIO.md: -------------------------------------------------------------------------------- 1 | # Guía de Usuario: CTF Write-up Builder 2 | 3 | ## ¿Qué es CTF Write-up Builder? 4 | 5 | CTF Write-up Builder es una aplicación web que te permite crear, organizar y exportar write-ups (resoluciones) de máquinas y retos de CTF de forma profesional, rápida y segura. 6 | Soporta español e inglés, y está diseñada para la comunidad de ciberseguridad. 7 | 8 | --- 9 | 10 | ## Índice 11 | 12 | 1. [Primeros pasos](#primeros-pasos) 13 | 2. [Estructura de la aplicación](#estructura-de-la-aplicación) 14 | 3. [Secciones principales y su uso](#secciones-principales-y-su-uso) 15 | 4. [Importar y exportar write-ups](#importar-y-exportar-write-ups) 16 | 5. [Uso de la IA (OpenAI/Gemini)](#uso-de-la-ia-openai-gemini) 17 | 6. [Personalización y plantillas](#personalización-y-plantillas) 18 | 7. [Gestión de idiomas](#gestión-de-idiomas) 19 | 8. [Consejos de seguridad y privacidad](#consejos-de-seguridad-y-privacidad) 20 | 9. [Preguntas frecuentes](#preguntas-frecuentes) 21 | 22 | --- 23 | 24 | ## Primeros pasos 25 | 26 | 1. **Accede a la app:** 27 | Abre la URL de la aplicación en tu navegador (por ejemplo, la de Vercel). 28 | 29 | 2. **Selecciona el idioma:** 30 | Puedes elegir entre español e inglés desde el menú superior o de configuración. 31 | 32 | 3. **Configura tu API Key (opcional):** 33 | Si quieres usar la IA para sugerencias automáticas, configura tu clave de OpenAI o Gemini en la sección de configuración. 34 | 35 | --- 36 | 37 | ## Estructura de la aplicación 38 | 39 | - **Barra superior:** 40 | Acceso a idioma, configuración, importación/exportación, y ayuda. 41 | 42 | - **Panel principal:** 43 | Aquí editas tu write-up, añades secciones, imágenes, y gestionas el contenido. 44 | 45 | - **Vista previa:** 46 | Puedes ver cómo quedará tu write-up final en tiempo real. 47 | 48 | --- 49 | 50 | ## Secciones principales y su uso 51 | 52 | ### 1. **Título y Metadatos** 53 | 54 | - **Título:** 55 | Escribe el nombre de la máquina o reto. Ejemplo: `HTB - Blue` 56 | - **Autor:** 57 | Tu nombre o alias. 58 | - **Fecha:** 59 | La fecha se genera automáticamente y **no se puede modificar**. Siempre refleja la fecha actual. 60 | - **Dificultad, sistema operativo:** 61 | Completa estos campos para un write-up profesional. 62 | 63 | ### 2. **Secciones del Write-up** 64 | 65 | La app incluye plantillas sugeridas (Reconocimiento, Explotación, Post-Explotación, etc.) y puedes añadir, eliminar o reordenar secciones a tu gusto. 66 | 67 | - **Añadir sección:** 68 | Haz clic en "Añadir sección" y elige un nombre. 69 | - **Editar sección:** 70 | Haz clic en el título o contenido para modificarlo. 71 | - **Reordenar secciones:** 72 | Arrastra las secciones de usuario para cambiar el orden (las plantillas fijas no se pueden mover). 73 | - **Eliminar sección:** 74 | Haz clic en el icono de papelera en la sección correspondiente. 75 | 76 | ### 3. **Imágenes y archivos** 77 | 78 | - **Añadir imagen:** 79 | **IMPORTANTE:** Solo puedes añadir imágenes a secciones que hayas editado o creado. Si no has modificado una plantilla sugerida o añadido una sección, no podrás subir imágenes ni modificar la estructura. 80 | - **Visualización:** 81 | Las imágenes se muestran en la vista previa y en la exportación. 82 | 83 | ### 4. **Notas y consejos** 84 | 85 | - Usa las secciones para documentar cada fase del reto. 86 | - Puedes copiar/pegar comandos, salidas de terminal, y resultados. 87 | 88 | --- 89 | 90 | ## Importar y exportar write-ups 91 | 92 | ### **Importar** 93 | 94 | - **Desde JSON:** 95 | Importa un write-up previamente exportado desde la app. 96 | - **Desde Markdown:** 97 | Importa write-ups en formato Markdown (.md). La app intentará mapear los encabezados y secciones automáticamente. 98 | - **Opciones de importación:** 99 | Puedes elegir entre fusionar con tu write-up actual o reemplazarlo completamente. 100 | 101 | ### **Exportar** 102 | 103 | - **A PDF:** 104 | Exporta tu write-up en formato PDF listo para compartir o entregar. 105 | - **A JSON:** 106 | Guarda tu progreso para continuar editando más tarde. 107 | - **A Markdown:** 108 | Exporta el contenido en formato Markdown para compartir en foros o repositorios. 109 | 110 | --- 111 | 112 | ## Uso de la IA (OpenAI/Gemini) 113 | 114 | - **Configura tu API Key:** 115 | Ve a configuración y añade tu clave de OpenAI o Gemini. 116 | - **Sugerencias automáticas:** 117 | En cada sección puedes pedir a la IA que te ayude a redactar, resumir o mejorar el texto. 118 | - **Privacidad:** 119 | Las claves se almacenan localmente y nunca se envían a servidores externos. 120 | 121 | --- 122 | 123 | ## Personalización y plantillas 124 | 125 | - **Secciones sugeridas:** 126 | Siempre tendrás plantillas de secciones recomendadas (Recon, Exploit, etc.). 127 | - **Crea tus propias plantillas:** 128 | Puedes añadir y guardar secciones personalizadas para futuros write-ups. 129 | 130 | --- 131 | 132 | ## Gestión de idiomas 133 | 134 | - **Cambia de idioma en cualquier momento:** 135 | El contenido de la interfaz y las plantillas se adaptan automáticamente. 136 | - **Advertencia:** 137 | Cambiar de idioma puede resetear el write-up actual (se muestra un aviso de confirmación). 138 | 139 | --- 140 | 141 | ## Consejos de seguridad y privacidad 142 | 143 | - **Todo el contenido se guarda localmente en tu navegador.** 144 | - **No se envía información a servidores externos, salvo que uses la IA.** 145 | - **Las claves API están protegidas y solo se usan en tu dispositivo.** 146 | - **No subas información sensible o privada si vas a compartir el write-up.** 147 | 148 | --- 149 | 150 | ## Preguntas frecuentes 151 | 152 | **¿Puedo usar la app sin conexión?** 153 | Sí, pero algunas funciones (como la IA) requieren internet. 154 | 155 | **¿Puedo compartir mis write-ups?** 156 | Sí, exporta a PDF, Markdown o JSON y compártelos como prefieras. 157 | 158 | **¿Qué pasa si cierro la app?** 159 | Tu progreso se guarda localmente, pero exporta regularmente para evitar pérdidas. 160 | 161 | **¿Puedo colaborar con otros?** 162 | Por ahora, la edición es individual, pero puedes compartir archivos exportados. 163 | 164 | --- 165 | 166 | ## Soporte y contacto 167 | 168 | - **¿Tienes dudas o sugerencias?** 169 | Consulta la documentación, revisa los archivos de ayuda, o contacta al equipo en: 170 | **writeup_builder@proton.me** 171 | 172 | --- 173 | 174 | ¿Necesitas la guía en inglés o un PDF/manual visual? ¡Dímelo y lo preparo! -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 🇪🇸 Leer en Español 3 |

4 | 5 | # CTF Write-up Builder Logo CTF Write-up Builder 6 | 7 | [![Next.js](https://img.shields.io/badge/Next.js-15.0-black?logo=next.js)](https://nextjs.org/) 8 | [![React](https://img.shields.io/badge/React-18.0-blue?logo=react)](https://reactjs.org/) 9 | [![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue?logo=typescript)](https://www.typescriptlang.org/) 10 | [![Security](https://img.shields.io/badge/Security-A+-green?logo=shield)](https://github.com/ilanami/ctf_writeup_builder) 11 | [![License](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 12 | 13 | > **A modern and private application for creating, organizing and exporting CTF write-ups with AI assistance** 14 | 15 | Designed by and for the cybersecurity community, CTF Write-up Builder allows you to document your CTFs professionally with Markdown support, automatic screenshots, AI content generation, and multiple export formats. 16 | 17 | ## 🌐 Live Demo 18 | 19 | **[🚀 Try the application here](https://ctf-writeup-builder.vercel.app/)** 20 | 21 | *No registration, no tracking, no limits. Your privacy is our priority.* 22 | 23 | ## 📸 Screenshots 24 | 25 | | Main View | AI Editor | Export | 26 | |-----------|-----------|---------| 27 | | ![Main View](docs/screenshots/main-view.png) | ![AI Editor](docs/screenshots/ai-editor.png) | ![Export](docs/screenshots/export.png) | 28 | 29 | ## ✨ Key Features 30 | 31 | ### 🤖 **Intelligent AI Generation** 32 | - **Google Gemini** and **OpenAI ChatGPT** integrated 33 | - Generate specific content for each section 34 | - Automated vulnerability analysis 35 | - Contextual tool suggestions 36 | 37 | ### 📝 **Advanced Editor** 38 | - Native **Markdown** with real-time preview 39 | - Integrated **screenshots** per section 40 | - Predefined **templates** for different CTF types 41 | - Customizable **tags** for organization 42 | 43 | ### 📄 **Multiple Export Formats** 44 | - Professional **PDF** with optimized formatting 45 | - Standard **Markdown** for GitHub/GitLab 46 | - **JSON** for backup and collaboration 47 | 48 | ### 🌐 **Multi-language** 49 | - Complete **Spanish** and **English** 50 | - Adaptive interface by region 51 | - Localized AI prompts 52 | 53 | ### 🛡️ **Privacy and Security** 54 | - **100% local** - No external servers 55 | - **Encrypted API keys** stored locally 56 | - **Open source** - Fully audited 57 | - **No tracking** or telemetry 58 | 59 | ### 📱 **User Experience** 60 | - **Responsive design** - Works on mobile and desktop 61 | - Professional **hacker theme** 62 | - **Auto-save** to prevent data loss 63 | 64 | ## 🚀 Installation 65 | 66 | ### Prerequisites 67 | - **Node.js** 18.0 or higher 68 | - **npm** or **yarn** 69 | 70 | ### Local Installation 71 | 72 | ```bash 73 | # Clone the repository 74 | git clone https://github.com/ilanami/ctf_writeup_builder.git 75 | 76 | # Navigate to directory 77 | cd ctf_writeup_builder 78 | 79 | # Install dependencies 80 | npm install 81 | 82 | # Run in development mode 83 | npm run dev 84 | ``` 85 | 86 | The application will be available at `http://localhost:3000` 87 | 88 | ### Production Build 89 | 90 | ```bash 91 | # Create optimized build 92 | npm run build 93 | 94 | # Run in production 95 | npm start 96 | ``` 97 | 98 | ## 💡 How to Use 99 | 100 | ### 1. **Configure AI (Optional)** 101 | - Click **"API Key"** in the top bar 102 | - Choose between **Google Gemini** or **OpenAI** 103 | - Enter your personal API key 104 | - [📖 How to get API Keys](#-api-configuration) 105 | 106 | ### 2. **Create Write-up** 107 | - Click **"New"** to start 108 | - Fill in basic information (title, difficulty, etc.) 109 | - Add sections according to your methodology 110 | 111 | ### 3. **Generate Content with AI** 112 | - In each section, click **"Generate with AI"** 113 | - Briefly describe what you found 114 | - AI will generate professional content 115 | 116 | ### 4. **Add Screenshots** 117 | - Use **"Add Screenshot"** in each section 118 | - Drag and drop images 119 | - Screenshots are automatically included in exports 120 | 121 | ### 5. **Export** 122 | - **PDF** for professional reports 123 | - **Markdown** for documentation 124 | - **JSON** for backup/collaboration 125 | 126 | ## 🔑 API Configuration 127 | 128 | ### Google Gemini (Recommended - Free) 129 | 1. Go to [Google AI Studio](https://makersuite.google.com/app/apikey) 130 | 2. Create a new API Key 131 | 3. Copy it to the app configuration 132 | 133 | ### OpenAI ChatGPT 134 | 1. Go to [OpenAI Platform](https://platform.openai.com/api-keys) 135 | 2. Create a new API Key (starts with `sk-`) 136 | 3. Copy it to the app configuration 137 | 138 | > 🔒 **Security**: Your API keys are stored encoded locally. They are never sent to external servers except to AI providers for content generation. 139 | 140 | ## 🏗️ Technical Architecture 141 | 142 | ### Technology Stack 143 | - **Frontend**: Next.js 15, React 18, TypeScript 144 | - **Styling**: Tailwind CSS, CSS Modules 145 | - **State**: React Context + useReducer 146 | - **AI**: Google Gemini & OpenAI APIs 147 | - **Security**: DOMPurify, Input sanitization 148 | - **Performance**: React.memo, useCallback optimizations 149 | 150 | ### Project Structure 151 | ``` 152 | src/ 153 | ├── app/ # Next.js App Router 154 | ├── components/ # Reusable React components 155 | ├── contexts/ # Global state (Context API) 156 | ├── utils/ # Utilities and helpers 157 | ├── ai/ # AI API integrations 158 | └── types/ # TypeScript definitions 159 | ``` 160 | 161 | ## 🛡️ Security 162 | 163 | This application has been fully audited for security: 164 | 165 | - ✅ **XSS Prevention** - DOMPurify on all dynamic HTML 166 | - ✅ **Input Sanitization** - Validation on all inputs 167 | - ✅ **API Security** - Keys encoded locally 168 | - ✅ **Dependency Audit** - No known vulnerabilities 169 | - ✅ **OWASP Compliance** - Best practices implemented 170 | 171 | See [SECURITY.md](SECURITY.md) for complete details. 172 | 173 | ## 🌍 Multi-language 174 | 175 | Supported languages: 176 | - 🇪🇸 **Español** (Spain/Latin America) 177 | - 🇺🇸 **English** (US/International) 178 | 179 | Want to add your language? [Contribute here](#-contributing) 180 | 181 | ## 📋 Roadmap 182 | 183 | ### v1.1 - Performance Plus 184 | - [ ] Complete lazy loading 185 | - [ ] Virtual scrolling for large lists 186 | - [ ] Bundle size optimization 187 | 188 | ### v1.2 - UX Enhancements 189 | - [ ] More CTF templates 190 | - [ ] Keyboard shortcuts 191 | - [ ] Drag & drop to reorganize sections 192 | 193 | ### v1.3 - Collaboration 194 | - [ ] Export to more formats (DOCX, HTML) 195 | - [ ] Git integration 196 | - [ ] Basic collaborative mode 197 | 198 | ### v1.4 - Advanced Features 199 | - [ ] Plugin system 200 | - [ ] Custom AI prompts 201 | - [ ] CTF platform integrations 202 | 203 | ## 🤝 Contributing 204 | 205 | Contributions are welcome! This application is made by and for the CTF community. 206 | 207 | ### Ways to Contribute 208 | - 🐛 **Report bugs** in [Issues](https://github.com/ilanami/ctf_writeup_builder/issues) 209 | - 💡 **Suggest features** 210 | - 🌍 **Translate** to new languages 211 | - 🔧 **Submit** Pull Requests 212 | - ⭐ **Star** the project 213 | 214 | ### Local Development 215 | ```bash 216 | # Fork the repository 217 | # Clone your fork 218 | git clone https://github.com/YOUR-USERNAME/ctf_writeup_builder.git 219 | 220 | # Create branch for your feature 221 | git checkout -b feature/new-functionality 222 | 223 | # Make changes and commit 224 | git commit -m "feat: add new functionality" 225 | 226 | # Push and create Pull Request 227 | git push origin feature/new-functionality 228 | ``` 229 | 230 | ## 🎁 Support my Projects and Tools 231 | 232 | If you've enjoyed my projects and tools and found them useful, consider buying me a coffee or making a donation as a thank you. 233 | 234 | It's not mandatory, but it would help me tremendously to continue creating tools like this and to pay for the cybersecurity certifications I want to obtain. 235 | 236 | Thank you very much for your support! 237 | 238 | [![PayPal](https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white)](https://www.paypal.me/1511amff) 239 | [![Buy Me A Coffee](https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black)](https://buymeacoffee.com/ilanami) 240 | 241 | ## 📞 Support and Contact 242 | 243 | ### 🐛 Report Issues 244 | - **GitHub Issues**: [Create new issue](https://github.com/ilanami/ctf_writeup_builder/issues/new) 245 | - **Email**: writeup_builder@proton.me 246 | 247 | ### 📧 Direct Contact 248 | For general inquiries, collaborations or proposals: 249 | **writeup_builder@proton.me** 250 | 251 | ## 📄 License 252 | 253 | This project is under the MIT license. See [LICENSE](LICENSE) for details. 254 | 255 | ## ⚠️ Note about Content Security Policy (CSP) and 'unsafe-eval' 256 | 257 | To ensure all features work correctly in development mode, the Content Security Policy (CSP) allows 'unsafe-eval' **only in development**. This is required by some dependencies for local development. 258 | 259 | **In production** (e.g., on Vercel), the CSP configuration **does not include** 'unsafe-eval', so the application is secure and compliant with platform security standards. 260 | 261 | **Do not modify the CSP to allow 'unsafe-eval' in production.** 262 | The current configuration manages this automatically depending on the environment. 263 | 264 | #### If you have issues with buttons or features not working in development: 265 | 266 | 1. Open the `next.config.mjs` file at the root of the project. 267 | 2. Make sure the CSP line looks like this: 268 | ```js 269 | { 270 | key: 'Content-Security-Policy', 271 | value: process.env.NODE_ENV === 'development' 272 | ? "default-src 'self'; img-src 'self' data: https:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; object-src 'none'; base-uri 'self';" 273 | : "default-src 'self'; img-src 'self' data: https:; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; base-uri 'self';" 274 | }, 275 | ``` 276 | 3. Save the file and restart the development server: 277 | ```bash 278 | npm run dev 279 | ``` 280 | 4. Reload the page in your browser with `Ctrl + F5`. 281 | 282 | **In production, 'unsafe-eval' is NOT allowed and the app is secure.** 283 | 284 | ## 🙏 Acknowledgments 285 | 286 | Thanks to all CTF players, open source contributors and the cybersecurity community that made this project possible. 287 | 288 | ### Technologies Used 289 | - [Next.js](https://nextjs.org/) - React Framework 290 | - [Tailwind CSS](https://tailwindcss.com/) - Styling 291 | - [Google Gemini](https://gemini.google.com/) - AI for generation 292 | - [OpenAI](https://openai.com/) - Alternative AI 293 | - [DOMPurify](https://github.com/cure53/DOMPurify) - XSS Sanitization 294 | 295 | --- 296 | 297 |
298 | 299 | **⭐ If this project helps you, consider giving it a star ⭐** 300 | 301 | **Made with ❤️ for the CTF community** 302 | 303 | [🚀 Try Application](https://ctf-writeup-builder.vercel.app) • [📖 Documentation](https://github.com/ilanami/ctf_writeup_builder/wiki) • [🐛 Report Bug](https://github.com/ilanami/ctf_writeup_builder/issues) 304 | 305 |
306 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | We support the latest major and minor versions of CTF Write-up Builder. Please ensure you are using the most recent release for the best security and features. 5 | 6 | | Version | Supported | 7 | | ------- | ----------------- | 8 | | Latest | :white_check_mark:| 9 | | Older | :x: | 10 | 11 | ## Reporting a Vulnerability 12 | If you discover a security vulnerability, please report it responsibly by emailing: **writeup_builder@proton.me** 13 | 14 | - Do **not** create public GitHub issues for security problems. 15 | - We aim to respond within 48 hours and resolve critical issues as soon as possible. 16 | 17 | ## Security Best Practices 18 | - All user input is sanitized using [DOMPurify](https://github.com/cure53/DOMPurify) to prevent XSS. 19 | - The app is 100% client-side; no sensitive data is sent to external servers (except for AI API requests, which go directly to the provider). 20 | - API keys are stored locally in Base64 (not secure encryption, but never sent to our servers). 21 | - We recommend using unique API keys and never sharing them. 22 | - Always keep your browser and OS updated. 23 | 24 | ## Responsible Disclosure Policy 25 | We encourage responsible disclosure of security issues. Please: 26 | - Report vulnerabilities privately to the email above. 27 | - Allow us reasonable time to investigate and fix before public disclosure. 28 | - Provide clear steps to reproduce the issue. 29 | 30 | ## Security Context 31 | - The app has been security-audited and maintains an A/A+ rating on securityheaders.com. 32 | - No XSS vulnerabilities are present; all Markdown and HTML rendering is sanitized. 33 | - No hardcoded secrets or credentials in the codebase. 34 | - CORS and security headers are enforced in production. 35 | 36 | Thank you for helping keep CTF Write-up Builder secure for the global CTF community! -------------------------------------------------------------------------------- /SECURITY_ES.md: -------------------------------------------------------------------------------- 1 | # Política de Seguridad 2 | 3 | ## Versiones Soportadas 4 | Solo se brinda soporte a la última versión principal y secundaria de CTF Write-up Builder. Por favor, utiliza siempre la versión más reciente para garantizar la mejor seguridad y experiencia. 5 | 6 | | Versión | Soportada | 7 | | ------- | ------------------- | 8 | | Última | :white_check_mark: | 9 | | Anteriores | :x: | 10 | 11 | ## Reporte de Vulnerabilidades 12 | Si detectas una vulnerabilidad de seguridad, repórtala de forma responsable escribiendo a: **writeup_builder@proton.me** 13 | 14 | - **No** crees issues públicos en GitHub para problemas de seguridad. 15 | - Nos comprometemos a responder en un plazo de 48 horas y resolver los problemas críticos lo antes posible. 16 | 17 | ## Buenas Prácticas de Seguridad 18 | - Todo input de usuario se sanitiza usando [DOMPurify](https://github.com/cure53/DOMPurify) para prevenir XSS. 19 | - La app es 100% cliente; ningún dato sensible se envía a servidores externos (excepto solicitudes de IA, que van directo al proveedor). 20 | - Las API keys se almacenan localmente en Base64 (no es cifrado seguro, pero nunca se envían a nuestros servidores). 21 | - Recomendamos usar API keys únicas y no compartirlas. 22 | - Mantén tu navegador y sistema operativo actualizados. 23 | 24 | ## Política de Divulgación Responsable 25 | Fomentamos la divulgación responsable de vulnerabilidades: 26 | - Reporta vulnerabilidades de forma privada al correo indicado. 27 | - Permite un tiempo razonable para investigar y corregir antes de divulgar públicamente. 28 | - Proporciona pasos claros para reproducir el problema. 29 | 30 | ## Contexto de Seguridad 31 | - La app ha sido auditada y mantiene una calificación A/A+ en securityheaders.com. 32 | - No existen vulnerabilidades XSS; todo el renderizado de Markdown y HTML es seguro. 33 | - No hay secretos ni credenciales hardcodeados en el código. 34 | - CORS y headers de seguridad están activos en producción. 35 | 36 | ¡Gracias por ayudar a mantener CTF Write-up Builder seguro para la comunidad CTF hispanohablante! -------------------------------------------------------------------------------- /USER_GUIDE.md: -------------------------------------------------------------------------------- 1 | # User Guide: CTF Write-up Builder 2 | 3 | ## What is CTF Write-up Builder? 4 | 5 | CTF Write-up Builder is a web application that allows you to create, organize, and export write-ups (solutions) for CTF machines and challenges in a professional, fast, and secure way. 6 | It supports both English and Spanish, and is designed for the cybersecurity community. 7 | 8 | --- 9 | 10 | ## Table of Contents 11 | 12 | 1. [Getting Started](#getting-started) 13 | 2. [Application Structure](#application-structure) 14 | 3. [Main Sections and Usage](#main-sections-and-usage) 15 | 4. [Import and Export Write-ups](#import-and-export-write-ups) 16 | 5. [Using AI (OpenAI/Gemini)](#using-ai-openai-gemini) 17 | 6. [Customization and Templates](#customization-and-templates) 18 | 7. [Language Management](#language-management) 19 | 8. [Security and Privacy Tips](#security-and-privacy-tips) 20 | 9. [Frequently Asked Questions](#frequently-asked-questions) 21 | 22 | --- 23 | 24 | ## Getting Started 25 | 26 | 1. **Access the app:** 27 | Open the application URL in your browser (e.g., the Vercel deployment). 28 | 29 | 2. **Select language:** 30 | Choose between English and Spanish from the top menu or settings. 31 | 32 | 3. **Configure API Key (optional):** 33 | If you want to use AI for automatic suggestions, configure your OpenAI or Gemini key in the settings section. 34 | 35 | --- 36 | 37 | ## Application Structure 38 | 39 | - **Top Bar:** 40 | Access to language, settings, import/export, and help. 41 | 42 | - **Main Panel:** 43 | Here you edit your write-up, add sections, images, and manage content. 44 | 45 | - **Preview:** 46 | You can see how your final write-up will look in real-time. 47 | 48 | --- 49 | 50 | ## Main Sections and Usage 51 | 52 | ### 1. **Title and Metadata** 53 | 54 | - **Title:** 55 | Write the name of the machine or challenge. Example: `HTB - Blue` 56 | - **Author:** 57 | Your name or alias. 58 | - **Date:** 59 | The date is automatically generated and **cannot be modified**. It always reflects the current date. 60 | - **Difficulty, Operating System:** 61 | Complete these fields for a professional write-up. 62 | 63 | ### 2. **Write-up Sections** 64 | 65 | The app includes suggested templates (Reconnaissance, Exploitation, Post-Exploitation, etc.) and you can add, delete, or reorder sections as needed. 66 | 67 | - **Add Section:** 68 | Click "Add Section" and choose a name. 69 | - **Edit Section:** 70 | Click on the title or content to modify it. 71 | - **Reorder Sections:** 72 | Drag user sections to change their order (fixed templates cannot be moved). 73 | - **Delete Section:** 74 | Click the trash icon in the corresponding section. 75 | 76 | ### 3. **Images and Files** 77 | 78 | - **Add Image:** 79 | **IMPORTANT:** You can only add images to sections that you have edited or created. If you haven't modified a suggested template or added a section, you won't be able to upload images or modify the structure. 80 | - **Visualization:** 81 | Images are displayed in the preview and export. 82 | 83 | ### 4. **Notes and Tips** 84 | 85 | - Use sections to document each phase of the challenge. 86 | - You can copy/paste commands, terminal outputs, and results. 87 | 88 | --- 89 | 90 | ## Import and Export Write-ups 91 | 92 | ### **Import** 93 | 94 | - **From JSON:** 95 | Import a previously exported write-up from the app. 96 | - **From Markdown:** 97 | Import write-ups in Markdown format (.md). The app will try to map headers and sections automatically. 98 | - **Import Options:** 99 | You can choose to merge with your current write-up or replace it completely. 100 | 101 | ### **Export** 102 | 103 | - **To PDF:** 104 | Export your write-up in PDF format ready to share or submit. 105 | - **To JSON:** 106 | Save your progress to continue editing later. 107 | - **To Markdown:** 108 | Export content in Markdown format to share in forums or repositories. 109 | 110 | --- 111 | 112 | ## Using AI (OpenAI/Gemini) 113 | 114 | - **Configure API Key:** 115 | Go to settings and add your OpenAI or Gemini key. 116 | - **Automatic Suggestions:** 117 | In each section, you can ask the AI to help you write, summarize, or improve the text. 118 | - **Privacy:** 119 | Keys are stored locally and are never sent to external servers. 120 | 121 | --- 122 | 123 | ## Customization and Templates 124 | 125 | - **Suggested Sections:** 126 | You'll always have recommended section templates (Recon, Exploit, etc.). 127 | - **Create Your Own Templates:** 128 | You can add and save custom sections for future write-ups. 129 | 130 | --- 131 | 132 | ## Language Management 133 | 134 | - **Change Language Anytime:** 135 | Interface content and templates adapt automatically. 136 | - **Warning:** 137 | Changing language may reset the current write-up (a confirmation warning is shown). 138 | 139 | --- 140 | 141 | ## Security and Privacy Tips 142 | 143 | - **All content is saved locally in your browser.** 144 | - **No information is sent to external servers, except when using AI.** 145 | - **API keys are protected and only used on your device.** 146 | - **Don't upload sensitive or private information if you plan to share the write-up.** 147 | 148 | --- 149 | 150 | ## Frequently Asked Questions 151 | 152 | **Can I use the app offline?** 153 | Yes, but some features (like AI) require internet. 154 | 155 | **Can I share my write-ups?** 156 | Yes, export to PDF, Markdown, or JSON and share them as you prefer. 157 | 158 | **What happens if I close the app?** 159 | Your progress is saved locally, but export regularly to avoid losses. 160 | 161 | **Can I collaborate with others?** 162 | For now, editing is individual, but you can share exported files. 163 | 164 | --- 165 | 166 | ## Support and Contact 167 | 168 | - **Questions or suggestions?** 169 | Check the documentation, review help files, or contact the team at: 170 | **writeup_builder@proton.me** 171 | 172 | --- 173 | 174 | Need the guide in Spanish or a PDF/visual manual? Just let me know! -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /ctf_writeup_builder-main.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ] 7 | } -------------------------------------------------------------------------------- /docs/blueprint.md: -------------------------------------------------------------------------------- 1 | # **App Name**: WriteUp Terminal 2 | 3 | ## Core Features: 4 | 5 | - Write-up editor: A terminal-inspired interface that enables the user to write and structure CTF write-ups. The app will allow import and export of JSON files and data will persist locally. 6 | - Customizable Sections: User can customize write-up sections. Add/remove/reorder sections to adapt the editor to specific challenges. 7 | - Live Preview: Preview the generated write-up as it is built. 8 | - Export: Allows exporting to a PDF for wider distribution, or as markdown. 9 | 10 | ## Style Guidelines: 11 | 12 | - Dark background (#0A0A0A) to simulate a terminal interface. 13 | - Primary color: Neon green (#00FF00) for text to maintain the hacker aesthetic. Should only be used for key pieces of information or for short highlights. 14 | - Accent color: Cyan (#00FFFF) to highlight interactive elements, buttons, and important UI affordances. 15 | - Use monospace fonts to mimic terminal output. 16 | - Simple line icons for actions and section types, complementing the terminal aesthetic. 17 | - A layout with a sidebar for general information and settings, a central panel for the write-up sections, and a preview panel. -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilanami/ctf_writeup_builder/09d0939aed968f56dcd6fd568b16d7560ebd947a/docs/logo.png -------------------------------------------------------------------------------- /docs/screenshots/ai-editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilanami/ctf_writeup_builder/09d0939aed968f56dcd6fd568b16d7560ebd947a/docs/screenshots/ai-editor.png -------------------------------------------------------------------------------- /docs/screenshots/export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilanami/ctf_writeup_builder/09d0939aed968f56dcd6fd568b16d7560ebd947a/docs/screenshots/export.png -------------------------------------------------------------------------------- /docs/screenshots/main-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilanami/ctf_writeup_builder/09d0939aed968f56dcd6fd568b16d7560ebd947a/docs/screenshots/main-view.png -------------------------------------------------------------------------------- /favicon.ico.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilanami/ctf_writeup_builder/09d0939aed968f56dcd6fd568b16d7560ebd947a/favicon.ico.png -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | async headers() { 4 | const isDev = process.env.NODE_ENV === 'development'; 5 | 6 | if (isDev) { 7 | // Desarrollo: CSP muy permisiva para que todo funcione 8 | console.log('🔧 Desarrollo: CSP permisiva'); 9 | return [ 10 | { 11 | source: '/(.*)', 12 | headers: [ 13 | { 14 | key: 'Content-Security-Policy', 15 | value: [ 16 | "default-src 'self'", 17 | "script-src 'self' 'unsafe-inline' 'unsafe-eval'", 18 | "style-src 'self' 'unsafe-inline'", 19 | "img-src 'self' data: https: blob:", 20 | "connect-src 'self' https:", 21 | "font-src 'self' data:", 22 | "object-src 'none'", 23 | "media-src 'self'", 24 | "frame-src 'self'", 25 | "worker-src 'self' blob:", 26 | "child-src 'self' blob:", 27 | "base-uri 'self'" 28 | ].join('; ') 29 | } 30 | ] 31 | } 32 | ]; 33 | } 34 | 35 | // Producción: CSP A+ pero funcional 36 | console.log('🔒 Producción: CSP A+ funcional'); 37 | return [ 38 | { 39 | source: '/(.*)', 40 | headers: [ 41 | { 42 | key: 'Content-Security-Policy', 43 | value: [ 44 | "default-src 'self'", 45 | // CRÍTICO: Permitir Next.js chunks y React 46 | "script-src 'self' 'unsafe-inline'", // Necesario para Next.js 47 | "style-src 'self' 'unsafe-inline'", // Necesario para Tailwind 48 | "img-src 'self' data: https: blob:", 49 | "connect-src 'self' https:", // Para API calls 50 | "font-src 'self' data:", 51 | "object-src 'none'", 52 | "media-src 'self'", 53 | "frame-src 'self'", 54 | "worker-src 'self' blob:", // Para Web Workers si los usas 55 | "child-src 'self' blob:", 56 | "base-uri 'self'", 57 | "form-action 'self'", 58 | "frame-ancestors 'none'" 59 | ].join('; ') 60 | }, 61 | // Headers A+ que SÍ funcionan 62 | { 63 | key: 'X-Frame-Options', 64 | value: 'DENY' 65 | }, 66 | { 67 | key: 'X-Content-Type-Options', 68 | value: 'nosniff' 69 | }, 70 | { 71 | key: 'Referrer-Policy', 72 | value: 'strict-origin-when-cross-origin' 73 | }, 74 | { 75 | key: 'Permissions-Policy', 76 | value: 'camera=(), microphone=(), geolocation=()' 77 | } 78 | // REMOVIDOS temporalmente los que causan problemas: 79 | // - Cross-Origin-Embedder-Policy 80 | // - Cross-Origin-Opener-Policy 81 | // - Strict-Transport-Security (Vercel lo maneja) 82 | ] 83 | } 84 | ]; 85 | }, 86 | 87 | poweredByHeader: false, 88 | 89 | experimental: { 90 | serverActions: { 91 | bodySizeLimit: '2mb' 92 | } 93 | } 94 | }; 95 | 96 | export default nextConfig; 97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextn", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev -p 3000", 7 | "genkit:dev": "genkit start -- tsx src/ai/dev.ts", 8 | "genkit:watch": "genkit start -- tsx --watch src/ai/dev.ts", 9 | "build": "next build", 10 | "start": "next start", 11 | "lint": "next lint", 12 | "typecheck": "tsc --noEmit" 13 | }, 14 | "dependencies": { 15 | "@genkit-ai/googleai": "^1.8.0", 16 | "@radix-ui/react-accordion": "^1.2.3", 17 | "@radix-ui/react-alert-dialog": "^1.1.6", 18 | "@radix-ui/react-avatar": "^1.1.3", 19 | "@radix-ui/react-checkbox": "^1.1.4", 20 | "@radix-ui/react-dialog": "^1.1.6", 21 | "@radix-ui/react-dropdown-menu": "^2.1.6", 22 | "@radix-ui/react-label": "^2.1.2", 23 | "@radix-ui/react-menubar": "^1.1.6", 24 | "@radix-ui/react-popover": "^1.1.6", 25 | "@radix-ui/react-progress": "^1.1.2", 26 | "@radix-ui/react-radio-group": "^1.2.3", 27 | "@radix-ui/react-scroll-area": "^1.2.3", 28 | "@radix-ui/react-select": "^2.1.6", 29 | "@radix-ui/react-separator": "^1.1.2", 30 | "@radix-ui/react-slider": "^1.2.3", 31 | "@radix-ui/react-slot": "^1.1.2", 32 | "@radix-ui/react-switch": "^1.1.3", 33 | "@radix-ui/react-tabs": "^1.1.3", 34 | "@radix-ui/react-toast": "^1.2.6", 35 | "@radix-ui/react-tooltip": "^1.1.8", 36 | "@tiptap/extension-link": "^2.12.0", 37 | "@tiptap/extension-placeholder": "^2.12.0", 38 | "@tiptap/extension-text-align": "^2.12.0", 39 | "@tiptap/extension-underline": "^2.12.0", 40 | "@tiptap/react": "^2.12.0", 41 | "@tiptap/starter-kit": "^2.12.0", 42 | "@types/dompurify": "^3.0.5", 43 | "@vercel/analytics": "^1.5.0", 44 | "class-variance-authority": "^0.7.1", 45 | "clsx": "^2.1.1", 46 | "date-fns": "^3.6.0", 47 | "dompurify": "^3.2.6", 48 | "geist": "^1.3.0", 49 | "genkit": "^1.8.0", 50 | "lucide-react": "^0.475.0", 51 | "marked": "^13.0.2", 52 | "next": "^15.3.2", 53 | "next-international": "^1.2.5", 54 | "pdfjs-dist": "^4.10.38", 55 | "react": "^18.3.1", 56 | "react-beautiful-dnd": "^13.1.1", 57 | "react-day-picker": "^8.10.1", 58 | "react-dom": "^18.3.1", 59 | "react-hook-form": "^7.54.2", 60 | "react-markdown": "^9.0.1", 61 | "recharts": "^2.15.1", 62 | "remark-gfm": "^4.0.0", 63 | "tailwind-merge": "^3.0.1", 64 | "tailwindcss-animate": "^1.0.7", 65 | "uuid": "^10.0.0" 66 | }, 67 | "devDependencies": { 68 | "@types/marked": "^6.0.0", 69 | "@types/react": "^18", 70 | "@types/react-dom": "^18", 71 | "@types/uuid": "^10.0.0", 72 | "genkit-cli": "^1.8.0", 73 | "tailwindcss": "^3.4.1", 74 | "typescript": "^5" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /src/ai/dev.ts: -------------------------------------------------------------------------------- 1 | // Flows will be imported for their side effects in this file. 2 | import '@/ai/flows/section-suggester-flow'; 3 | -------------------------------------------------------------------------------- /src/ai/flows/pdf-processor-flow.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | /** 3 | * @fileOverview AI flow to process a PDF and extract structured content. 4 | * 5 | * - processPdf - Extracts structured sections (title, content as Markdown) from a PDF. 6 | * - PdfInput - Input type for the flow (PDF data URI). 7 | * - AiPdfParseOutput - Output type for the flow (array of parsed sections). 8 | */ 9 | 10 | import { ai } from '@/ai/genkit'; 11 | import { z } from 'genkit'; 12 | 13 | export const PdfInputSchema = z.object({ 14 | pdfDataUri: z 15 | .string() 16 | .describe( 17 | "The PDF file content as a data URI. Expected format: 'data:application/pdf;base64,'." 18 | ), 19 | }); 20 | export type PdfInput = z.infer; 21 | 22 | const AiProcessedSectionSchema = z.object({ 23 | title: z.string().describe('A concise title for the identified section.'), 24 | content: z.string().describe("The Markdown content for this section. IMPORTANT: If images were present in this section of the PDF, you MUST insert a placeholder in the Markdown text exactly where the image appeared. The placeholder format is: \"[IMAGEN: Provide a 1-2 sentence, human-readable description of the image's content and its relevance to the section. e.g., 'Nmap scan results showing open ports.' or 'Login page screenshot with SQL injection payload.']\". Do not attempt to reproduce or describe the image data itself, only insert this placeholder string."), 25 | }); 26 | 27 | export const AiPdfParseOutputSchema = z.object({ 28 | parsed_sections: z 29 | .array(AiProcessedSectionSchema) 30 | .describe('An array of sections extracted and structured from the PDF by the AI.'), 31 | }); 32 | export type AiPdfParseOutput = z.infer; 33 | 34 | 35 | export async function processPdf(input: PdfInput): Promise { 36 | if (!input.pdfDataUri) { 37 | throw new Error('PDF data URI is required for AI processing.'); 38 | } 39 | return pdfProcessorFlow(input); 40 | } 41 | 42 | const pdfProcessingPrompt = ai.definePrompt({ 43 | name: 'pdfProcessingPrompt', 44 | input: { schema: PdfInputSchema }, 45 | output: { schema: AiPdfParseOutputSchema }, 46 | prompt: `You are an expert in analyzing technical documents, especially cybersecurity CTF (Capture The Flag) write-ups. 47 | Your task is to process the provided PDF content and extract its structure into logical sections. 48 | 49 | For the PDF provided via the 'pdfDataUri' input: 50 | 1. Identify the main sections or logical parts of the document. These could be chapters, major headings, or distinct topics. 51 | 2. For each section you identify: 52 | a. Extract or formulate a concise and descriptive title for that section. 53 | b. Extract the relevant textual content for that section and format it as well-structured Markdown. Ensure code blocks, lists, and other formatting are preserved or appropriately converted to Markdown. 54 | c. **Crucially**: If you encounter any images within a section of the PDF, you MUST insert a placeholder string in the Markdown content *exactly where the image appeared*. The placeholder format is: "[IMAGEN: Provide a 1-2 sentence, human-readable description of the image's content and its relevance or context within the section. For example: '[IMAGEN: Nmap scan results showing open ports 22, 80, and 443 on the target IP.]' or '[IMAGEN: Screenshot of the web application's dashboard after successful login.]']". Do NOT attempt to describe the image in prose outside this placeholder. Do NOT try to reproduce the image data itself. 55 | 3. Return the extracted information as a JSON object matching the output schema, containing an array called "parsed_sections". Each element in this array should be an object with "title" and "content" fields. 56 | 57 | If the PDF is unparsable, contains no discernible text, or is not a document format you can understand, return an empty "parsed_sections" array. 58 | Do not invent content. Only extract and structure what is present in the PDF. 59 | Focus on clear, well-formatted Markdown output for the content of each section. 60 | `, 61 | }); 62 | 63 | const pdfProcessorFlow = ai.defineFlow( 64 | { 65 | name: 'pdfProcessorFlow', 66 | inputSchema: PdfInputSchema, 67 | outputSchema: AiPdfParseOutputSchema, 68 | }, 69 | async (input) => { 70 | try { 71 | const { output } = await pdfProcessingPrompt(input); 72 | if (!output) { 73 | console.warn('AI PDF processing returned no output, returning empty sections.'); 74 | return { parsed_sections: [] }; 75 | } 76 | // Ensure parsed_sections is always an array, even if the AI fails to structure it correctly 77 | return { parsed_sections: Array.isArray(output.parsed_sections) ? output.parsed_sections : [] }; 78 | } catch (error) { 79 | console.error('Error in pdfProcessorFlow during AI prompt execution:', error); 80 | // Return empty sections on error to allow fallback to pdfjs-dist 81 | return { parsed_sections: [] }; 82 | } 83 | } 84 | ); 85 | -------------------------------------------------------------------------------- /src/ai/flows/section-suggester-flow.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | /** 3 | * @fileOverview AI flow to suggest content for a CTF write-up section. 4 | * 5 | * - suggestSectionContent - Generates Markdown content for a given section title, type, and user prompt. 6 | * - SuggestSectionContentInput - Input type for the flow. 7 | * - SuggestSectionContentOutput - Output type for the flow. 8 | */ 9 | 10 | import { ai } from '@/ai/genkit'; 11 | import type { SectionType } from '@/lib/types'; 12 | import { z } from 'genkit'; 13 | 14 | const SuggestSectionContentInputSchema = z.object({ 15 | sectionTitle: z.string().describe('The title of the section for which to generate content.'), 16 | sectionType: z.custom().describe('The type of the section (e.g., paso, pregunta, flag, notas).'), 17 | userPrompt: z.string().optional().describe('An optional user-provided prompt or keywords to guide content generation.'), 18 | }); 19 | export type SuggestSectionContentInput = z.infer; 20 | 21 | const SuggestSectionContentOutputSchema = z.object({ 22 | suggestedContent: z.string().describe('The AI-generated Markdown content for the section.'), 23 | }); 24 | export type SuggestSectionContentOutput = z.infer; 25 | 26 | export async function suggestSectionContent(input: SuggestSectionContentInput): Promise { 27 | // Basic validation 28 | if (!input.sectionTitle || input.sectionTitle.trim() === "") { 29 | throw new Error("Section title cannot be empty for AI generation."); 30 | } 31 | return sectionSuggesterFlow(input); 32 | } 33 | 34 | const sectionContentPrompt = ai.definePrompt({ 35 | name: 'sectionContentPrompt', 36 | input: { schema: SuggestSectionContentInputSchema }, 37 | output: { schema: SuggestSectionContentOutputSchema }, 38 | prompt: `You are an expert cybersecurity Capture The Flag (CTF) player and technical writer. 39 | Your task is to generate initial Markdown content for a specific section of a CTF write-up. 40 | 41 | Section Title: {{{sectionTitle}}} 42 | Section Type: {{{sectionType}}} 43 | {{#if userPrompt}}User's Focus/Keywords for this section: {{{userPrompt}}}{{/if}} 44 | 45 | Based on this information, provide a comprehensive and well-formatted Markdown draft for this section. 46 | - If the section type is 'paso' (step), describe common actions, tools, or commands relevant to the title and prompt. 47 | - If it's 'pregunta' (question), formulate a relevant question based on the title/prompt and provide a common or example answer. 48 | - If it's 'flag', describe how a flag might be typically found, formatted, or what it might represent in the context of the title/prompt. 49 | - If it's 'notas' (notes), provide general observations, tips, or further research points related to the title/prompt. 50 | 51 | Use Markdown effectively: 52 | - Employ headings (e.g., ## Sub-heading) if appropriate for structure. 53 | - Use code blocks (e.g., \`\`\`bash ... \`\`\`) for commands or code snippets. 54 | - Use lists (bulleted or numbered) for steps or enumerated items. 55 | - Emphasize key terms using **bold** or *italics*. 56 | 57 | Be concise but informative. Aim for a helpful starting point that the user can then elaborate on. 58 | 59 | Generated Markdown Content: 60 | `, 61 | }); 62 | 63 | const sectionSuggesterFlow = ai.defineFlow( 64 | { 65 | name: 'sectionSuggesterFlow', 66 | inputSchema: SuggestSectionContentInputSchema, 67 | outputSchema: SuggestSectionContentOutputSchema, 68 | }, 69 | async (input) => { 70 | const { output } = await sectionContentPrompt(input); 71 | if (!output) { 72 | // This case should ideally be handled by Genkit if the model fails to produce output matching the schema. 73 | // However, as a fallback: 74 | throw new Error('AI did not return an output matching the expected schema.'); 75 | } 76 | return output; 77 | } 78 | ); 79 | -------------------------------------------------------------------------------- /src/ai/genkit.ts: -------------------------------------------------------------------------------- 1 | import {genkit} from 'genkit'; 2 | import {googleAI} from '@genkit-ai/googleai'; 3 | 4 | export const ai = genkit({ 5 | plugins: [googleAI()], 6 | model: 'googleai/gemini-1.5-pro', 7 | }); 8 | -------------------------------------------------------------------------------- /src/app/[locale]/layout.tsx: -------------------------------------------------------------------------------- 1 | // src/app/[locale]/layout.tsx 2 | import type { Metadata } from 'next'; 3 | import { GeistSans } from 'geist/font/sans'; 4 | import { GeistMono } from 'geist/font/mono'; 5 | import '../globals.css'; 6 | import { Toaster } from "@/components/ui/toaster"; 7 | import { I18nProviderClient } from '@/locales/client'; 8 | import type { ReactNode } from 'react'; 9 | import { Analytics } from '@vercel/analytics/react'; 10 | 11 | export const metadata: Metadata = { 12 | title: 'CTF Write-up Builder >_', 13 | description: 'CTF Write-up Builder - Create, manage, and export CTF reports with a hacker aesthetic.', 14 | }; 15 | 16 | 17 | export default async function RootLayout({ 18 | children, 19 | params 20 | }: Readonly<{ 21 | children: ReactNode; 22 | params: Promise<{ locale: string }>; 23 | }>) { 24 | const { locale } = await params; 25 | console.log(`RootLayout rendering with locale: ${locale}`); 26 | return ( 27 | 28 | 29 | 30 | {/* Eliminado script inline para máxima seguridad CSP */} 31 | 32 | 33 | {children} 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | } 41 | 42 | export async function generateStaticParams() { 43 | return [{ locale: 'en' }, { locale: 'es' }]; 44 | } 45 | -------------------------------------------------------------------------------- /src/app/[locale]/page.tsx: -------------------------------------------------------------------------------- 1 | // src/app/[locale]/page.tsx 2 | import { WriteUpProvider } from '@/contexts/WriteUpContext'; 3 | import { AppLayout } from '@/components/AppLayout'; 4 | 5 | export default async function HomePage({ params }: { params: Promise<{ locale: string }> }) { 6 | const { locale } = await params; 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/app/api/ai-generate/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | 3 | export async function POST(request: NextRequest) { 4 | try { 5 | const { prompt, provider, apiKey } = await request.json(); 6 | 7 | if (!prompt || !provider || !apiKey) { 8 | return NextResponse.json( 9 | { error: 'Faltan parámetros requeridos' }, 10 | { status: 400 } 11 | ); 12 | } 13 | 14 | let response: Response; 15 | let result: string = ''; 16 | 17 | if (provider === 'openai') { 18 | response = await fetch('https://api.openai.com/v1/chat/completions', { 19 | method: 'POST', 20 | headers: { 21 | 'Authorization': `Bearer ${apiKey}`, 22 | 'Content-Type': 'application/json', 23 | }, 24 | body: JSON.stringify({ 25 | model: 'gpt-3.5-turbo', 26 | messages: [{ role: 'user', content: prompt }], 27 | max_tokens: 1000, 28 | }), 29 | }); 30 | 31 | if (!response.ok) { 32 | const errorData = await response.json(); 33 | throw new Error(`OpenAI API error: ${errorData.error?.message || 'Unknown error'}`); 34 | } 35 | 36 | const data = await response.json(); 37 | result = data.choices[0]?.message?.content || ''; 38 | 39 | } else if (provider === 'gemini') { 40 | response = await fetch( 41 | `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${apiKey}`, 42 | { 43 | method: 'POST', 44 | headers: { 45 | 'Content-Type': 'application/json', 46 | }, 47 | body: JSON.stringify({ 48 | contents: [{ parts: [{ text: prompt }] }], 49 | }), 50 | } 51 | ); 52 | 53 | if (!response.ok) { 54 | const errorData = await response.json(); 55 | throw new Error(`Gemini API error: ${errorData.error?.message || 'Unknown error'}`); 56 | } 57 | 58 | const data = await response.json(); 59 | result = data.candidates?.[0]?.content?.parts?.[0]?.text || ''; 60 | } 61 | 62 | return NextResponse.json({ content: result }); 63 | 64 | } catch (error: any) { 65 | console.error('AI Generation Error:', error); 66 | return NextResponse.json( 67 | { error: `No se pudo generar contenido: ${error.message}` }, 68 | { status: 500 } 69 | ); 70 | } 71 | } -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilanami/ctf_writeup_builder/09d0939aed968f56dcd6fd568b16d7560ebd947a/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 4%; /* #0a0a0a */ 8 | --foreground: 120 100% 50%; /* #00ff00 Neon Green */ 9 | 10 | --muted: 0 0% 14%; /* Darker gray for muted elements */ 11 | --muted-foreground: 120 100% 35%; /* Dimmer Neon Green for muted text */ 12 | 13 | --popover: 0 0% 6%; /* Slightly lighter than background for popovers */ 14 | --popover-foreground: 120 100% 50%; /* Neon Green for popover text */ 15 | 16 | --card: 0 0% 6%; /* Slightly lighter than background for cards */ 17 | --card-foreground: 120 100% 50%; /* Neon Green for card text */ 18 | 19 | --border: 150 50% 20%; /* Dark Green/Cyanish border */ 20 | --input: 0 0% 10%; /* Background for input fields */ 21 | 22 | --primary: 180 100% 50%; /* #00ffff Cyan for primary actions/buttons */ 23 | --primary-foreground: 0 0% 0%; /* Black for text on primary elements for contrast */ 24 | 25 | --secondary: 0 0% 12%; /* Dark gray for secondary elements */ 26 | --secondary-foreground: 120 100% 45%; /* Slightly dimmer Neon Green for secondary text */ 27 | 28 | --accent: 180 100% 60%; /* Lighter Cyan for accents like hover/focus */ 29 | --accent-foreground: 0 0% 0%; /* Black for text on accent elements */ 30 | 31 | --destructive: 0 84.2% 60.2%; /* Default red, can be themed later */ 32 | --destructive-foreground: 0 0% 98%; 33 | 34 | --ring: 180 100% 50%; /* Cyan for focus rings */ 35 | 36 | --radius: 0.5rem; 37 | 38 | /* Chart colors (can be adjusted for hacker theme if charts are used) */ 39 | --chart-1: 120 100% 50%; /* Neon Green */ 40 | --chart-2: 180 100% 50%; /* Cyan */ 41 | --chart-3: 210 100% 50%; /* Blue */ 42 | --chart-4: 60 100% 50%; /* Yellow */ 43 | --chart-5: 300 100% 50%; /* Magenta */ 44 | 45 | /* Sidebar specific theme variables (will be less used with new layout) */ 46 | --sidebar-background: 0 0% 6%; /* Dark background for sidebar */ 47 | --sidebar-foreground: 120 100% 50%; /* Neon Green text in sidebar */ 48 | --sidebar-primary: 180 100% 50%; /* Cyan for active/primary items in sidebar */ 49 | --sidebar-primary-foreground: 0 0% 0%; /* Black text on sidebar primary items */ 50 | --sidebar-accent: 0 0% 10%; /* Darker background for hover/accent in sidebar */ 51 | --sidebar-accent-foreground: 120 100% 60%; /* Brighter Neon Green for text on sidebar accent */ 52 | --sidebar-border: 120 50% 15%; /* Dark green border for sidebar */ 53 | --sidebar-ring: 180 100% 50%; /* Cyan for focus rings within sidebar */ 54 | } 55 | } 56 | 57 | @layer base { 58 | * { 59 | @apply border-border; 60 | } 61 | body { 62 | @apply bg-background text-foreground font-mono; /* Apply monospace font globally */ 63 | } 64 | } 65 | 66 | @keyframes blink-animation { 67 | to { 68 | visibility: hidden; 69 | } 70 | } 71 | .blinking-cursor { 72 | display: inline-block; 73 | animation: blink-animation 1s steps(2, start) infinite; 74 | font-weight: bold; 75 | } 76 | 77 | .text-center-green { 78 | color: #00ff00 !important; 79 | text-align: center; 80 | } -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation'; 2 | 3 | export default function RootLayout({ 4 | children, 5 | }: { 6 | children: React.ReactNode; 7 | }) { 8 | // Si estamos en la raíz, redirigir a /en 9 | if (typeof window !== 'undefined' && window.location.pathname === '/') { 10 | redirect('/en'); 11 | } 12 | 13 | // Si no estamos en la raíz, mostrar el contenido 14 | return children; 15 | } -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | 2 | // This file is no longer the root page. 3 | // Its content has been moved to src/app/[locale]/page.tsx 4 | // This file can be safely removed. 5 | export default function DeprecatedHomePage() { 6 | return
Please navigate to a localized path like /es or /en.
; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/AboutModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useScopedI18n } from '@/locales/client'; 3 | import { 4 | Dialog, 5 | DialogContent, 6 | DialogHeader, 7 | DialogTitle, 8 | DialogFooter, 9 | } from '@/components/ui/dialog'; 10 | import { Button } from '@/components/ui/button'; 11 | import { ScrollArea } from '@/components/ui/scroll-area'; 12 | 13 | interface AboutModalProps { 14 | isOpen: boolean; 15 | onClose: () => void; 16 | } 17 | 18 | export default function AboutModal({ isOpen, onClose }: AboutModalProps) { 19 | const t = useScopedI18n('about'); 20 | // Construir features desde las claves i18n 21 | const features: string[] = []; 22 | let i = 0; 23 | while (true) { 24 | try { 25 | const key = `features.${i}` as any; 26 | const feature = t(key); 27 | if (feature && feature !== key) { 28 | features.push(feature); 29 | i++; 30 | } else { 31 | break; 32 | } 33 | } catch { 34 | break; 35 | } 36 | } 37 | return ( 38 | 39 | 40 |
{t('description')}
41 | 42 |
43 |
44 | 🏴‍☠️ 45 |
46 | 47 | {t('title')} v1.0.0 48 | 49 |
50 |
51 | 52 | Status: OPERATIONAL 53 | 54 | Security: A+ 55 | 56 | Ready for CTF 57 |
58 |
59 | 60 |
61 | {/* Misión */} 62 |
63 |
64 | 🎯 65 |

MISSION

66 |
67 |

{t('description')}

68 |
69 | {/* Features */} 70 |
71 |
72 | 73 |

{t('featuresTitle')}

74 |
75 |
76 | {features.map((feature: string, i: number) => ( 77 |
78 | 79 | {feature} 80 |
81 | ))} 82 |
83 |
84 | {/* Tech Stack */} 85 |
86 |
87 | 🛠️ 88 |

TECH STACK

89 |
90 |
91 |
92 |
Frontend
93 |
Next.js 15
94 |
React 18
95 |
TypeScript
96 |
97 |
98 |
Styling
99 |
Tailwind CSS
100 |
Custom Terminal
101 |
Responsive
102 |
103 |
104 |
AI
105 |
Google Gemini
106 |
OpenAI GPT
107 |
Smart Prompts
108 |
109 |
110 |
Security
111 |
DOMPurify
112 |
Input Sanitization
113 |
XSS Protection
114 |
115 |
116 |
117 | {/* Developer Info */} 118 |
119 |
120 | 👨‍💻 121 |

DEVELOPER

122 |
123 |
124 |
125 |
126 | Name: 127 | Ilana Aminoff 128 |
129 |
130 | Handle: 131 | @ilanami 132 |
133 |
134 | Focus: 135 | Cybersecurity & CTF 136 |
137 |
138 |
139 |
140 | Contact: 141 | 145 | writeup_builder@proton.me 146 | 147 |
148 |
149 | GitHub: 150 | 156 | github.com/ilanami 157 | 158 |
159 |
160 | Repository: 161 | 167 | /ctf_writeup_builder 168 | 169 |
170 |
171 |
172 |
173 | {/* Stats */} 174 |
175 |
176 | 📊 177 |

PROJECT STATS

178 |
179 |
180 |
181 |
100%
182 |
Security Score
183 |
184 |
185 |
2
186 |
AI Providers
187 |
188 |
189 |
190 |
Write-ups
191 |
192 |
193 |
0
194 |
Data Tracking
195 |
196 |
197 |
198 | {/* Acknowledgments */} 199 |
200 |
201 | 🙏 202 |

{t('acknowledgements')}

203 |
204 |

205 | {t('thanks')} 206 |

207 |
208 | {/* Call to Action */} 209 |
210 |
Ready to dominate CTFs? 🏴‍☠️
211 |
212 | Create professional write-ups with AI assistance and secure your victories! 213 |
214 |
215 |
216 |
217 | 218 |
219 |
220 | Made with ❤️ for the CTF community 221 |
222 |
223 | 230 | 236 |
237 |
238 |
239 |
240 |
241 | ); 242 | } -------------------------------------------------------------------------------- /src/components/ApiKeyConfigModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useScopedI18n } from '@/locales/client'; 3 | 4 | const PROVIDERS = [ 5 | { value: 'gemini', label: 'Google Gemini' }, 6 | { value: 'openai', label: 'OpenAI ChatGPT' }, 7 | ]; 8 | 9 | export default function ApiKeyConfigModal({ onSave, onCancel }: { onSave?: (data: { provider: string; apiKey: string }) => void; onCancel?: () => void }) { 10 | const [provider, setProvider] = useState('gemini'); 11 | const [apiKey, setApiKey] = useState(''); 12 | const [error, setError] = useState(''); 13 | const tai = useScopedI18n('aiConfig'); 14 | 15 | // Funciones de codificación Base64 (NO es encriptación) 16 | const encodeApiKey = (key: string): string => btoa(key); 17 | const decodeApiKey = (encoded: string): string => { 18 | try { 19 | return atob(encoded); 20 | } catch { 21 | return encoded; // Si no está codificado en Base64 22 | } 23 | }; 24 | 25 | // Cargar valores guardados 26 | useEffect(() => { 27 | const savedProvider = localStorage.getItem('aiProvider'); 28 | const savedEncodedKey = localStorage.getItem('aiApiKey'); 29 | 30 | if (savedProvider) setProvider(savedProvider); 31 | if (savedEncodedKey) { 32 | const decodedKey = decodeApiKey(savedEncodedKey); 33 | setApiKey(decodedKey); 34 | } 35 | }, []); 36 | 37 | // Validar API Key 38 | const validateApiKey = (key: string, providerType: string): string | null => { 39 | if (!key.trim()) { 40 | return tai('errorApiKeyEmpty'); 41 | } 42 | if (providerType === 'openai' && !key.startsWith('sk-')) { 43 | return tai('errorOpenAIPrefix'); 44 | } 45 | if (key.length < 10) { 46 | return tai('errorApiKeyTooShort'); 47 | } 48 | return null; 49 | }; 50 | 51 | // Obtener enlace de ayuda 52 | const getHelpLink = () => { 53 | return provider === 'gemini' 54 | ? 'https://makersuite.google.com/app/apikey' 55 | : 'https://platform.openai.com/api-keys'; 56 | }; 57 | 58 | // Obtener título dinámico 59 | const getTitle = () => { 60 | return provider === 'gemini' 61 | ? tai('configureApiKeyTitleGemini') 62 | : tai('configureApiKeyTitleOpenAI'); 63 | }; 64 | 65 | const handleSave = () => { 66 | setError(''); 67 | const validationError = validateApiKey(apiKey, provider); 68 | if (validationError) { 69 | setError(validationError); 70 | return; 71 | } 72 | // Codificar en Base64 y guardar (NO es encriptación) 73 | const encodedKey = encodeApiKey(apiKey); 74 | localStorage.setItem('aiProvider', provider); 75 | localStorage.setItem('aiApiKey', encodedKey); 76 | if (onSave) onSave({ provider, apiKey }); 77 | }; 78 | 79 | const handleDeleteApiKey = () => { 80 | const savedEncodedKey = localStorage.getItem('aiApiKey'); 81 | if (!savedEncodedKey) { 82 | alert(tai('apiKeyEmptyTitle')); 83 | return; 84 | } 85 | localStorage.removeItem('aiApiKey'); 86 | localStorage.removeItem('aiProvider'); 87 | setApiKey(''); 88 | setProvider('gemini'); 89 | setError(''); 90 | alert(tai('apiKeyDeletedTitle')); 91 | if (onSave) onSave({ provider: 'gemini', apiKey: '' }); 92 | }; 93 | 94 | return ( 95 |
96 |
97 | {/* Título dinámico */} 98 |

99 | {getTitle()} 100 |

101 | {/* Selector de proveedor */} 102 |
103 | 104 | 112 |
113 | {/* Instrucciones */} 114 |

115 | {provider === 'gemini' ? tai('configureApiKeyDescriptionGemini') : tai('configureApiKeyDescriptionOpenAI')} 116 |

117 |

118 | {tai('responsibilityNote')} 119 |

120 | {/* Input de API Key */} 121 |
122 | 125 | { 129 | setApiKey(e.target.value); 130 | setError(''); // Limpiar error al escribir 131 | }} 132 | placeholder={tai('apiKeyPlaceholder')} 133 | style={styles.input} 134 | /> 135 | {/* Enlace de ayuda */} 136 | 146 |
147 | {/* Error */} 148 | {error && ( 149 |
150 | ⚠️ {error} 151 |
152 | )} 153 | {/* Aviso de seguridad */} 154 |
155 | {tai('securityNote')} 156 |
157 | {/* Botones */} 158 |
159 | 162 | 165 | 168 |
169 |
170 |
171 | ); 172 | } 173 | 174 | // Estilos manteniendo tu tema hacker 175 | const styles = { 176 | overlay: { 177 | position: 'fixed' as 'fixed', 178 | top: 0, 179 | left: 0, 180 | right: 0, 181 | bottom: 0, 182 | backgroundColor: 'rgba(0, 0, 0, 0.8)', 183 | display: 'flex', 184 | alignItems: 'center', 185 | justifyContent: 'center', 186 | zIndex: 1000, 187 | }, 188 | modal: { 189 | backgroundColor: '#000', 190 | color: '#00ff00', 191 | padding: '20px', 192 | borderRadius: '8px', 193 | border: '2px solid #00ff00', 194 | width: '90%', 195 | maxWidth: '500px', 196 | fontFamily: 'monospace', 197 | }, 198 | title: { 199 | color: '#00ff00', 200 | fontSize: '18px', 201 | fontWeight: 'bold', 202 | marginBottom: '15px', 203 | textAlign: 'left' as 'left', 204 | }, 205 | providerSection: { 206 | marginBottom: '15px', 207 | }, 208 | providerLabel: { 209 | color: '#00ff00', 210 | fontSize: '14px', 211 | display: 'block', 212 | marginBottom: '5px', 213 | }, 214 | select: { 215 | width: '100%', 216 | padding: '8px', 217 | backgroundColor: '#000', 218 | color: '#00ff00', 219 | border: '1px solid #00ff00', 220 | borderRadius: '4px', 221 | fontFamily: 'monospace', 222 | fontSize: '14px', 223 | }, 224 | description: { 225 | color: '#00ff00', 226 | fontSize: '12px', 227 | lineHeight: '1.4', 228 | marginBottom: '10px', 229 | }, 230 | responsibility: { 231 | color: '#00ff00', 232 | fontSize: '12px', 233 | marginBottom: '15px', 234 | fontWeight: 'bold', 235 | }, 236 | inputSection: { 237 | marginBottom: '15px', 238 | }, 239 | inputLabel: { 240 | color: '#00ff00', 241 | fontSize: '14px', 242 | display: 'block', 243 | marginBottom: '5px', 244 | }, 245 | input: { 246 | width: '100%', 247 | padding: '10px', 248 | backgroundColor: '#000', 249 | color: '#00ff00', 250 | border: '1px solid #00ff00', 251 | borderRadius: '4px', 252 | fontFamily: 'monospace', 253 | fontSize: '14px', 254 | marginBottom: '8px', 255 | boxSizing: 'border-box' as 'border-box', 256 | }, 257 | helpSection: { 258 | fontSize: '12px', 259 | color: '#00ff00', 260 | }, 261 | helpLink: { 262 | color: '#00ff00', 263 | textDecoration: 'underline', 264 | }, 265 | error: { 266 | backgroundColor: 'rgba(255, 0, 0, 0.2)', 267 | border: '1px solid #ff0000', 268 | color: '#ff0000', 269 | padding: '8px', 270 | borderRadius: '4px', 271 | marginBottom: '10px', 272 | fontSize: '12px', 273 | fontFamily: 'monospace', 274 | }, 275 | securityNote: { 276 | fontSize: '11px', 277 | color: '#00ff00', 278 | opacity: 0.8, 279 | marginBottom: '15px', 280 | textAlign: 'center' as 'center', 281 | }, 282 | buttonContainer: { 283 | display: 'flex', 284 | justifyContent: 'flex-end', 285 | gap: '10px', 286 | }, 287 | cancelButton: { 288 | padding: '8px 15px', 289 | backgroundColor: 'transparent', 290 | color: '#00ff00', 291 | border: '1px solid #00ff00', 292 | borderRadius: '4px', 293 | cursor: 'pointer', 294 | fontFamily: 'monospace', 295 | fontSize: '14px', 296 | }, 297 | saveButton: { 298 | padding: '8px 15px', 299 | backgroundColor: '#00ff00', 300 | color: '#000', 301 | border: '1px solid #00ff00', 302 | borderRadius: '4px', 303 | cursor: 'pointer', 304 | fontFamily: 'monospace', 305 | fontSize: '14px', 306 | fontWeight: 'bold', 307 | }, 308 | }; -------------------------------------------------------------------------------- /src/components/GeneralInfoPanel.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from 'react'; 4 | import { useWriteUp } from '@/hooks/useWriteUp'; 5 | import { Button } from '@/components/ui/button'; 6 | import { Input } from '@/components/ui/input'; 7 | import { Label } from '@/components/ui/label'; 8 | import { 9 | Select, 10 | SelectContent, 11 | SelectItem, 12 | SelectTrigger, 13 | SelectValue, 14 | } from '@/components/ui/select'; 15 | import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; 16 | import { Calendar } from "@/components/ui/calendar"; 17 | import { CalendarIcon } from 'lucide-react'; 18 | import { format, parseISO } from 'date-fns'; 19 | import { es, enUS } from 'date-fns/locale'; 20 | import { DIFFICULTIES_KEYS, OS_KEYS } from '@/lib/constants'; 21 | import type { Difficulty, OperatingSystem, Screenshot } from '@/lib/types'; 22 | import { ImageUploader } from './ImageUploader'; 23 | import { TagInput } from './TagInput'; 24 | import { ScrollArea } from '@/components/ui/scroll-area'; 25 | import { useScopedI18n, useCurrentLocale } from '@/locales/client'; 26 | 27 | export const GeneralInfoPanel: React.FC = () => { 28 | const { state, dispatch } = useWriteUp(); 29 | const { writeUp } = state; 30 | const t = useScopedI18n('generalInfo'); 31 | const currentLocale = useCurrentLocale(); 32 | const dateLocale = currentLocale === 'es' ? es : enUS; 33 | const tDifficulties = useScopedI18n('difficulties'); 34 | const tOS = useScopedI18n('operatingSystems'); 35 | 36 | 37 | const handleInputChange = (field: keyof typeof writeUp, value: any) => { 38 | dispatch({ type: 'UPDATE_GENERAL_INFO', payload: { [field]: value } }); 39 | }; 40 | 41 | const handleDateChange = (date: Date | undefined) => { 42 | if (date) { 43 | handleInputChange('date', date.toISOString().split('T')[0]); 44 | } 45 | }; 46 | 47 | const handleMachineImageUpload = (screenshot: Screenshot) => { 48 | dispatch({ type: 'SET_MACHINE_IMAGE', payload: screenshot }); 49 | }; 50 | 51 | const handleMachineImageRemove = () => { 52 | dispatch({ type: 'SET_MACHINE_IMAGE', payload: undefined }); 53 | }; 54 | 55 | return ( 56 | 57 |
58 |

{t('title')}

59 | 60 |
61 | 62 | handleInputChange('title', e.target.value)} 66 | placeholder={t('writeupTitlePlaceholder')} 67 | /> 68 |
69 | 70 |
71 | 72 | handleInputChange('author', e.target.value)} 76 | placeholder={t('authorPlaceholder')} 77 | /> 78 |
79 | 80 |
81 | 82 | 83 | 84 | 98 | 99 | 100 | 107 | 108 | 109 |
110 | 111 |
112 | 113 | 128 |
129 |
130 | 131 | handleInputChange('tags', tags)} 135 | placeholder={t('addTag')} 136 | /> 137 |
138 | 139 |
140 | 141 | 156 |
157 | 158 |
159 | 160 | 167 |
168 |
169 |
170 | ); 171 | }; 172 | -------------------------------------------------------------------------------- /src/components/ImageUploader.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { ChangeEvent } from 'react'; 4 | import React, { useState, useRef } from 'react'; 5 | import type { Screenshot } from '@/lib/types'; 6 | import { Button } from '@/components/ui/button'; 7 | import { Input } from '@/components/ui/input'; 8 | import { Card, CardContent, CardFooter } from '@/components/ui/card'; 9 | import Image from 'next/image'; 10 | import { UploadCloud, XCircle } from 'lucide-react'; 11 | import { v4 as uuidv4 } from 'uuid'; 12 | import { cn } from '@/lib/utils'; 13 | import { useI18n } from '@/locales/client'; // Import I18n hook 14 | 15 | interface ImageUploaderProps { 16 | onImageUpload: (screenshot: Screenshot) => void; 17 | onImageRemove?: () => void; // For single image uploader like machine image 18 | currentImage?: Screenshot; 19 | label?: string; 20 | className?: string; 21 | accept?: string; 22 | id?: string; 23 | canUpload?: () => boolean; 24 | } 25 | 26 | const MAX_FILE_SIZE_MB = 5; 27 | const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024; 28 | 29 | export const ImageUploader: React.FC = ({ 30 | onImageUpload, 31 | onImageRemove, 32 | currentImage, 33 | label, // Label is passed as a prop, already translated by parent 34 | className, 35 | accept = "image/*", 36 | id = "image-upload", 37 | canUpload 38 | }) => { 39 | const t = useI18n(); 40 | const [error, setError] = useState(null); 41 | const [isDragging, setIsDragging] = useState(false); 42 | const fileInputRef = useRef(null); 43 | 44 | const handleFileChange = (event: ChangeEvent) => { 45 | const file = event.target.files?.[0]; 46 | if (file) { 47 | processFile(file); 48 | } 49 | }; 50 | 51 | const processFile = (file: File) => { 52 | if (file.size > MAX_FILE_SIZE_BYTES) { 53 | setError(t('imageUploader.fileTooLarge', { maxSize: MAX_FILE_SIZE_MB })); 54 | return; 55 | } 56 | if (!file.type.startsWith('image/')) { 57 | setError(t('imageUploader.invalidFileType')); 58 | return; 59 | } 60 | setError(null); 61 | const reader = new FileReader(); 62 | reader.onloadend = () => { 63 | onImageUpload({ 64 | id: uuidv4(), 65 | name: file.name, 66 | dataUrl: reader.result as string, 67 | }); 68 | }; 69 | reader.onerror = () => { 70 | setError(t('imageUploader.errorReadingFile')); 71 | }; 72 | reader.readAsDataURL(file); 73 | }; 74 | 75 | const handleDragOver = (event: React.DragEvent) => { 76 | event.preventDefault(); 77 | event.stopPropagation(); 78 | setIsDragging(true); 79 | }; 80 | 81 | const handleDragLeave = (event: React.DragEvent) => { 82 | event.preventDefault(); 83 | event.stopPropagation(); 84 | setIsDragging(false); 85 | }; 86 | 87 | const handleDrop = (event: React.DragEvent) => { 88 | event.preventDefault(); 89 | event.stopPropagation(); 90 | setIsDragging(false); 91 | if (canUpload && !canUpload()) return; 92 | const file = event.dataTransfer.files?.[0]; 93 | if (file) { 94 | processFile(file); 95 | } 96 | }; 97 | 98 | const triggerFileInput = () => { 99 | if (canUpload && !canUpload()) return; 100 | fileInputRef.current?.click(); 101 | }; 102 | 103 | return ( 104 | 105 | 106 | {currentImage ? ( 107 |
108 | {currentImage.name 115 | {onImageRemove && ( 116 | 125 | )} 126 |
127 | ) : ( 128 |
e.key === 'Enter' && triggerFileInput()} 134 | > 135 | 136 |

{label || t('imageUploader.defaultLabel')}

137 |

{t('imageUploader.dragDropOrClick')}

138 |

{t('imageUploader.maxFileSize', { maxSize: MAX_FILE_SIZE_MB })}

139 |
140 | )} 141 | 149 | {error &&

{error}

} 150 |
151 | {!currentImage && ( 152 | 153 | 156 | 157 | )} 158 |
159 | ); 160 | }; 161 | -------------------------------------------------------------------------------- /src/components/MarkdownEditor.tsx: -------------------------------------------------------------------------------- 1 | 2 | "use client"; 3 | 4 | import React, { useRef } from 'react'; 5 | import { Textarea } from '@/components/ui/textarea'; 6 | import { Label } from '@/components/ui/label'; 7 | import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/components/ui/card'; 8 | import { Button } from '@/components/ui/button'; 9 | import { Bold, Italic, List, ListOrdered, Code2, Link2 } from 'lucide-react'; 10 | import { cn } from '@/lib/utils'; 11 | 12 | interface MarkdownEditorProps { 13 | value: string; 14 | onChange: (value: string) => void; 15 | label?: string; 16 | placeholder?: string; 17 | className?: string; 18 | rows?: number; 19 | id?: string; 20 | error?: string; 21 | showWordCount?: boolean; 22 | } 23 | 24 | export const MarkdownEditor: React.FC = ({ 25 | value, 26 | onChange, 27 | label, 28 | placeholder = "Escribe tu contenido Markdown aquí...", 29 | className, 30 | rows = 10, 31 | id = "markdown-editor", 32 | error, 33 | showWordCount = true, 34 | }) => { 35 | const textareaRef = useRef(null); 36 | const wordCount = value?.split(/\s+/).filter(Boolean).length || 0; 37 | const charCount = value?.length || 0; 38 | 39 | const handleFormat = (type: 'bold' | 'italic' | 'ul' | 'ol' | 'codeblock' | 'link') => { 40 | const textarea = textareaRef.current; 41 | if (!textarea) return; 42 | 43 | const currentValue = textarea.value; 44 | const start = textarea.selectionStart; 45 | const end = textarea.selectionEnd; 46 | const selectedText = currentValue.substring(start, end); 47 | 48 | let newText = ''; 49 | let newSelectionStart = start; 50 | let newSelectionEnd = end; 51 | 52 | const prefix = currentValue.substring(0, start); 53 | const suffix = currentValue.substring(end); 54 | 55 | switch (type) { 56 | case 'bold': 57 | if (selectedText) { 58 | newText = `${prefix}**${selectedText}**${suffix}`; 59 | newSelectionStart = start + 2; 60 | newSelectionEnd = start + 2 + selectedText.length; 61 | } else { 62 | newText = `${prefix}****${suffix}`; 63 | newSelectionStart = start + 2; 64 | newSelectionEnd = start + 2; 65 | } 66 | break; 67 | case 'italic': 68 | if (selectedText) { 69 | newText = `${prefix}*${selectedText}*${suffix}`; 70 | newSelectionStart = start + 1; 71 | newSelectionEnd = start + 1 + selectedText.length; 72 | } else { 73 | newText = `${prefix}**${suffix}`; // For italic placeholder, should be * * 74 | newSelectionStart = start + 1; 75 | newSelectionEnd = start + 1; 76 | } 77 | break; 78 | case 'ul': 79 | if (selectedText) { 80 | const lines = selectedText.split('\n'); 81 | const formattedLines = lines.map(line => `- ${line}`).join('\n'); 82 | newText = `${prefix}${formattedLines}${suffix}`; 83 | newSelectionStart = start; 84 | newSelectionEnd = start + formattedLines.length; 85 | } else { 86 | const linePrefix = (start === 0 || currentValue[start - 1] === '\n') ? '' : '\n'; 87 | newText = `${prefix}${linePrefix}- ${suffix}`; 88 | newSelectionStart = start + linePrefix.length + 2; 89 | newSelectionEnd = newSelectionStart; 90 | } 91 | break; 92 | case 'ol': 93 | if (selectedText) { 94 | const lines = selectedText.split('\n'); 95 | const formattedLines = lines.map((line, index) => `${index + 1}. ${line}`).join('\n'); 96 | newText = `${prefix}${formattedLines}${suffix}`; 97 | newSelectionStart = start; 98 | newSelectionEnd = start + formattedLines.length; 99 | } else { 100 | const linePrefix = (start === 0 || currentValue[start - 1] === '\n') ? '' : '\n'; 101 | newText = `${prefix}${linePrefix}1. ${suffix}`; 102 | newSelectionStart = start + linePrefix.length + 3; 103 | newSelectionEnd = newSelectionStart; 104 | } 105 | break; 106 | case 'codeblock': 107 | if (selectedText) { 108 | newText = `${prefix}\`\`\`\n${selectedText}\n\`\`\`${suffix}`; 109 | newSelectionStart = start + prefix.length + 4; 110 | newSelectionEnd = newSelectionStart + selectedText.length; 111 | } else { 112 | newText = `${prefix}\`\`\`\n\n\`\`\`${suffix}`; 113 | newSelectionStart = start + prefix.length + 4; 114 | newSelectionEnd = newSelectionStart; 115 | } 116 | break; 117 | case 'link': 118 | const linkDisplayText = selectedText || 'texto_del_enlace'; 119 | newText = `${prefix}[${linkDisplayText}](aqui_la_url)${suffix}`; 120 | if (selectedText) { 121 | newSelectionStart = prefix.length + 1 + linkDisplayText.length + 2; 122 | newSelectionEnd = newSelectionStart + 'aqui_la_url'.length; 123 | } else { 124 | newSelectionStart = prefix.length + 1; 125 | newSelectionEnd = newSelectionStart + linkDisplayText.length; 126 | } 127 | break; 128 | } 129 | 130 | onChange(newText); 131 | 132 | setTimeout(() => { 133 | if (textareaRef.current) { 134 | textareaRef.current.focus(); 135 | textareaRef.current.setSelectionRange(newSelectionStart, newSelectionEnd); 136 | } 137 | }, 0); 138 | }; 139 | 140 | 141 | return ( 142 | 143 | {label && ( 144 | 145 | 146 | 147 | )} 148 | 149 |
150 | 153 | 156 | 159 | 162 | 165 | 168 |
169 |