commit b9fbed9a543be6b0b54054757d396d38c9743520 Author: Hadi <112569860+anotherhadi@users.noreply.github.com> Date: Wed Sep 24 17:20:03 2025 +0200 init diff --git a/.github/assets/banner.png b/.github/assets/banner.png new file mode 100644 index 0000000..e0eb561 Binary files /dev/null and b/.github/assets/banner.png differ diff --git a/.github/assets/logo.png b/.github/assets/logo.png new file mode 100644 index 0000000..a8da970 Binary files /dev/null and b/.github/assets/logo.png differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fff8bb5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +result/ +testdata/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0f066ed --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,13 @@ +# Contributing + +Everybody is invited and welcome to contribute. There is a lot to do... Check +the issues! + +The process is straight-forward. + +- Read + [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) + by Kubernetes (but skip step 0 and 1) +- Fork this git repository +- Write your changes (bug fixes, new features, ...). +- Create a Pull Request against the main branch. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5615ec7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Hadi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..891affa --- /dev/null +++ b/README.md @@ -0,0 +1,117 @@ +
+ nixy logo +
+ +
+ +# Eleakxir β€” Self-hosted search engine for leaked data. + +[Eleakxir](https://eleakxir.hadi.diy) is a **self-hosted search engine** that +lets you connect to your own **private and secure server**, **explore data +wells** (parquet files) from multiple sources, and visualize results in a clean, +modern web interface. + +> ✨ 100% open-source β€” you control your data, you control your server. + +## πŸš€ Features + +- πŸ” **Private by design** β€” connect to your own Eleakxir server with a custom + URL + password. +- 🎨 **Beautiful UI** β€” built with Svelte, TailwindCSS, and DaisyUI. +- πŸ›  **Open source & extensible** β€” hack it, self-host it, extend it. +- **πŸ“ Efficient File Format**: Uses the columnar **Parquet** format for high + compression and rapid query performance. +- **πŸ€– Automatic Discovery**: Automatically detects new `.parquet` files in your + folders and updates its metadata cache on a configurable schedule. +- **πŸ“œ Standardized Schema**: Includes a detailed guide on how to normalize your + data leaks for consistent and effective searching across different breaches. + (See [here](./leak-utils/DATALEAKS-NORMALIZATION.md)) +- **🧰 Data Utility Toolkit**: Includes a dedicated command-line tool + [leak-utils](./leak-utils/README.md) for managing, cleaning, and converting + data files to the standardized Parquet format. +- **πŸ” OSINT Tools**: Integration of various OSINT tools: + - [github-recon](https://github.com/anotherhadi/github-recon) + - [gravatar-recon](https://github.com/anotherhadi/gravatar-recon) (To-do) + - sherlock (To-do) + - holehe (To-do) + - ghunt (To-do) + +## βš™οΈ How it works + +1. You run an **Elixir server** that manages parquet files from various leaked + data sources and multiple OSINT tools. +2. Eleakxir (the web client) connects to your server via HTTPS and authenticated + headers. +3. You can: + - Search across indexed leaks and OSINT tools + - Browse results interactively + - Review history and stats + +## 🚨 Disclaimer + +Eleakxir is provided **for educational and research purposes only**. You are +solely responsible for how you use this software. Accessing, storing, or +distributing leaked data may be illegal in your jurisdiction. The authors and +contributors **do not condone or promote illegal activity**. Use responsibly and +only with data you are legally permitted to process. + +## πŸ§‘β€πŸ’» Tech stack + +- **Frontend**: [Svelte](https://svelte.dev/), + [sv-router](https://github.com/colinlienard/sv-router), + [TailwindCSS](https://tailwindcss.com/), [DaisyUI](https://daisyui.com/) +- **Backend**: [Golang](https://go.dev), [Gin](https://gin-gonic.com), + [duckdb](https://duckdb.org) + +## πŸ“¦ Getting started + +### Install with NixOS + +1. In the `flake.nix` file, add `eleakxir` in the `inputs` section and import + the `eleakxir.nixosModules.default` module: + +```nix +{ + inputs = { + eleakxir.url = "github:anotherhadi/eleakxir"; + }; + outputs = { + # ... + modules = [ + inputs.eleakxir.nixosModules.eleakxir + ]; + # ... + } +} +``` + +2. Enable the backend service: + +```nix +services.eleakxir = { + enable = true; + # port = 9198; + folders = ["/var/lib/eleakxir/leaks/"] # Folders with parquet files +}; +``` + +## πŸ”‘ Configuration + +### Backend + +Check the [back.nix](./nix/back.nix) file to see configuration options. + +### Client + +Before searching, configure your server in the client: + +1. Open [https://eleakxir.hadi.diy](https://eleakxir.hadi.diy) in your browser + and add your server. +2. Click **β€œConnect your server”** in the UI. +3. Enter your **server URL** and **password**. +4. Start searching πŸš€ + +## 🀝 Contributing + +[Contributions are welcome](./CONTRIBUTING.md)! Feel free to open issues or +submit PRs. diff --git a/back/.air.toml b/back/.air.toml new file mode 100644 index 0000000..197045c --- /dev/null +++ b/back/.air.toml @@ -0,0 +1,21 @@ +root = "." +tmp_dir = "tmp" + +[build] +cmd = "go build -o ./tmp/main ./cmd/main.go" +full_bin = "DEBUG=true DATALEAKS_FOLDERS=../testdata ./tmp/main" +args = [] +exclude_dir = ["tmp", "vendor"] +exclude_file = [] +exclude_regex = ["_test.go"] +exclude_unchanged = false +follow_symlink = false +send_interrupt = false +delay = 1000 # ms +stop_on_error = true + +[log] +time = true + +[misc] +clean_on_exit = true diff --git a/back/.gitignore b/back/.gitignore new file mode 100644 index 0000000..3fec32c --- /dev/null +++ b/back/.gitignore @@ -0,0 +1 @@ +tmp/ diff --git a/back/api/api.go b/back/api/api.go new file mode 100644 index 0000000..5cc0f62 --- /dev/null +++ b/back/api/api.go @@ -0,0 +1,174 @@ +package api + +import ( + "net/http" + "strings" + "time" + + "github.com/anotherhadi/eleakxir/backend/search" + "github.com/anotherhadi/eleakxir/backend/server" + "github.com/gin-gonic/gin" +) + +// TODO: We need to know when we hit the LIMIT + +func routes(s *server.Server, cache *map[string]*search.Result) { + s.Router.Use( + func(c *gin.Context) { + if s.Settings.Password != "" { + password := c.GetHeader("X-Password") + if password != s.Settings.Password { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + return + } + } + c.Next() + }, + ) + + s.Router.GET("/", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "Settings": s.Settings, + "Dataleaks": s.Dataleaks, + "TotalDataleaks": s.TotalDataleaks, + "TotalRows": s.TotalRows, + "TotalSize": s.TotalSize, + }) + }) + + s.Router.GET("/history", func(c *gin.Context) { + type historyItem struct { + Id string + Status string + Date time.Time + Query search.Query + Results int + } + var history []historyItem + s.Mu.RLock() + for _, r := range *cache { + history = append(history, historyItem{ + Id: r.Id, + Status: r.Status, + Date: r.Date, + Query: r.Query, + Results: len(r.LeakResult.Rows), + }) + } + s.Mu.RUnlock() + for i := 0; i < len(history)-1; i++ { + for j := 0; j < len(history)-i-1; j++ { + if history[j].Date.Before(history[j+1].Date) { + history[j], history[j+1] = history[j+1], history[j] + } + } + } + c.JSON(http.StatusOK, gin.H{ + "History": history, + }) + }) + + s.Router.POST("/search", func(c *gin.Context) { + var query search.Query + if err := c.BindJSON(&query); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"Error": "invalid JSON"}) + return + } + query = cleanQuery(query) + if len(query.Text) <= s.Settings.MinimumQueryLength { + c.JSON(http.StatusBadRequest, gin.H{"Error": "query too short"}) + return + } + id := search.EncodeQueryID(query, *s.TotalDataleaks) + s.Mu.RLock() + _, exists := (*cache)[id] + s.Mu.RUnlock() + if exists { + c.JSON(http.StatusOK, gin.H{"Id": id}) + return + } + r := search.Result{ + Id: id, + } + go search.Search(s, query, &r, s.Mu) + s.Mu.Lock() + (*cache)[id] = &r + s.Mu.Unlock() + c.JSON(http.StatusOK, gin.H{"Id": id}) + }) + + s.Router.GET("/search/:id", func(c *gin.Context) { + id := c.Param("id") + s.Mu.RLock() + r, exists := (*cache)[id] + s.Mu.RUnlock() + if !exists { + c.JSON(http.StatusNotFound, gin.H{"Error": "not found"}) + return + } + c.JSON(http.StatusOK, r) + }) +} + +func Init(s *server.Server) { + if !s.Settings.Debug { + gin.SetMode(gin.ReleaseMode) + } + + s.Router = gin.Default() + s.Router.Use(CORSMiddleware()) + + cache := make(map[string]*search.Result) + + go func() { + for { + time.Sleep(time.Minute) + deleteOldCache(s, &cache) + } + }() + + routes(s, &cache) +} + +func deleteOldCache(s *server.Server, cache *map[string]*search.Result) { + s.Mu.Lock() + defer s.Mu.Unlock() + now := time.Now() + for id, r := range *cache { + if now.Sub(r.Date) > s.Settings.MaxCacheDuration { + delete(*cache, id) + } + } +} + +func cleanQuery(q search.Query) search.Query { + q.Column = strings.ToLower(strings.TrimSpace(q.Column)) + q.Column = strings.Join(strings.Fields(q.Column), " ") + q.Column = strings.ReplaceAll(q.Column, "`", "") + q.Column = strings.ReplaceAll(q.Column, "'", "") + q.Column = strings.ReplaceAll(q.Column, "-", "_") + q.Column = strings.ReplaceAll(q.Column, " ", "_") + q.Column = strings.ReplaceAll(q.Column, "\"", "") + + q.Text = strings.TrimSpace(q.Text) + q.Text = strings.Join(strings.Fields(q.Text), " ") + + return q +} + +func CORSMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + c.Writer.Header().Set("Access-Control-Allow-Origin", "*") + c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") + c.Writer.Header(). + Set("Access-Control-Allow-Headers", "X-Password, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") + c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE") + + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(204) + return + } + + c.Next() + } +} diff --git a/back/cmd/main.go b/back/cmd/main.go new file mode 100644 index 0000000..db444f3 --- /dev/null +++ b/back/cmd/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "fmt" + "strconv" + + "github.com/anotherhadi/eleakxir/backend/api" + "github.com/anotherhadi/eleakxir/backend/server" +) + +func main() { + server := server.NewServer() + fmt.Println("Starting the server.") + + api.Init(server) + + err := server.Router.Run(":" + strconv.Itoa(server.Settings.Port)) + if err != nil { + panic(err) + } +} diff --git a/back/go.mod b/back/go.mod new file mode 100644 index 0000000..6b45ca6 --- /dev/null +++ b/back/go.mod @@ -0,0 +1,70 @@ +module github.com/anotherhadi/eleakxir/backend + +go 1.25.0 + +require ( + github.com/anotherhadi/github-recon v1.5.6 + github.com/charmbracelet/log v0.4.2 + github.com/gin-gonic/gin v1.10.1 + github.com/marcboeker/go-duckdb v1.8.5 +) + +require ( + github.com/apache/arrow-go/v18 v18.1.0 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/lipgloss v1.1.0 // indirect + github.com/charmbracelet/x/ansi v0.8.0 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/google/flatbuffers v25.1.24+incompatible // indirect + github.com/google/go-github/v72 v72.0.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pierrec/lz4/v4 v4.1.22 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/saran13raj/go-pixels v0.0.0-20250629121333-58b240a3ae51 // indirect + github.com/spf13/pflag v1.0.7 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/zeebo/xxh3 v1.0.2 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.38.0 // indirect + golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect + golang.org/x/image v0.28.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/sync v0.15.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.26.0 // indirect + golang.org/x/tools v0.33.0 // indirect + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect + google.golang.org/protobuf v1.36.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/back/go.sum b/back/go.sum new file mode 100644 index 0000000..edf6922 --- /dev/null +++ b/back/go.sum @@ -0,0 +1,179 @@ +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/anotherhadi/github-recon v1.5.6 h1:IN3lQZRqqNbPpSyP5fvNoJrYODbM2tNwS5tiRgD+i1s= +github.com/anotherhadi/github-recon v1.5.6/go.mod h1:E2tmCmjEZdJeBx8u1J8sSMtnmU8aDQ6IjCoq3ykoHtY= +github.com/apache/arrow-go/v18 v18.1.0 h1:agLwJUiVuwXZdwPYVrlITfx7bndULJ/dggbnLFgDp/Y= +github.com/apache/arrow-go/v18 v18.1.0/go.mod h1:tigU/sIgKNXaesf5d7Y95jBBKS5KsxTqYBKXFsvKzo0= +github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE= +github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig= +github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw= +github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= +github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= +github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/flatbuffers v25.1.24+incompatible h1:4wPqL3K7GzBd1CwyhSd3usxLKOaJN/AC6puCca6Jm7o= +github.com/google/flatbuffers v25.1.24+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-github/v72 v72.0.0 h1:FcIO37BLoVPBO9igQQ6tStsv2asG4IPcYFi655PPvBM= +github.com/google/go-github/v72 v72.0.0/go.mod h1:WWtw8GMRiL62mvIquf1kO3onRHeWWKmK01qdCY8c5fg= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= +github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/marcboeker/go-duckdb v1.8.5 h1:tkYp+TANippy0DaIOP5OEfBEwbUINqiFqgwMQ44jME0= +github.com/marcboeker/go-duckdb v1.8.5/go.mod h1:6mK7+WQE4P4u5AFLvVBmhFxY5fvhymFptghgJX6B+/8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/saran13raj/go-pixels v0.0.0-20250629121333-58b240a3ae51 h1:H/XUfYcLxI3CBmDlgBpnOeTntRgqWvIoUXnqhCF5a0s= +github.com/saran13raj/go-pixels v0.0.0-20250629121333-58b240a3ae51/go.mod h1:sqhdZVLvqzTEBtmZBuTnFDUW0Lsryw2X2/wrLgqLEYg= +github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= +github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc= +golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= +golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE= +golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0= +gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o= +google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= +google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/back/search/dataleak/dataleak.go b/back/search/dataleak/dataleak.go new file mode 100644 index 0000000..4632474 --- /dev/null +++ b/back/search/dataleak/dataleak.go @@ -0,0 +1,191 @@ +package dataleak + +import ( + "fmt" + "slices" + "strconv" + "strings" + "time" + + "github.com/anotherhadi/eleakxir/backend/server" + "github.com/charmbracelet/log" +) + +type LeakResult struct { + Duration time.Duration + Rows []map[string]string + Error string +} + +func Search(s *server.Server, queryText, column string, exactMatch bool) LeakResult { + if len(*(s.Dataleaks)) == 0 { + return LeakResult{ + Error: "No dataleak configured", + } + } + now := time.Now() + result := LeakResult{} + + sqlQuery := buildSqlQuery(s, queryText, column, exactMatch) + + if s.Settings.Debug { + log.Info("New query:", "query", sqlQuery) + } + rows, err := s.Duckdb.Query(sqlQuery) + if err != nil { + result.Error = err.Error() + return result + } + defer rows.Close() + + cols, err := rows.Columns() + if err != nil { + result.Error = err.Error() + return result + } + + rawResult := make([][]byte, len(cols)) + dest := make([]any, len(cols)) + for i := range rawResult { + dest[i] = &rawResult[i] + } + + for rows.Next() { + err := rows.Scan(dest...) + if err != nil { + result.Error = err.Error() + return result + } + + rowMap := make(map[string]string) + for i, colName := range cols { + if rawResult[i] == nil || colName == "" { + continue + } + if colName == "filename" { + rowMap["source"] = server.FormatParquetName(string(rawResult[i])) + continue + } + rowMap[colName] = string(rawResult[i]) + } + result.Rows = append(result.Rows, rowMap) + } + + if err = rows.Err(); err != nil { + result.Error = err.Error() + return result + } + + result.Rows = removeDuplicateMaps(result.Rows) + + result.Duration = time.Since(now) + return result +} + +func removeDuplicateMaps(maps []map[string]string) []map[string]string { + seen := make(map[string]struct{}) + result := []map[string]string{} + + for _, m := range maps { + // Create a unique key for the map by concatenating its key-value pairs + var sb strings.Builder + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + slices.Sort(keys) // Sort keys to ensure consistent order + for _, k := range keys { + sb.WriteString(k) + sb.WriteString("=") + sb.WriteString(m[k]) + sb.WriteString(";") + } + key := sb.String() + + if _, exists := seen[key]; !exists { + seen[key] = struct{}{} + result = append(result, m) + } + } + + return result +} + +func buildSqlQuery(s *server.Server, queryText, column string, exactMatch bool) string { + limit := strconv.Itoa(s.Settings.Limit) + from := getFromClause(s) + if column == "name" { + column = "full_name" + } + columns := []string{column} + if column == "all" || column == "" { + columns = s.Settings.BaseColumns + } + columnsFiltered := []string{} + allColumns := []string{} + // TODO: Add columns that ends with _col aswell + for _, dataleak := range *s.Dataleaks { + for _, col := range dataleak.Columns { + if !slices.Contains(allColumns, col) { + allColumns = append(allColumns, col) + } + } + } + if column == "full_text" { + columnsFiltered = allColumns + } else { + for _, col := range columns { + if slices.Contains(allColumns, col) { + columnsFiltered = append(columnsFiltered, col) + } + } + } + + if len(columnsFiltered) == 0 { + return fmt.Sprintf("SELECT * FROM %s LIMIT %s", from, limit) + } + + where := getWhereClause(queryText, columnsFiltered, exactMatch) + return fmt.Sprintf("SELECT * FROM %s WHERE %s LIMIT %s", from, where, limit) +} + +func getWhereClause(queryText string, columns []string, exactMatch bool) string { + terms := strings.Fields(queryText) + var andClauses []string + + for _, term := range terms { + var orClausesForTerm []string + termEscaped := strings.ReplaceAll(term, "'", "''") + + for _, col := range columns { + if exactMatch { + termEscapedILike := strings.ReplaceAll(termEscaped, "_", "\\_") + termEscapedILike = strings.ReplaceAll(termEscapedILike, "%", "\\%") + orClausesForTerm = append(orClausesForTerm, fmt.Sprintf("\"%s\" ILIKE '%s' ESCAPE '\\'", col, strings.ToLower(termEscapedILike))) + } else { + // Escape characters for ILIKE + termEscapedILike := strings.ReplaceAll(termEscaped, "_", "\\_") + termEscapedILike = strings.ReplaceAll(termEscapedILike, "%", "\\%") + orClausesForTerm = append(orClausesForTerm, fmt.Sprintf("\"%s\" ILIKE '%%%s%%' ESCAPE '\\'", col, strings.ToLower(termEscapedILike))) + } + } + andClauses = append(andClauses, "("+strings.Join(orClausesForTerm, " OR ")+")") + } + return strings.Join(andClauses, " AND ") +} + +func getFromClause(s *server.Server) string { + parquets := []string{} + for _, dataleak := range *s.Dataleaks { + parquets = append(parquets, "'"+dataleak.Path+"'") + } + return fmt.Sprintf("read_parquet([%s], union_by_name=true, filename=true)", strings.Join(parquets, ", ")) +} + +func castAllColumns(cols []string) []string { + casted := make([]string, len(cols)) + for i, col := range cols { + casted[i] = fmt.Sprintf("cast(%s as text)", col) + } + return casted +} diff --git a/back/search/osint/github.go b/back/search/osint/github.go new file mode 100644 index 0000000..3ec1fbc --- /dev/null +++ b/back/search/osint/github.go @@ -0,0 +1,90 @@ +package osint + +import ( + "strings" + "time" + + "github.com/anotherhadi/eleakxir/backend/server" + + recon_email "github.com/anotherhadi/github-recon/github-recon/email" + recon_username "github.com/anotherhadi/github-recon/github-recon/username" + github_recon_settings "github.com/anotherhadi/github-recon/settings" +) + +type GithubResult struct { + Duration time.Duration + Error string + + UsernameResult *recon_username.UsernameResult + EmailResult *recon_email.EmailResult +} + +func Search(s *server.Server, queryText, column string) *GithubResult { + if !s.Settings.GithubRecon { + return nil + } + gr := GithubResult{} + now := time.Now() + settings := github_recon_settings.GetDefaultSettings() + settings.Token = s.Settings.GithubToken + settings.DeepScan = s.Settings.GithubDeepMode + if settings.Token != "null" && strings.TrimSpace(settings.Token) != "" { + settings.Client = settings.Client.WithAuthToken(settings.Token) + } + settings.Silent = true + + queryText = strings.TrimSpace(queryText) + + if column == "email" || strings.HasSuffix(column, "_email") || + column == "username" || strings.HasSuffix(column, "_username") || + column == "" || column == "all" { + if isValidEmail(queryText) { + settings.Target = queryText + settings.TargetType = github_recon_settings.TargetEmail + result := recon_email.Email(settings) + gr.EmailResult = &result + } else if isValidUsername(queryText) { + settings.Target = queryText + settings.TargetType = github_recon_settings.TargetUsername + result, err := recon_username.Username(settings) + if err != nil { + gr.Error = err.Error() + } + if result.User.Username == "" { + gr.UsernameResult = nil + } else { + gr.UsernameResult = &result + } + } else { + return nil + } + } else { + return nil + } + + gr.Duration = time.Since(now) + return &gr +} + +func isValidEmail(email string) bool { + if !strings.Contains(email, "@") || !strings.Contains(email, ".") { + return false + } + if strings.HasPrefix(email, "@") || strings.HasSuffix(email, "@") { + return false + } + if strings.Contains(email, " ") { + return false + } + return true +} + +func isValidUsername(username string) bool { + if len(username) < 1 || len(username) > 39 { + return false + } + if strings.Contains(username, " ") { + return false + } + return true +} diff --git a/back/search/search.go b/back/search/search.go new file mode 100644 index 0000000..7677096 --- /dev/null +++ b/back/search/search.go @@ -0,0 +1,68 @@ +package search + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "sync" + "time" + + "github.com/anotherhadi/eleakxir/backend/search/dataleak" + "github.com/anotherhadi/eleakxir/backend/search/osint" + "github.com/anotherhadi/eleakxir/backend/server" +) + +type Query struct { + Text string + Column string // The column to search in (e.g., "email", "password", etc. + ExactMatch bool // Whether to search for an exact match +} + +type Result struct { + Id string + Date time.Time + Status string // "pending", "completed" + Query Query + + LeakResult dataleak.LeakResult + GithubResult osint.GithubResult +} + +func Search(s *server.Server, q Query, r *Result, mu *sync.RWMutex) { + var wg sync.WaitGroup + + mu.Lock() + r.Date = time.Now() + r.Status = "pending" + r.Query = q + mu.Unlock() + + wg.Add(2) + + go func() { + leakResult := dataleak.Search(s, q.Text, q.Column, q.ExactMatch) + mu.Lock() + r.LeakResult = leakResult + mu.Unlock() + wg.Done() + }() + + go func() { + githubResult := osint.Search(s, q.Text, q.Column) + mu.Lock() + r.GithubResult = *githubResult + mu.Unlock() + wg.Done() + }() + + wg.Wait() + + mu.Lock() + r.Status = "completed" + mu.Unlock() +} + +func EncodeQueryID(q Query, dataleaksCount uint64) string { + raw, _ := json.Marshal(q) + return fmt.Sprintf("%d:%s", dataleaksCount, base64.URLEncoding.EncodeToString(raw)) +} diff --git a/back/server/dataleak.go b/back/server/dataleak.go new file mode 100644 index 0000000..a2903ac --- /dev/null +++ b/back/server/dataleak.go @@ -0,0 +1,111 @@ +package server + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + + "github.com/charmbracelet/log" +) + +type Dataleak struct { + Path string + Name string + Columns []string + Length uint64 + Size uint64 +} + +const CACHE_FILENAME = "dataleaks_cache.json" + +// TODO: check os.FileInfo.ModTime() to see if the file has changed since last cache update + +func Cache(s *Server) error { + if len(s.Settings.Folders) == 0 { + return nil + } + if s.Settings.CacheFolder == "" { + s.Settings.CacheFolder = s.Settings.Folders[0] + } + if err := createDirectoryIfNotExists(s.Settings.CacheFolder); err != nil { + return err + } + + cacheFile := filepath.Join(s.Settings.CacheFolder, CACHE_FILENAME) + dataleaks := []Dataleak{} + + data, err := os.ReadFile(cacheFile) + if err == nil { + if err := json.Unmarshal(data, &dataleaks); err != nil { + log.Warn("Failed to unmarshal dataleaks cache", "error", err) + } + } else { + log.Warn("Failed to read dataleaks cache file", "error", err) + } + + // Filter out non-existent files + filteredDataleaks := []Dataleak{} + writeOutput := false + for _, d := range dataleaks { + if _, err := os.Stat(d.Path); err == nil { + filteredDataleaks = append(filteredDataleaks, d) + } else if os.IsNotExist(err) { + log.Info("Removing non-existent file from cache", "path", d.Path) + writeOutput = true + } else { + log.Error("Error checking file existence", "path", d.Path, "error", err) + } + } + dataleaks = filteredDataleaks + + // Create a map for quick lookups + dataleakMap := make(map[string]struct{}, len(dataleaks)) + for _, d := range dataleaks { + dataleakMap[d.Path] = struct{}{} + } + + // Add new files + parquetFiles := getAllParquetFiles(s.Settings.Folders) + for _, p := range parquetFiles { + if _, found := dataleakMap[p]; found { + continue + } + + writeOutput = true + dataleaks = append(dataleaks, getDataleak(*s, p)) + } + + if writeOutput { + data, err := json.MarshalIndent(dataleaks, "", " ") + if err != nil { + return fmt.Errorf("error marshalling cache: %w", err) + } + if err := os.WriteFile(cacheFile, data, 0644); err != nil { + return fmt.Errorf("error writing cache: %w", err) + } + } + + s.Dataleaks = &dataleaks + totalDataleaks := uint64(len(dataleaks)) + totalRows := uint64(0) + totalSize := uint64(0) + for _, d := range dataleaks { + totalRows += d.Length + totalSize += d.Size + } + s.TotalDataleaks = &totalDataleaks + s.TotalSize = &totalSize + s.TotalRows = &totalRows + return nil +} + +func getDataleak(s Server, path string) Dataleak { + return Dataleak{ + Path: path, + Name: FormatParquetName(path), + Columns: getParquetColumns(s, path), + Length: getParquetLength(s, path), + Size: getFileSize(path), + } +} diff --git a/back/server/server.go b/back/server/server.go new file mode 100644 index 0000000..e16fdd6 --- /dev/null +++ b/back/server/server.go @@ -0,0 +1,61 @@ +package server + +import ( + "database/sql" + "sync" + "time" + + "github.com/charmbracelet/log" + "github.com/gin-gonic/gin" + _ "github.com/marcboeker/go-duckdb" +) + +type Server struct { + Settings ServerSettings + + Dataleaks *[]Dataleak + + TotalRows *uint64 + TotalDataleaks *uint64 + TotalSize *uint64 // MB + + Router *gin.Engine + Duckdb *sql.DB + Mu *sync.RWMutex +} + +func NewServer() *Server { + zero := uint64(0) + emptyDataleak := []Dataleak{} + s := &Server{ + Settings: LoadServerSettings(), + Mu: &sync.RWMutex{}, + TotalDataleaks: &zero, + TotalRows: &zero, + TotalSize: &zero, + Dataleaks: &emptyDataleak, + } + + var err error + + s.Duckdb, err = sql.Open("duckdb", "") + if err != nil { + panic(err) + } + + err = Cache(s) + if err != nil { + panic(err) + } + go func() { + for { + time.Sleep(s.Settings.ReloadDataleaksInterval) + err := Cache(s) + if err != nil { + log.Error(err) + } + } + }() + + return s +} diff --git a/back/server/settings.go b/back/server/settings.go new file mode 100644 index 0000000..8a3c04f --- /dev/null +++ b/back/server/settings.go @@ -0,0 +1,129 @@ +package server + +import ( + "os" + "strconv" + "strings" + "time" + + github_recon_settings "github.com/anotherhadi/github-recon/settings" +) + +type ServerSettings struct { + Port int `json:"-"` // Port to run the server on + Debug bool + Password string `json:"-"` // Do not expose the password in JSON + MinimumQueryLength int + MaxCacheDuration time.Duration // Delete a search from the cache after this duration + + // Dataleaks + Folders []string // Folders to search in for parquets, recursive + CacheFolder string + BaseColumns []string // Use these columns when column="all" + Limit int // Limit number of rows returned + ReloadDataleaksInterval time.Duration // Reload dataleaks files from disk every X + + // OSINT Tools + GithubRecon bool // Activate github-recon OSINT tool + GithubToken string `json:"-"` // Github token for github-recon + GithubTokenLoaded bool + GithubDeepMode bool // Deep mode for github-recon +} + +func LoadServerSettings() ServerSettings { + ss := ServerSettings{ + Port: getEnvPortOrDefault("PORT", 9198), + Debug: getEnvBoolOrDefault("DEBUG", false), + Password: getEnvStringOrDefault("PASSWORD", ""), + MinimumQueryLength: getEnvIntOrDefault("MINIMUM_QUERY_LENGTH", 3), + MaxCacheDuration: getEnvDurationOrDefault("MAX_CACHE_DURATION", 24*time.Hour), + + // Dataleaks + Folders: getEnvStringListOrDefault("DATALEAKS_FOLDERS", []string{}), + CacheFolder: getEnvStringOrDefault("DATALEAKS_CACHE_FOLDER", ""), + BaseColumns: getEnvStringListOrDefault("BASE_COLUMNS", []string{"email", "username", "password", "full_name", "phone", "url"}), + Limit: getEnvIntOrDefault("LIMIT", 100), + ReloadDataleaksInterval: getEnvDurationOrDefault("RELOAD_DATALEAKS_INTERVAL", 20*time.Minute), + + // OSINT Tools + GithubRecon: getEnvBoolOrDefault("GITHUB_RECON", true), + GithubToken: getEnvStringOrDefault("GITHUB_TOKEN", "null"), + GithubDeepMode: getEnvBoolOrDefault("GITHUB_DEEP_MODE", false), + } + + if ss.GithubToken == "null" || strings.TrimSpace(ss.GithubToken) == "" { + ss.GithubToken = github_recon_settings.GetToken() + } + + if ss.GithubToken != "null" && strings.TrimSpace(ss.GithubToken) != "" { + ss.GithubTokenLoaded = true + } + + return ss +} + +func getEnvStringOrDefault(envKey, defaultValue string) string { + value := strings.TrimSpace(os.Getenv(envKey)) + if value == "" { + return defaultValue + } + return value +} + +func getEnvBoolOrDefault(envKey string, defaultValue bool) bool { + value := strings.TrimSpace(os.Getenv(envKey)) + if value == "" { + return defaultValue + } + value = strings.ToLower(value) + if value == "true" || value == "1" { + return true + } else if value == "false" || value == "0" { + return false + } + return defaultValue +} + +func getEnvDurationOrDefault(envKey string, defaultValue time.Duration) time.Duration { + v := getEnvStringOrDefault(envKey, "") + if v == "" { + return defaultValue + } + t, err := time.ParseDuration(v) + if err != nil { + return defaultValue + } + return t +} + +func getEnvStringListOrDefault(envKey string, defaultValue []string) []string { + value := strings.TrimSpace(os.Getenv(envKey)) + if value == "" { + return defaultValue + } + l := strings.Split(value, ",") + for i := range l { + l[i] = strings.TrimSpace(l[i]) + } + return l +} + +func getEnvIntOrDefault(envKey string, defaultValue int) int { + value := strings.TrimSpace(os.Getenv(envKey)) + if value == "" { + return defaultValue + } + i, err := strconv.Atoi(value) + if err != nil { + return defaultValue + } + return i +} + +func getEnvPortOrDefault(envKey string, defaultValue int) int { + p := getEnvIntOrDefault(envKey, defaultValue) + if p <= 0 || p >= 65534 { + return defaultValue + } + return p +} diff --git a/back/server/utils.go b/back/server/utils.go new file mode 100644 index 0000000..13678dc --- /dev/null +++ b/back/server/utils.go @@ -0,0 +1,131 @@ +package server + +import ( + "database/sql" + "fmt" + "os" + "path/filepath" + "strings" +) + +func getParquetColumns(s Server, path string) []string { + query := fmt.Sprintf("DESCRIBE SELECT * FROM '%s';", path) + + rows, err := s.Duckdb.Query(query) + if err != nil { + return []string{} + } + defer rows.Close() + + var columns []string + + for rows.Next() { + var columnName string + var columnType string + var nullable string + + var key sql.NullString + var defaultValue sql.NullString + var extra sql.NullString + + if err := rows.Scan(&columnName, &columnType, &nullable, &key, &defaultValue, &extra); err != nil { + return []string{} + } + columns = append(columns, columnName) + } + + if err = rows.Err(); err != nil { + return []string{} + } + + if len(columns) == 0 { + return []string{} + } + + return columns +} + +func getParquetLength(s Server, path string) uint64 { + query := fmt.Sprintf("SELECT COUNT(*) FROM '%s';", path) + + row := s.Duckdb.QueryRow(query) + + var count uint64 + if err := row.Scan(&count); err != nil { + return 0 + } + + return count +} + +// Walk through the given folder and its subfolders to find all parquet files +// Return a list of path +func getAllParquetFiles(folders []string) []string { + var paths []string + for _, baseDir := range folders { + _ = filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() || !strings.HasSuffix(info.Name(), ".parquet") { + return err + } + paths = append(paths, path) + return nil + }) + } + return paths +} + +func getFileSize(path string) uint64 { + info, err := os.Stat(path) + if err != nil { + return 0 + } + return uint64(info.Size() / (1024 * 1024)) // MB +} + +func FormatParquetName(path string) string { + _, file := filepath.Split(path) + fileName := strings.TrimSuffix(file, ".parquet") + + parts := strings.Split(fileName, "-") + sourceName := parts[0] + var blocks []string + + for _, part := range parts[1:] { + if strings.HasPrefix(part, "date_") { + dateStr := strings.TrimPrefix(part, "date_") + dateStr = strings.ReplaceAll(dateStr, "_", "/") + blocks = append(blocks, fmt.Sprintf("date: %s", dateStr)) + } else if strings.HasPrefix(part, "source_") { + sourceStr := strings.TrimPrefix(part, "source_") + blocks = append(blocks, fmt.Sprintf("source: %s", sourceStr)) + } else if strings.HasPrefix(part, "notes_") { + noteStr := strings.TrimPrefix(part, "notes_") + noteStr = strings.ReplaceAll(noteStr, "_", " ") + blocks = append(blocks, noteStr) + } + } + + sourceName = strings.ReplaceAll(sourceName, "_", " ") + sourceWords := strings.Fields(sourceName) + for i, word := range sourceWords { + if len(word) > 0 { + sourceWords[i] = strings.ToUpper(string(word[0])) + word[1:] + } + } + formattedSourceName := strings.Join(sourceWords, " ") + + if len(blocks) > 0 { + return fmt.Sprintf("%s (%s)", formattedSourceName, strings.Join(blocks, ", ")) + } + + return formattedSourceName +} + +func createDirectoryIfNotExists(path string) error { + if _, err := os.Stat(path); os.IsNotExist(err) { + if err := os.MkdirAll(path, 0755); err != nil { + return err + } + } + return nil +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..553b50a --- /dev/null +++ b/flake.lock @@ -0,0 +1,43 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1757487488, + "narHash": "sha256-zwE/e7CuPJUWKdvvTCB7iunV4E/+G0lKfv4kk/5Izdg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "ab0f3607a6c7486ea22229b92ed2d355f1482ee0", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "systems": "systems" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..d4dca0f --- /dev/null +++ b/flake.nix @@ -0,0 +1,58 @@ +{ + description = "Flake for eleakxir"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + systems.url = "github:nix-systems/default"; + }; + + outputs = { + systems, + self, + nixpkgs, + ... + }: let + eachSystem = nixpkgs.lib.genAttrs (import systems); + + importBackend = system: + import ./nix/back.nix { + pkgs = nixpkgs.legacyPackages.${system}; + lib = nixpkgs.lib; + inherit self; + }; + + importUtils = system: + import ./nix/leak-utils.nix { + pkgs = nixpkgs.legacyPackages.${system}; + lib = nixpkgs.lib; + inherit self; + }; + + importDevShell = system: + import ./nix/devshell.nix { + pkgs = nixpkgs.legacyPackages.${system}; + lib = nixpkgs.lib; + inherit self; + }; + in { + packages = eachSystem (system: { + backend = (importBackend system).package; + leak-utils = (importUtils system).package; + }); + + devShells = eachSystem (system: { + default = (importDevShell system).devShell; + }); + + nixosModules.eleakxir = { + config, + lib, + pkgs, + ... + }: + (importBackend pkgs.system).nixosModule { + inherit config lib pkgs; + inherit self; + }; + }; +} diff --git a/front/.gitignore b/front/.gitignore new file mode 100644 index 0000000..3aee7df --- /dev/null +++ b/front/.gitignore @@ -0,0 +1,26 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +.router diff --git a/front/bun.lock b/front/bun.lock new file mode 100644 index 0000000..0d205dd --- /dev/null +++ b/front/bun.lock @@ -0,0 +1,377 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "svelte-app", + "dependencies": { + "@lucide/svelte": "^0.542.0", + "@tailwindcss/vite": "^4.1.12", + "axios": "^1.12.1", + "clsx": "^2.1.1", + "marked": "^16.3.0", + "path": "^0.12.7", + "sv-router": "latest", + "svelte-sonner": "^1.0.5", + "tailwind-merge": "^3.3.1", + "tailwindcss": "^4.1.12", + "theme-change": "^2.5.0", + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.3", + "@tailwindcss/typography": "^0.5.18", + "daisyui": "^5.1.6", + "svelte": "^5.20.2", + "svelte-check": "^4.1.4", + "typescript": "~5.7.2", + "vite": "^6.2.0", + }, + }, + }, + "packages": { + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.9", "", { "os": "aix", "cpu": "ppc64" }, "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.9", "", { "os": "android", "cpu": "arm" }, "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.9", "", { "os": "android", "cpu": "arm64" }, "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.9", "", { "os": "android", "cpu": "x64" }, "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.9", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.9", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.9", "", { "os": "linux", "cpu": "arm" }, "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.9", "", { "os": "linux", "cpu": "ia32" }, "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.9", "", { "os": "linux", "cpu": "ppc64" }, "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.9", "", { "os": "linux", "cpu": "s390x" }, "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.9", "", { "os": "linux", "cpu": "x64" }, "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.9", "", { "os": "none", "cpu": "arm64" }, "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.9", "", { "os": "none", "cpu": "x64" }, "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.9", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.9", "", { "os": "openbsd", "cpu": "x64" }, "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.9", "", { "os": "none", "cpu": "arm64" }, "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.9", "", { "os": "sunos", "cpu": "x64" }, "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.9", "", { "os": "win32", "cpu": "ia32" }, "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.9", "", { "os": "win32", "cpu": "x64" }, "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ=="], + + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.30", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q=="], + + "@lucide/svelte": ["@lucide/svelte@0.542.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-NuWttxTVfMSURpOxcKiKvoCtma3JtEpcJWzF/0cO69saZfXlv6G8NYAvEEGLmk75YPl+I+ROe+F97WhddM8r2A=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.50.0", "", { "os": "android", "cpu": "arm" }, "sha512-lVgpeQyy4fWN5QYebtW4buT/4kn4p4IJ+kDNB4uYNT5b8c8DLJDg6titg20NIg7E8RWwdWZORW6vUFfrLyG3KQ=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.50.0", "", { "os": "android", "cpu": "arm64" }, "sha512-2O73dR4Dc9bp+wSYhviP6sDziurB5/HCym7xILKifWdE9UsOe2FtNcM+I4xZjKrfLJnq5UR8k9riB87gauiQtw=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.50.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vwSXQN8T4sKf1RHr1F0s98Pf8UPz7pS6P3LG9NSmuw0TVh7EmaE+5Ny7hJOZ0M2yuTctEsHHRTMi2wuHkdS6Hg=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.50.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cQp/WG8HE7BCGyFVuzUg0FNmupxC+EPZEwWu2FCGGw5WDT1o2/YlENbm5e9SMvfDFR6FRhVCBePLqj0o8MN7Vw=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.50.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-UR1uTJFU/p801DvvBbtDD7z9mQL8J80xB0bR7DqW7UGQHRm/OaKzp4is7sQSdbt2pjjSS72eAtRh43hNduTnnQ=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.50.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-G/DKyS6PK0dD0+VEzH/6n/hWDNPDZSMBmqsElWnCRGrYOb2jC0VSupp7UAHHQ4+QILwkxSMaYIbQ72dktp8pKA=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.50.0", "", { "os": "linux", "cpu": "arm" }, "sha512-u72Mzc6jyJwKjJbZZcIYmd9bumJu7KNmHYdue43vT1rXPm2rITwmPWF0mmPzLm9/vJWxIRbao/jrQmxTO0Sm9w=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.50.0", "", { "os": "linux", "cpu": "arm" }, "sha512-S4UefYdV0tnynDJV1mdkNawp0E5Qm2MtSs330IyHgaccOFrwqsvgigUD29uT+B/70PDY1eQ3t40+xf6wIvXJyg=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.50.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-1EhkSvUQXJsIhk4msxP5nNAUWoB4MFDHhtc4gAYvnqoHlaL9V3F37pNHabndawsfy/Tp7BPiy/aSa6XBYbaD1g=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.50.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-EtBDIZuDtVg75xIPIK1l5vCXNNCIRM0OBPUG+tbApDuJAy9mKago6QxX+tfMzbCI6tXEhMuZuN1+CU8iDW+0UQ=="], + + "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.50.0", "", { "os": "linux", "cpu": "none" }, "sha512-BGYSwJdMP0hT5CCmljuSNx7+k+0upweM2M4YGfFBjnFSZMHOLYR0gEEj/dxyYJ6Zc6AiSeaBY8dWOa11GF/ppQ=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.50.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-I1gSMzkVe1KzAxKAroCJL30hA4DqSi+wGc5gviD0y3IL/VkvcnAqwBf4RHXHyvH66YVHxpKO8ojrgc4SrWAnLg=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.50.0", "", { "os": "linux", "cpu": "none" }, "sha512-bSbWlY3jZo7molh4tc5dKfeSxkqnf48UsLqYbUhnkdnfgZjgufLS/NTA8PcP/dnvct5CCdNkABJ56CbclMRYCA=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.50.0", "", { "os": "linux", "cpu": "none" }, "sha512-LSXSGumSURzEQLT2e4sFqFOv3LWZsEF8FK7AAv9zHZNDdMnUPYH3t8ZlaeYYZyTXnsob3htwTKeWtBIkPV27iQ=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.50.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-CxRKyakfDrsLXiCyucVfVWVoaPA4oFSpPpDwlMcDFQvrv3XY6KEzMtMZrA+e/goC8xxp2WSOxHQubP8fPmmjOQ=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.50.0", "", { "os": "linux", "cpu": "x64" }, "sha512-8PrJJA7/VU8ToHVEPu14FzuSAqVKyo5gg/J8xUerMbyNkWkO9j2ExBho/68RnJsMGNJq4zH114iAttgm7BZVkA=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.50.0", "", { "os": "linux", "cpu": "x64" }, "sha512-SkE6YQp+CzpyOrbw7Oc4MgXFvTw2UIBElvAvLCo230pyxOLmYwRPwZ/L5lBe/VW/qT1ZgND9wJfOsdy0XptRvw=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.50.0", "", { "os": "none", "cpu": "arm64" }, "sha512-PZkNLPfvXeIOgJWA804zjSFH7fARBBCpCXxgkGDRjjAhRLOR8o0IGS01ykh5GYfod4c2yiiREuDM8iZ+pVsT+Q=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.50.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-q7cIIdFvWQoaCbLDUyUc8YfR3Jh2xx3unO8Dn6/TTogKjfwrax9SyfmGGK6cQhKtjePI7jRfd7iRYcxYs93esg=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.50.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-XzNOVg/YnDOmFdDKcxxK410PrcbcqZkBmz+0FicpW5jtjKQxcW1BZJEQOF0NJa6JO7CZhett8GEtRN/wYLYJuw=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.50.0", "", { "os": "win32", "cpu": "x64" }, "sha512-xMmiWRR8sp72Zqwjgtf3QbZfF1wdh8X2ABu3EaozvZcyHJeU0r+XAnXdKgs4cCAp6ORoYoCygipYP1mjmbjrsg=="], + + "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.5", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ=="], + + "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@5.1.1", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", "debug": "^4.4.1", "deepmerge": "^4.3.1", "kleur": "^4.1.5", "magic-string": "^0.30.17", "vitefu": "^1.0.6" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.0.0" } }, "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ=="], + + "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@4.0.1", "", { "dependencies": { "debug": "^4.3.7" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^5.0.0", "svelte": "^5.0.0", "vite": "^6.0.0" } }, "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw=="], + + "@tailwindcss/node": ["@tailwindcss/node@4.1.12", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.5.1", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.12" } }, "sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.12", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.12", "@tailwindcss/oxide-darwin-arm64": "4.1.12", "@tailwindcss/oxide-darwin-x64": "4.1.12", "@tailwindcss/oxide-freebsd-x64": "4.1.12", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.12", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.12", "@tailwindcss/oxide-linux-arm64-musl": "4.1.12", "@tailwindcss/oxide-linux-x64-gnu": "4.1.12", "@tailwindcss/oxide-linux-x64-musl": "4.1.12", "@tailwindcss/oxide-wasm32-wasi": "4.1.12", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.12", "@tailwindcss/oxide-win32-x64-msvc": "4.1.12" } }, "sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.12", "", { "os": "android", "cpu": "arm64" }, "sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.12", "", { "os": "linux", "cpu": "arm" }, "sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.12", "", { "os": "linux", "cpu": "x64" }, "sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.12", "", { "os": "linux", "cpu": "x64" }, "sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A=="], + + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.12", "", { "dependencies": { "@emnapi/core": "^1.4.5", "@emnapi/runtime": "^1.4.5", "@emnapi/wasi-threads": "^1.0.4", "@napi-rs/wasm-runtime": "^0.2.12", "@tybys/wasm-util": "^0.10.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.12", "", { "os": "win32", "cpu": "x64" }, "sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA=="], + + "@tailwindcss/typography": ["@tailwindcss/typography@0.5.18", "", { "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-dDIgwZOlf+tVkZ7A029VvQ1+ngKATENDjMEx2N35s2yPjfTS05RWSM8ilhEWSa5DMJ6ci2Ha9WNZEd2GQjrdQg=="], + + "@tailwindcss/vite": ["@tailwindcss/vite@4.1.12", "", { "dependencies": { "@tailwindcss/node": "4.1.12", "@tailwindcss/oxide": "4.1.12", "tailwindcss": "4.1.12" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-4pt0AMFDx7gzIrAOIYgYP0KCBuKWqyW8ayrdiLEjoJTT4pKTjrzG/e4uzWtTLDziC+66R9wbUqZBccJalSE5vQ=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "axios": ["axios@1.12.1", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-Kn4kbSXpkFHCGE6rBFNwIv0GQs4AvDT80jlveJDKFxjbTYMUeB4QtsdPCv6H8Cm19Je7IU6VFtRl2zWZI0rudQ=="], + + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "daisyui": ["daisyui@5.1.6", "", {}, "sha512-KCzv25f+3lwWbfnPZZG9Xo0kSGO1NSysyIiS5AoCtDotIrvvArggHklCey1Fg6U2gZuqxsi2rptT1q3khoYCMw=="], + + "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + + "detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "esbuild": ["esbuild@0.25.9", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.9", "@esbuild/android-arm": "0.25.9", "@esbuild/android-arm64": "0.25.9", "@esbuild/android-x64": "0.25.9", "@esbuild/darwin-arm64": "0.25.9", "@esbuild/darwin-x64": "0.25.9", "@esbuild/freebsd-arm64": "0.25.9", "@esbuild/freebsd-x64": "0.25.9", "@esbuild/linux-arm": "0.25.9", "@esbuild/linux-arm64": "0.25.9", "@esbuild/linux-ia32": "0.25.9", "@esbuild/linux-loong64": "0.25.9", "@esbuild/linux-mips64el": "0.25.9", "@esbuild/linux-ppc64": "0.25.9", "@esbuild/linux-riscv64": "0.25.9", "@esbuild/linux-s390x": "0.25.9", "@esbuild/linux-x64": "0.25.9", "@esbuild/netbsd-arm64": "0.25.9", "@esbuild/netbsd-x64": "0.25.9", "@esbuild/openbsd-arm64": "0.25.9", "@esbuild/openbsd-x64": "0.25.9", "@esbuild/openharmony-arm64": "0.25.9", "@esbuild/sunos-x64": "0.25.9", "@esbuild/win32-arm64": "0.25.9", "@esbuild/win32-ia32": "0.25.9", "@esbuild/win32-x64": "0.25.9" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g=="], + + "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], + + "esrap": ["esrap@2.1.0", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], + + "form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "inherits": ["inherits@2.0.3", "", {}, "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw=="], + + "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], + + "jiti": ["jiti@2.5.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="], + + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + + "lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="], + + "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="], + + "magic-string": ["magic-string@0.30.18", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ=="], + + "marked": ["marked@16.3.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-K3UxuKu6l6bmA5FUwYho8CfJBlsUWAooKtdGgMcERSpF7gcBUrCGsLH7wDaaNOzwq18JzSUDyoEb/YsrqMac3w=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "minizlib": ["minizlib@3.0.2", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA=="], + + "mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="], + + "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "path": ["path@0.12.7", "", { "dependencies": { "process": "^0.11.1", "util": "^0.10.3" } }, "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="], + + "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], + + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "rollup": ["rollup@4.50.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.50.0", "@rollup/rollup-android-arm64": "4.50.0", "@rollup/rollup-darwin-arm64": "4.50.0", "@rollup/rollup-darwin-x64": "4.50.0", "@rollup/rollup-freebsd-arm64": "4.50.0", "@rollup/rollup-freebsd-x64": "4.50.0", "@rollup/rollup-linux-arm-gnueabihf": "4.50.0", "@rollup/rollup-linux-arm-musleabihf": "4.50.0", "@rollup/rollup-linux-arm64-gnu": "4.50.0", "@rollup/rollup-linux-arm64-musl": "4.50.0", "@rollup/rollup-linux-loongarch64-gnu": "4.50.0", "@rollup/rollup-linux-ppc64-gnu": "4.50.0", "@rollup/rollup-linux-riscv64-gnu": "4.50.0", "@rollup/rollup-linux-riscv64-musl": "4.50.0", "@rollup/rollup-linux-s390x-gnu": "4.50.0", "@rollup/rollup-linux-x64-gnu": "4.50.0", "@rollup/rollup-linux-x64-musl": "4.50.0", "@rollup/rollup-openharmony-arm64": "4.50.0", "@rollup/rollup-win32-arm64-msvc": "4.50.0", "@rollup/rollup-win32-ia32-msvc": "4.50.0", "@rollup/rollup-win32-x64-msvc": "4.50.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-/Zl4D8zPifNmyGzJS+3kVoyXeDeT/GrsJM94sACNg9RtUE0hrHa1bNPtRSrfHTMH5HjRzce6K7rlTh3Khiw+pw=="], + + "runed": ["runed@0.28.0", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ=="], + + "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "sv-router": ["sv-router@0.8.1", "", { "dependencies": { "esm-env": "^1.2.2" }, "peerDependencies": { "svelte": "^5" }, "bin": { "sv-router": "src/cli/index.js" } }, "sha512-evUoE6TFEB89u8FzgEXiqj8aCTTCbCrWLBbfPe8qZxbNUT5I0sARoTLIZJtBNuumXe+FxpCCDrb8v/d6va+ZVQ=="], + + "svelte": ["svelte@5.38.6", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^2.1.0", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-ltBPlkvqk3bgCK7/N323atUpP3O3Y+DrGV4dcULrsSn4fZaaNnOmdplNznwfdWclAgvSr5rxjtzn/zJhRm6TKg=="], + + "svelte-check": ["svelte-check@4.3.1", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-lkh8gff5gpHLjxIV+IaApMxQhTGnir2pNUAqcNgeKkvK5bT/30Ey/nzBxNLDlkztCH4dP7PixkMt9SWEKFPBWg=="], + + "svelte-sonner": ["svelte-sonner@1.0.5", "", { "dependencies": { "runed": "^0.28.0" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-9dpGPFqKb/QWudYqGnEz93vuY+NgCEvyNvxoCLMVGw6sDN/3oVeKV1xiEirW2E1N3vJEyj5imSBNOGltQHA7mg=="], + + "tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="], + + "tailwindcss": ["tailwindcss@4.1.12", "", {}, "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA=="], + + "tapable": ["tapable@2.2.3", "", {}, "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg=="], + + "tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="], + + "theme-change": ["theme-change@2.5.0", "", {}, "sha512-B/UdsgdHAGhSKHTAQnxg/etN0RaMDpehuJmZIjLMDVJ6DGIliRHGD6pODi1CXLQAN9GV0GSyB3G6yCuK05PkPQ=="], + + "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], + + "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], + + "util": ["util@0.10.4", "", { "dependencies": { "inherits": "2.0.3" } }, "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="], + + "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], + + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + + "zimmerframe": ["zimmerframe@1.1.2", "", {}, "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" }, "bundled": true }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], + + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="], + + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + } +} diff --git a/front/index.html b/front/index.html new file mode 100644 index 0000000..9c04752 --- /dev/null +++ b/front/index.html @@ -0,0 +1,14 @@ + + + + + + + Eleakxir + + + +
+ + + diff --git a/front/package.json b/front/package.json new file mode 100644 index 0000000..05077cc --- /dev/null +++ b/front/package.json @@ -0,0 +1,34 @@ +{ + "name": "svelte-app", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-check", + "postinstall": "sv-router" + }, + "dependencies": { + "@lucide/svelte": "^0.542.0", + "@tailwindcss/vite": "^4.1.12", + "axios": "^1.12.1", + "clsx": "^2.1.1", + "marked": "^16.3.0", + "path": "^0.12.7", + "sv-router": "latest", + "svelte-sonner": "^1.0.5", + "tailwind-merge": "^3.3.1", + "tailwindcss": "^4.1.12", + "theme-change": "^2.5.0" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.3", + "@tailwindcss/typography": "^0.5.18", + "daisyui": "^5.1.6", + "svelte": "^5.20.2", + "svelte-check": "^4.1.4", + "typescript": "~5.7.2", + "vite": "^6.2.0" + } +} diff --git a/front/public/favicon.svg b/front/public/favicon.svg new file mode 100644 index 0000000..449b4ce --- /dev/null +++ b/front/public/favicon.svg @@ -0,0 +1,3 @@ + + + diff --git a/front/public/l.png b/front/public/l.png new file mode 100644 index 0000000..14f3ca3 Binary files /dev/null and b/front/public/l.png differ diff --git a/front/src/App.svelte b/front/src/App.svelte new file mode 100644 index 0000000..6eabc04 --- /dev/null +++ b/front/src/App.svelte @@ -0,0 +1,6 @@ + + + diff --git a/front/src/app.css b/front/src/app.css new file mode 100644 index 0000000..f096603 --- /dev/null +++ b/front/src/app.css @@ -0,0 +1,94 @@ +@import "tailwindcss"; +@plugin "@tailwindcss/typography"; + +@plugin "daisyui" { + themes: + light, + dark --prefersdark --default; +} + +@plugin "daisyui/theme" { + name: "dark"; + default: true; /* set as default */ + prefersdark: true; /* set as default dark mode (prefers-color-scheme:dark) */ + color-scheme: dark; /* color of browser-provided UI */ + + --color-base-100: oklch(0.2096 0.0275 290.36); + --color-base-200: oklch(0.1896 0.0242 287.67); + --color-base-300: oklch(0.1674 0.0229 292.08); + --color-base-content: oklch(0.841 0.0056 297.71); + + --color-primary: oklch(0.5454 0.2756 292.04); + --color-primary-content: oklch(0.9074 0.049167 293.0386); + + --color-secondary: oklch(0.5103 0.2756 292.04); + --color-secondary-content: oklch(0.9074 0.049167 293.0386); + + --color-accent: oklch(0.6241 0.1575 277.95); + --color-accent-content: oklch(0.1248 0.031 280.93); + + --color-neutral: oklch(0.2813 0.0153 269.13); + --color-neutral-content: oklch(0.8574 0.003 264.54); + + --radius-selector: 1rem; + --radius-field: 0.5rem; + --radius-box: 1rem; + --size-selector: 0.25rem; + --size-field: 0.25rem; + --border: 1px; + --depth: 0; + --noise: 0; +} + +@plugin "daisyui/theme" { + name: "light"; + default: false; /* set as default */ + prefersdark: false; /* set as default dark mode (prefers-color-scheme:dark) */ + color-scheme: light; /* color of browser-provided UI */ + + --color-primary: oklch(0.5454 0.2756 292.04); + --color-primary-content: oklch(0.9074 0.049167 293.0386); + + --color-secondary: oklch(0.5103 0.2756 292.04); + --color-secondary-content: oklch(0.9074 0.049167 293.0386); + + --color-accent: oklch(0.6241 0.1575 277.95); + --color-accent-content: oklch(0.1248 0.031 280.93); + + --radius-selector: 1rem; + --radius-field: 0.5rem; + --radius-box: 1rem; + --size-selector: 0.25rem; + --size-field: 0.25rem; + --border: 1px; + --depth: 0; + --noise: 0; +} + +main { + @apply mx-auto w-full px-6; +} + +.h1 { + @apply scroll-m-20 text-4xl sm:text-5xl font-extrabold tracking-tight lg:text-5xl; +} + +.h2 { + @apply scroll-m-20 text-3xl sm:text-4xl font-semibold tracking-tight transition-colors; +} + +.h3 { + @apply scroll-m-20 text-2xl font-semibold tracking-tight transition-colors; +} + +.h4 { + @apply scroll-m-20 text-xl font-semibold tracking-tight transition-colors; +} + +.h5 { + @apply scroll-m-20 text-lg font-semibold tracking-tight transition-colors; +} + +.h6 { + @apply scroll-m-20 text-base font-semibold tracking-tight transition-colors; +} diff --git a/front/src/lib/components/accordion.svelte b/front/src/lib/components/accordion.svelte new file mode 100644 index 0000000..22c952c --- /dev/null +++ b/front/src/lib/components/accordion.svelte @@ -0,0 +1,79 @@ + + + +{#if children != null} + {#if isOpen} +
  • + {@render children()} +
  • + {/if} +{/if} diff --git a/front/src/lib/components/dark-mode-toggle.svelte b/front/src/lib/components/dark-mode-toggle.svelte new file mode 100644 index 0000000..7b9f688 --- /dev/null +++ b/front/src/lib/components/dark-mode-toggle.svelte @@ -0,0 +1,29 @@ + + + diff --git a/front/src/lib/components/index/AnimatedBeam.svelte b/front/src/lib/components/index/AnimatedBeam.svelte new file mode 100644 index 0000000..10cbb78 --- /dev/null +++ b/front/src/lib/components/index/AnimatedBeam.svelte @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + diff --git a/front/src/lib/components/index/AnimatedBeamMultiple.svelte b/front/src/lib/components/index/AnimatedBeamMultiple.svelte new file mode 100644 index 0000000..cd991d4 --- /dev/null +++ b/front/src/lib/components/index/AnimatedBeamMultiple.svelte @@ -0,0 +1,125 @@ + + +
    +
    +
    + + +
    + +
    +
    + + +
    + +
    +
    + + +
    + +
    +
    + + +
    + +
    +
    + + +
    + +
    +
    +
    +
    + + +
    + + + +
    +
    +
    +
    + + +
    + +
    +
    +
    +
    + + + + + + +
    diff --git a/front/src/lib/components/index/Circle.svelte b/front/src/lib/components/index/Circle.svelte new file mode 100644 index 0000000..c341878 --- /dev/null +++ b/front/src/lib/components/index/Circle.svelte @@ -0,0 +1,19 @@ + + +
    +
    + {@render children()} +
    +
    diff --git a/front/src/lib/components/index/search/datawells.svelte b/front/src/lib/components/index/search/datawells.svelte new file mode 100644 index 0000000..f04bdc6 --- /dev/null +++ b/front/src/lib/components/index/search/datawells.svelte @@ -0,0 +1,115 @@ + + +
    + + +
    + + + + + + + {#if showColumns} + + {/if} + + + + {#if paginatedDataleaks.length > 0} + {#each paginatedDataleaks as item} + + + + {#if showColumns} + + {/if} + + {/each} + {:else} + + + + {/if} + +
    NameNumber of rowsColumns
    + {item.Name} + {item.Length.toLocaleString("fr")} + {item.Columns.map((col) => col.replace(/_/g, " ")).join(", ")} +
    (Ξ‡.Ξ‡)
    No data wells found
    +
    + + {#if totalPages > 1} +
    + + + +
    + {/if} +
    diff --git a/front/src/lib/components/index/search/history.svelte b/front/src/lib/components/index/search/history.svelte new file mode 100644 index 0000000..3ea5b2c --- /dev/null +++ b/front/src/lib/components/index/search/history.svelte @@ -0,0 +1,130 @@ + + +
    + + +
    + + + + + + + + + + + + + {#if paginatedHistory.length > 0} + {#each paginatedHistory as item} + + + + + + + + {/each} + {:else} + + + + {/if} + +
    QueryResultsStatusDate
    + + {item.Results}
    + {item.Status} +
    {formatDate(item.Date)} { + navigate(`/search/:id`, { params: { id: item.Id } }); + }} + >
    (Ξ‡.Ξ‡)
    No history found
    +
    + + {#if totalPages > 1} +
    + + + +
    + {/if} +
    diff --git a/front/src/lib/components/index/search/howToSearch.svelte b/front/src/lib/components/index/search/howToSearch.svelte new file mode 100644 index 0000000..5c77e35 --- /dev/null +++ b/front/src/lib/components/index/search/howToSearch.svelte @@ -0,0 +1,46 @@ + + +
    +

    + Eleakxir's search engine is designed to be both fast and flexible, letting + you find what you need in multiple ways. +

    + +

    Search Modes

    + +

    + All: This is the default mode. + It searches for your query across a set of standard columns like email, username, + and phone number. This is the fastest and most efficient way to find a specific + user or account. +

    + +

    + Specific column: + This mode lets you choose a specific column to search within, such as email or + username. It's useful when you know exactly where the data you're looking for + is stored. +

    + +

    + Full Text: This mode combines + all available columns into a single, large text field and searches within it. + It's great for finding data that might be in an unexpected column, but it's way + slower. +

    + +

    Query Matching

    +

    + Standard Search: By default, + Eleakxir uses a "fuzzy" search. This means it will find results where your search + terms are part of a larger string. For example, searching for 1234 would find + john.doe@1234.com. +

    + +

    + Exact Match: When you enable + "Exact Match," the search will only return results where the data in a column + is an exact match for your search term. This is useful for finding specific, + unique values. +

    +
    diff --git a/front/src/lib/components/index/search/id/githubResult.svelte b/front/src/lib/components/index/search/id/githubResult.svelte new file mode 100644 index 0000000..ba24ad7 --- /dev/null +++ b/front/src/lib/components/index/search/id/githubResult.svelte @@ -0,0 +1,313 @@ + + +{#if githubResult.UsernameResult} +
    +
    +
    +
    + Avatar of {githubResult.UsernameResult.User.Username} +
    +
    +
    +
    +

    {githubResult.UsernameResult.User.Name}

    +

    + @{githubResult.UsernameResult.User.Username} +

    +
    +

    {githubResult.UsernameResult.User.Bio}

    +
    +
    +
    +
    + + + + {#if githubResult.UsernameResult.Socials && githubResult.UsernameResult.Socials.length > 0} +
    +

    Social Links

    + +
    + {/if} + {#if githubResult.UsernameResult.CloseFriends && githubResult.UsernameResult.CloseFriends.length > 0} +
    +
      + +
    + + + + {/if} + {#if githubResult.UsernameResult.Orgs && githubResult.UsernameResult.Orgs.length > 0} +
    +

    Organizations

    +
      + +
    + + + + {/if} + {#if githubResult.UsernameResult.Commits && githubResult.UsernameResult.Commits.length > 0} +
    +

    Commits

    +
      + {#each githubResult.UsernameResult.Commits as commit} + "} + subtitle={"Occurrences: " + commit.Occurrences} + > +
    + + {/each} + + + {/if} + {#if githubResult.UsernameResult.SshKeys && githubResult.UsernameResult.SshKeys.length > 0} +
    +

    SSH Keys

    +
      + {#each githubResult.UsernameResult.SshKeys as key} + +
      {key.Key}
      +
      + {/each} +
    +
    + {/if} + {#if githubResult.UsernameResult.SshSigningKeys && githubResult.UsernameResult.SshSigningKeys.length > 0} +
    +

    SSH Signing Keys

    +
      + {#each githubResult.UsernameResult.SshSigningKeys as key} + +
      {key.Key}
      +
      + {/each} +
    +
    + {/if} + {#if githubResult.UsernameResult.GpgKeys && githubResult.UsernameResult.GpgKeys.length > 0} +
    +

    GPG Keys

    +
      + {#each githubResult.UsernameResult.GpgKeys as key} + 0 ? key.Emails[0].Email : key.KeyID} + subtitle={"Created At: " + key.CreatedAt} + > +
    + + {/each} + + + {/if} + {#if githubResult.UsernameResult.DeepScan} + {#if githubResult.UsernameResult.DeepScan.Authors && githubResult.UsernameResult.DeepScan.Authors.length > 0} +
    +

    Deep scan authors

    +
      + +
    + + + + {/if} + {#if githubResult.UsernameResult.DeepScan.Emails && githubResult.UsernameResult.DeepScan.Emails.length > 0} +
    +

    Deep scan emails

    +
      + +
    + + + + {/if} + {#if githubResult.UsernameResult.DeepScan.Secrets && githubResult.UsernameResult.DeepScan.Secrets.length > 0} + {@const flattenedSecrets = githubResult.UsernameResult.DeepScan.Secrets.map(FlattenObject)} +
    +

    Deep scan secrets

    +
      + +
    + + + + {/if} + {/if} + +{:else if githubResult.EmailResult} +
    + {#if githubResult.EmailResult.Spoofing} +

    From spoofing

    +
    +
    +
    + Avatar of {githubResult.EmailResult.Spoofing.Username} +
    +
    +
    +
    +

    @{githubResult.EmailResult.Spoofing.Username}

    + {#if githubResult.EmailResult.Spoofing.Name} +

    + Name: + {githubResult.EmailResult.Spoofing.Name} +

    + {/if} + {#if githubResult.EmailResult.Spoofing.Email} +

    + Public email: + {githubResult.EmailResult.Spoofing.Email} +

    + {/if} + {#if githubResult.EmailResult.Target} +

    + Primary email: + {githubResult.EmailResult.Target} +

    + {/if} + + {githubResult.EmailResult.Spoofing.Url} + + +
    +
    +
    + {/if} + {#if githubResult.EmailResult.Commits} +
    +

    Commits

    +
      + {#each githubResult.EmailResult.Commits as commit} + +
    + + {/each} + + + {/if} + +{/if} diff --git a/front/src/lib/components/index/search/id/row.svelte b/front/src/lib/components/index/search/id/row.svelte new file mode 100644 index 0000000..726765b --- /dev/null +++ b/front/src/lib/components/index/search/id/row.svelte @@ -0,0 +1,100 @@ + + + +{#if isOpen} +
  • +
  • + +{/if} diff --git a/front/src/lib/components/index/search/id/rows.svelte b/front/src/lib/components/index/search/id/rows.svelte new file mode 100644 index 0000000..623693f --- /dev/null +++ b/front/src/lib/components/index/search/id/rows.svelte @@ -0,0 +1,72 @@ + + +
    +{#if result} +
      + {#each paginated as row (row)} + + {/each} +
    + + {#if totalPages > 1} +
    + + + +
    + {/if} +{:else} + No result +{/if} diff --git a/front/src/lib/components/index/search/id/stats.svelte b/front/src/lib/components/index/search/id/stats.svelte new file mode 100644 index 0000000..c96deae --- /dev/null +++ b/front/src/lib/components/index/search/id/stats.svelte @@ -0,0 +1,54 @@ + + +
    +
    +
    + +
    +
    Results
    +
    + {nresult.toLocaleString("fr")} + {#if result.Status === "pending"} + + {/if} +
    +
    +
    +
    + +
    +
    Date
    +
    + {formatDate(result.Date)} +
    +
    +
    +
    + +
    +
    Status
    +
    + {result.Status} + {#if result.Status === "pending"} + + {/if} +
    +
    +
    diff --git a/front/src/lib/components/index/search/searchbar.svelte b/front/src/lib/components/index/search/searchbar.svelte new file mode 100644 index 0000000..a07f05f --- /dev/null +++ b/front/src/lib/components/index/search/searchbar.svelte @@ -0,0 +1,104 @@ + + +
    +
    + {#each filters as filter} + + {/each} +
    + +
    { + e.preventDefault(); + NewSearch(); + }} + > + + + +
    diff --git a/front/src/lib/components/index/search/services.svelte b/front/src/lib/components/index/search/services.svelte new file mode 100644 index 0000000..6cf258f --- /dev/null +++ b/front/src/lib/components/index/search/services.svelte @@ -0,0 +1,83 @@ + + +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + +
    ServiceStatus
    Data wells lookup + {#if serverInfo.Dataleaks.length !== 0} +
    +
    +
    +
    + Active + {:else} +
    +
    +
    +
    + Inactive + {/if} +
    + Github recon + {#if serverInfo.Settings.GithubTokenLoaded === true} +
    Token
    + {/if} + {#if serverInfo.Settings.GithubDeepMode === true} +
    Deep Mode
    + {/if} +
    + {#if serverInfo.Settings.GithubRecon === true} +
    +
    +
    +
    + Active + {:else} +
    +
    +
    +
    + Inactive + {/if} +
    Google hunt + {#if serverInfo.Settings.GithubRecon === true} +
    +
    +
    +
    + Active + {:else} +
    +
    +
    +
    + Inactive + {/if} +
    +
    +
    diff --git a/front/src/lib/components/index/search/stats.svelte b/front/src/lib/components/index/search/stats.svelte new file mode 100644 index 0000000..ee7dc0c --- /dev/null +++ b/front/src/lib/components/index/search/stats.svelte @@ -0,0 +1,46 @@ + + +
    +
    +
    + +
    +
    Rows available
    +
    + {serverInfo?.TotalRows + ? serverInfo.TotalRows.toLocaleString("fr") + : "-- --- --- ---"} +
    +
    +
    +
    + +
    +
    Data wells available
    +
    + {serverInfo?.TotalDataleaks + ? serverInfo.TotalDataleaks.toLocaleString("fr") + : "---"} +
    +
    +
    +
    + +
    +
    Storage used
    +
    + {serverInfo?.TotalSize + ? mbToGb(serverInfo.TotalSize).toLocaleString("fr") + " Gb" + : "--- Gb"} +
    +
    +
    diff --git a/front/src/lib/components/logo.svelte b/front/src/lib/components/logo.svelte new file mode 100644 index 0000000..665ae9b --- /dev/null +++ b/front/src/lib/components/logo.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/front/src/lib/components/server-dialog.svelte b/front/src/lib/components/server-dialog.svelte new file mode 100644 index 0000000..f3b8ba1 --- /dev/null +++ b/front/src/lib/components/server-dialog.svelte @@ -0,0 +1,144 @@ + + +
    + +
    + {#if $serverUrl !== ""} +
    + {:else} +
    +
    + {/if} +
    +
    + +
    + + + + + diff --git a/front/src/lib/components/table.svelte b/front/src/lib/components/table.svelte new file mode 100644 index 0000000..b5b6378 --- /dev/null +++ b/front/src/lib/components/table.svelte @@ -0,0 +1,79 @@ + + +
    + + {#if Array.isArray(row) && row.length !== 0} + {@const head = Object.entries(row[0])} + + + + {#each head as [key, _]} + + {/each} + + + + {#each row as item} + + {#each Object.entries(item) as [key, value]} + + {/each} + + {/each} + + {:else} + + {#each Object.entries(row) as [key, value]} + {#if key !== "source" && value !== "" && value !== null} + + + + + + {/if} + {/each} + + {/if} +
    + {key} +
    + {#if key.toLowerCase() === "url" && value !== "" && value !== null} + + {value} + + + {:else} + {value} + {/if} +
    {key.replace(/_/g, " ")} + {#if key.toLowerCase() === "url"} + + {value} + + + {:else} + {value} + {/if} +
    +
    diff --git a/front/src/lib/navigation/sidebar-menu-item.svelte b/front/src/lib/navigation/sidebar-menu-item.svelte new file mode 100644 index 0000000..597f303 --- /dev/null +++ b/front/src/lib/navigation/sidebar-menu-item.svelte @@ -0,0 +1,30 @@ + + +
  • + {#if item.items} +
    + + {#if item.icon} + + {/if} + {item.title} + +
      + {#each item.items as subitem} + + {/each} +
    +
    + {:else} + + {#if item.icon} + + {/if} + {item.title} + + {/if} +
  • diff --git a/front/src/lib/navigation/sidebar.svelte b/front/src/lib/navigation/sidebar.svelte new file mode 100644 index 0000000..97c0639 --- /dev/null +++ b/front/src/lib/navigation/sidebar.svelte @@ -0,0 +1,57 @@ + + +
    + + +
    diff --git a/front/src/lib/navigation/topbar.svelte b/front/src/lib/navigation/topbar.svelte new file mode 100644 index 0000000..6448cb0 --- /dev/null +++ b/front/src/lib/navigation/topbar.svelte @@ -0,0 +1,47 @@ + + + + + diff --git a/front/src/lib/stores/server.ts b/front/src/lib/stores/server.ts new file mode 100644 index 0000000..750c0f8 --- /dev/null +++ b/front/src/lib/stores/server.ts @@ -0,0 +1,15 @@ +import { writable } from "svelte/store"; + +function persistent(key: string, initial: any) { + const stored = localStorage.getItem(key); + const data = writable(stored ? stored : initial); + + data.subscribe((value) => { + localStorage.setItem(key, value); + }); + + return data; +} + +export const serverUrl = persistent("serverUrl", ""); +export const serverPassword = persistent("serverPassword", ""); diff --git a/front/src/lib/types.ts b/front/src/lib/types.ts new file mode 100644 index 0000000..7344dfd --- /dev/null +++ b/front/src/lib/types.ts @@ -0,0 +1,78 @@ +type Query = { + Text: string; + Column: string; + ExactMatch: boolean; +}; + +type LeakResult = { + Duration: number; + Error: string; + Rows: Array>; +}; + +type GithubResult = { + Duration: number; + Error: string; + + EmailResult: any; + UsernameResult: any; +}; + +type Result = { + Id: string; + Status: "pending" | "completed"; + Date: string; + Query: Query; + LeakResult: LeakResult; + GithubResult: GithubResult; +}; + +type HistoryItem = { + Id: string; + Status: "pending" | "completed"; + Date: string; + Query: Query; + Results: number; +}; + +type History = HistoryItem[]; + +type ServerSettings = { + Folders: string[]; + CacheFolder: string; + Limit: number; + MinimumQueryLength: number; + + GithubRecon: boolean; + GithubTokenLoaded: boolean; + GithubDeepMode: boolean; +}; + +type Server = { + Settings: ServerSettings; + + Dataleaks: Dataleak[]; + + TotalRows: number; + TotalDataleaks: number; + TotalSize: number; +}; + +type Dataleak = { + Name: string; + Columns: string[]; + Length: number; + Size: number; +}; + +export type { + Query, + LeakResult, + History, + HistoryItem, + GithubResult, + Result, + ServerSettings, + Server, + Dataleak, +}; diff --git a/front/src/lib/utils.ts b/front/src/lib/utils.ts new file mode 100644 index 0000000..fb8be92 --- /dev/null +++ b/front/src/lib/utils.ts @@ -0,0 +1,80 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +// take "2025-09-13T21:14:46.13030464+02:00" +// return "13/09/2025 21:14" +export function formatDate(date: string) { + const d = new Date(date); + const day = String(d.getDate()).padStart(2, "0"); + const month = String(d.getMonth() + 1).padStart(2, "0"); + const year = d.getFullYear(); + const hours = String(d.getHours()).padStart(2, "0"); + const minutes = String(d.getMinutes()).padStart(2, "0"); + return `${day}/${month}/${year} ${hours}:${minutes}`; +} + +export function convertNanoSeconds(nanoseconds: number): string { + const ONE_MS_IN_NS = 1e6; + const ONE_S_IN_NS = 1e9; + const ONE_MIN_IN_NS = 6e10; + + if (nanoseconds < ONE_MS_IN_NS) { + return `${nanoseconds} ns`; // Garde la sortie en ns pour les très petites valeurs + } else if (nanoseconds < ONE_S_IN_NS) { + const ms = Math.round(nanoseconds / ONE_MS_IN_NS); + return `${ms} ms`; + } else if (nanoseconds < ONE_MIN_IN_NS) { + const s = Math.round(nanoseconds / ONE_S_IN_NS); + return `${s} s`; + } else { + const totalSeconds = Math.round(nanoseconds / ONE_S_IN_NS); + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + return `${minutes}m${seconds}s`; + } +} + +type FlatObject = { [key: string]: any }; + +export function FlattenObject(obj: object): FlatObject { + const flattened: FlatObject = {}; + + function recurse(currentObj: any, prefix: string = ""): void { + for (const key in currentObj) { + if (Object.prototype.hasOwnProperty.call(currentObj, key)) { + const value = currentObj[key]; + const newKey = prefix ? `${prefix}.${key}` : key; + + if ( + typeof value === "object" && + value !== null && + !Array.isArray(value) + ) { + // Si la valeur est un objet, on continue la récursion + recurse(value, newKey); + } else if (Array.isArray(value)) { + // Si la valeur est un tableau, on itère sur ses éléments + value.forEach((item, index) => { + // On continue la récursion pour les objets dans le tableau + if (typeof item === "object" && item !== null) { + recurse(item, `${newKey}.${index}`); + } else { + // On ajoute les valeurs primitives + flattened[`${newKey}.${index}`] = item; + } + }); + } else { + // Si la valeur est une primitive, on l'ajoute à l'objet aplati + flattened[newKey] = value; + } + } + } + } + + recurse(obj); + return flattened; +} diff --git a/front/src/main.ts b/front/src/main.ts new file mode 100644 index 0000000..dd9a3e8 --- /dev/null +++ b/front/src/main.ts @@ -0,0 +1,5 @@ +import { mount } from "svelte"; +import App from "./App.svelte"; +import "sv-router/generated"; + +mount(App, { target: document.querySelector("#app")! }); diff --git a/front/src/routes/dataleaks/index.svelte b/front/src/routes/dataleaks/index.svelte new file mode 100644 index 0000000..6d5d59a --- /dev/null +++ b/front/src/routes/dataleaks/index.svelte @@ -0,0 +1,57 @@ + + +
    +
    +

    πŸ—ƒοΈ Data wells

    +

    List of data wells (databases) available on the connected server.

    +
    + {#if serverInfo} +
    + +
    + + {:else} +

    Loading...

    + {/if} +
    diff --git a/front/src/routes/index.svelte b/front/src/routes/index.svelte new file mode 100644 index 0000000..0c0d44e --- /dev/null +++ b/front/src/routes/index.svelte @@ -0,0 +1,137 @@ + + +
    +
    +
    + + ✨ Check the Github repo + +
    + +

    Eleakxir

    +
    +

    + Eleakxir is a self-hosted search engine that lets you connect to your own + private and secure server, explore data wells (parquet files) from + multiple sources, and visualize results in a clean, modern web interface. +

    + +
    +
    + +
    +
    +
    +
    +

    βš™οΈ How Eleakxir works?

    +

    + You run an Elixir server that manages parquet files from various + leaked data sources and multiple OSINT tools. The web client connects + to your server via HTTPS and authenticated headers then you can search + across indexed leaks and OSINT tools, browse results interactively and + review history and stats +
    +
    + And it's open source! +

    + +
    + {#if $serverUrl === "https://" || $serverUrl === ""} + + {/if} + + + +
    +
    + +
    +
    + +
    +

    πŸš€ Features

    +
    + {#each [{ title: "πŸ” Private by design", content: "connect to your own Eleakxir server with a custom URL + password." }, { title: "πŸ›  Open source & extensible", content: "hack it, self-host it, extend it." }, { title: "πŸ“ Efficient File Format", content: "Uses the columnar Parquet format for high compression and rapid query performance." }, { title: "πŸ” OSINT Tools", content: "Includes Github-recon, GHunt, sherlock and more." }, { title: "πŸ“œ Standardized Schema", content: "Includes a detailed guide on how to normalize your data leaks for consistent and effective searching across different breaches." }] as value} +
    +
    +

    {value.title}

    +

    + {value.content} +

    +
    +
    + {/each} +
    +
    + +
    +

    🐒 Speed

    +

    + While Eleakxir is designed to be storage-efficient rather than + lightning-fast, searches will naturally take longer compared to an indexed + engine like Elasticsearch. Indexing systems can provide near-instant + results, but at the cost of massive disk usage β€” often requiring multiple + terabytes even for relatively modest datasets. In contrast, Eleakxir + trades some speed for compactness: for example, I’m able to store 25 + billion rows in just over 600 GB on entry-level hardware. A query might + take around an hour to complete, but the key point is that it’s actually + possible to run such searches at home β€” something that would be completely + out of reach if I had to maintain Elasticsearch’s much larger index + footprint. +

    +
    + +
    +

    🚨 Disclaimer

    +

    + Eleakxir is provided for educational and research purposes only. You are + solely responsible for how you use this software. Accessing, storing, or + distributing leaked data may be illegal in your jurisdiction. The authors + and contributors do not condone or promote illegal activity. Use + responsibly and only with data you are legally permitted to process. +

    +
    +
    + +
    diff --git a/front/src/routes/layout.svelte b/front/src/routes/layout.svelte new file mode 100644 index 0000000..366ce8a --- /dev/null +++ b/front/src/routes/layout.svelte @@ -0,0 +1,28 @@ + + + +
    + +
    + +
    + {@render children()} +
    + +
    diff --git a/front/src/routes/leak-utils/index.svelte b/front/src/routes/leak-utils/index.svelte new file mode 100644 index 0000000..c2f8f08 --- /dev/null +++ b/front/src/routes/leak-utils/index.svelte @@ -0,0 +1,18 @@ + + +
    + {@html marked(text)} +
    diff --git a/front/src/routes/parquet/index.svelte b/front/src/routes/parquet/index.svelte new file mode 100644 index 0000000..3acb2ba --- /dev/null +++ b/front/src/routes/parquet/index.svelte @@ -0,0 +1,18 @@ + + +
    + {@html marked(text)} +
    diff --git a/front/src/routes/search/[id]/index.svelte b/front/src/routes/search/[id]/index.svelte new file mode 100644 index 0000000..6ec01a1 --- /dev/null +++ b/front/src/routes/search/[id]/index.svelte @@ -0,0 +1,226 @@ + + +
    + {#if result} +
    + +

    πŸ” Search

    +
    + + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + + Data wells lookup +
    + {#if result.LeakResult.Error !== ""} + + {:else if result.LeakResult.Duration === 0} + + {:else if result.LeakResult.Rows.length > 0} + + {:else} + + {/if} +
    +
    + {#if result.LeakResult.Error !== ""} + + {:else if result.LeakResult.Duration === 0} +
      + {#each Array(5) as _} +
      +
      +
      +
      +
      +
      +
      +
      +
      + +
      +
      + {/each} +
    + {:else} +

    + {result.LeakResult.Rows.length} results in {convertNanoSeconds( + result.LeakResult.Duration, + )} +

    + + {/if} +
    +
    +
    + +
    +
    + + Github Recon +
    + {#if result.GithubResult.Error !== ""} + + {:else if result.GithubResult.Duration === 0} + + {:else if !result.GithubResult.EmailResult?.Commits && !result.GithubResult.EmailResult?.Spoofing && !result.GithubResult.UsernameResult?.User} + + {:else if result.GithubResult.UsernameResult || result.GithubResult.EmailResult} + + {/if} +
    +
    + {#if result.GithubResult.Error !== ""} + + {:else if result.GithubResult.Duration === 0} + + {:else if !result.GithubResult.EmailResult?.Commits && !result.GithubResult.EmailResult?.Spoofing && !result.GithubResult.UsernameResult?.User} + + {:else} +

    + Found a result in {convertNanoSeconds( + result.GithubResult.Duration, + )} +

    + + {/if} +
    +
    +
    + {/if} + +
    +
    diff --git a/front/src/routes/search/index.svelte b/front/src/routes/search/index.svelte new file mode 100644 index 0000000..f14dc05 --- /dev/null +++ b/front/src/routes/search/index.svelte @@ -0,0 +1,94 @@ + + +
    +
    +

    πŸ” Search

    + +
    + +
    + +
    +
    + +
    +
    +

    History

    + +
    +
    +

    Active services

    +
    + {#if !serverInfo} +

    Loading...

    + {:else} + + {/if} +
    +
    +
    +

    Last data wells added

    +
    + +
    +
    +
    +

    How to search

    + +
    +
    + +
    +
    diff --git a/front/src/vite-env.d.ts b/front/src/vite-env.d.ts new file mode 100644 index 0000000..4078e74 --- /dev/null +++ b/front/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/front/svelte.config.js b/front/svelte.config.js new file mode 100644 index 0000000..e5353cd --- /dev/null +++ b/front/svelte.config.js @@ -0,0 +1,6 @@ +/** @type {import('@sveltejs/vite-plugin-svelte').SvelteConfig} */ +export default { + compilerOptions: { + runes: true, + }, +}; diff --git a/front/tsconfig.json b/front/tsconfig.json new file mode 100644 index 0000000..2dc2374 --- /dev/null +++ b/front/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": ["./.router/tsconfig.json"], + "compilerOptions": { + "target": "ES2020", + "module": "preserve", + "moduleResolution": "bundler", + "esModuleInterop": true, + "strict": true, + "noEmit": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true, + "baseUrl": ".", + "paths": { + "$src": ["./src"], + "$src/*": ["./src/*"], + "$lib": ["./src/lib"], + "$lib/*": ["./src/lib/*"], + "sv-router/generated": [".router/router.ts"], + } + } +} diff --git a/front/vite.config.ts b/front/vite.config.ts new file mode 100644 index 0000000..b0a10ee --- /dev/null +++ b/front/vite.config.ts @@ -0,0 +1,15 @@ +import { svelte } from "@sveltejs/vite-plugin-svelte"; +import { router } from "sv-router/vite-plugin"; +import { defineConfig } from "vite"; +import tailwindcss from "@tailwindcss/vite"; +import path from "path"; + +export default defineConfig({ + plugins: [tailwindcss(), svelte({}), router()], + resolve: { + alias: { + $lib: path.resolve("./src/lib"), + $src: path.resolve("./src"), + }, + }, +}); diff --git a/leak-utils/DATALEAKS-NORMALIZATION.md b/leak-utils/DATALEAKS-NORMALIZATION.md new file mode 100644 index 0000000..8fb4d9b --- /dev/null +++ b/leak-utils/DATALEAKS-NORMALIZATION.md @@ -0,0 +1,138 @@ +# Rules for handling Data Leaks + +This normalization framework is designed to standardize data leaks for +[Eleakxir](https://github.com/anotherhadi/eleakxir), the open-source search +engine, using +[leak-utils](https://github.com/anotherhadi/eleakxir-temp/blob/main/leak-utils/README.md), +a dedicated CLI tool that converts and cleans files for efficient indexing and +searching. + +## The Relevance of Parquet for Data Leaks + +Parquet is an efficient, open-source columnar storage file format designed to +handle complex data in bulk. When dealing with data leaks, its choice is highly +relevant for several reasons: + +- **Compression**: Parquet files offer superior compression compared to + row-based formats like CSV. By storing data column by column, it applies more + effective compression algorithms, which significantly reduces disk space. For + data leaks, where file sizes can range from gigabytes to terabytes, this is + crucial for minimizing storage costs. +- **Query Performance**: As a columnar format, Parquet allows you to read only + the specific columns you need for a query. In a data leak, you might only be + interested in emails and passwords, not full addresses or phone numbers. This + selective reading drastically speeds up search operations, as the system + doesn't have to scan through entire rows of irrelevant data. +- **Efficiency**: The format is optimized for analytics. It stores data with + metadata and statistics (min/max values) for each column, allowing for query + **pruning**. This means a query can skip entire blocks of data that don't + match the filtering criteria, boosting performance even further. + +## Disclaimer + +The information in this document is provided **for research and educational +purposes only**. I am **not responsible** for how this data, methods, or +guidelines are used. Any misuse, unlawful activity, or harm resulting from +applying this content is the sole responsibility of the individual or +organization using it. + +## File Naming Convention + +- **Lowercase only**, ASCII (no accents). +- **Separators**: + - `_` inside blocks (`date_2023_10`) + - `-` between blocks (`instagram.com-date_2023_10`) +- **Prefix**: always start with the **source name/url** (e.g., `instagram.com`, + `alien_txt`). +- **Blocks**: each additional part must be prefixed by its block name: + + - `date_YYYY[_MM[_DD]]` β†’ use ISO format (year, or year-month, or full date). + - `source_*` β†’ origin of the leak (e.g., `scrape`, `dump`, `combo`). + - `version_v*` β†’ versioning if regenerated or transformed. + - `notes_*` β†’ optional clarifications. +- **Extension**: always `.parquet`. + +**Recommended pattern:** + +```txt +{source}-date_{YYYY[_MM[_DD]]}-source_{origin}-version_{vN}-notes_{info}.parquet +``` + +**Examples:** + +```txt +instagram.com-date_2023_10.parquet +alien_txt-date_2022-source_dump.parquet +combo_french-notes_crypto.parquet +``` + +## Column Naming Convention + +- **snake\_case only** (lowercase, `_` separator). + +- **No dots (`.`)** in column names (`husband.phone` β†’ `husband_phone`). + +- **Allowed characters**: `[a-z0-9_]+` (no spaces, hyphens, or accents). + +- **Multiple variants of the same field**: + + - Relations β†’ prefix clearly: `husband_phone`, `mother_last_name`. + - Multiples of the same type β†’ numbered prefix: `1_phone`, `2_phone`, + `3_phone`. + - Always end with the column "type" (e.g., `_phone`, `_last_name`). + +- **Rename if mislabeled**: If a `username` column actually contains only emails + rename it to `email`. + +- **Remove irrelevant columns**: Drop meaningless identifiers like `id` or + fields with no analytical value. + +- **Standard columns**: to enable schema alignment across leaks: + + | Column | + | ------------- | + | email | + | username | + | password | + | password_hash | + | phone | + | date | + | birth_date | + | age | + | first_name | + | last_name | + | full_name | + | address | + | city | + | country | + | state | + | postal_code | + | ip | + | url | + | city | + +## Standard Column Formatting + +- **Email**: lowercase, trimmed, keep only `[^a-z0-9._@-]`. + +- **Phone**: keep only `[^0-9]` + +- **Names**: + + - Keep `first_name` / `last_name` if present. + - Generate `full_name = CONCAT(first_name, ' ', last_name)`. + - If only `name` exists, rename it to `full_name`. + +- **Passwords**: + + - Hashes β†’ `password_hash`. + - Plaintext β†’ `password`. + - Never mix hashes and plaintext in the same column. + +- **NULLs**: always use SQL `NULL` (never `""` or `"NULL"`). + +## Deduplication + +Deduplication is often **impractical at scale** (billions of rows). Do **not** +attempt to deduplicate at ingestion time. Instead, handle deduplication **after +running a search** to optimize performance and storage. diff --git a/leak-utils/README.md b/leak-utils/README.md new file mode 100644 index 0000000..b9c4058 --- /dev/null +++ b/leak-utils/README.md @@ -0,0 +1,107 @@ +# πŸ›  leak-utils: The Eleakxir Data Utility Toolkit + +`leak-utils` is a powerful command-line tool built to help you manage, process, +and optimize data leaks for use with the **Eleakxir** search engine. It provides +a suite of utilities for data cleaning, format conversion, and file +manipulation, all designed to ensure your data wells are efficient and +standardized. + +`leak-utils` is written in **Go** and leverages **DuckDB** for its +high-performance in-memory processing, ensuring fast and reliable operations on +large datasets. + +## πŸš€ Features + +- **Parquet File Management**: Clean and inspect existing `.parquet` files. +- **Format Conversion**: Seamlessly convert `.csv`, `.txt`, `.json` files into + the optimized `.parquet` format. +- **Schema Uniformity**: Tools designed to help you standardize and normalize + your data to align with the + [Eleakxir data leak normalization rules](./DATALEAKS-NORMALIZATION.md). This + ensures a consistent schema across all your files, which is crucial for + efficient searching and consistent results. +- **High Performance**: Built with Go and DuckDB for fast and efficient data + processing. + +## βš™οΈ How to Use + +The tool operates via a single executable with different commands, each +corresponding to a specific action. You can find the executable in the +`leak-utils` directory of the Eleakxir project. + +### Install + +#### With go + +```bash +go install "github.com/anotherhadi/eleakxir/leak-utils@latest" +``` + +#### With Nix/NixOS + +
    +Click to expand + +**From anywhere (using the repo URL):** + +```bash +nix run "github:anotherhadi/eleakxir#leak-utils" -- action [--flags value] +``` + +**Permanent Installation:** + +```bash +# add the flake to your flake.nix +{ + inputs = { + eleakxir.url = "github:anotherhadi/eleakxir"; + }; +} + +# then add it to your packages +environment.systemPackages = with pkgs; [ # or home.packages + eleakxir.packages.${pkgs.system}.leak-utils +]; +``` + +
    + +### Available Actions + +#### `cleanParquet` + +Optimizes and cleans an existing Parquet file. This can be used to change +columns, clean rows, ... + +See: + +```bash +leak-utils cleanParquet --help +``` + +#### `infoParquet` + +Displays metadata and schema information for a given Parquet file. Useful for +inspecting file structure and column types. + +#### `csvToParquet` + +Converts a `.csv` file into a highly compressed and efficient `.parquet` file. +This is the recommended way to prepare your data for Eleakxir. + +#### `mergeFiles` + +Merges multiple files (of the same type) into a single, larger file. This is +useful for combining smaller data leaks. + +#### `removeUrlSchemeFromUlp` + +This utility prevents the colon (`:`) in URL schemes like `https://` from being +mistakenly parsed as a column separator when processing ULP data in flat files +like CSV or TXT. + +## 🀝 Contributing + +[Contributions](../CONTRIBUTING.md) to `leak-utils` are welcome! Feel free to +open issues or submit pull requests for new features, bug fixes, or performance +improvements. diff --git a/leak-utils/go.mod b/leak-utils/go.mod new file mode 100644 index 0000000..4e7da71 --- /dev/null +++ b/leak-utils/go.mod @@ -0,0 +1,42 @@ +module github.com/anotherhadi/eleakxir/leak-utils + +go 1.25.0 + +require ( + github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1 + github.com/charmbracelet/log v0.4.2 + github.com/marcboeker/go-duckdb v1.8.5 + github.com/spf13/pflag v1.0.10 +) + +require ( + github.com/apache/arrow-go/v18 v18.1.0 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/colorprofile v0.3.0 // indirect + github.com/charmbracelet/lipgloss v1.1.0 // indirect + github.com/charmbracelet/x/ansi v0.8.0 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13 // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/google/flatbuffers v25.1.24+incompatible // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/pierrec/lz4/v4 v4.1.22 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/zeebo/xxh3 v1.0.2 // indirect + golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect + golang.org/x/mod v0.22.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/tools v0.29.0 // indirect + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect +) diff --git a/leak-utils/go.sum b/leak-utils/go.sum new file mode 100644 index 0000000..2c61a9f --- /dev/null +++ b/leak-utils/go.sum @@ -0,0 +1,94 @@ +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/apache/arrow-go/v18 v18.1.0 h1:agLwJUiVuwXZdwPYVrlITfx7bndULJ/dggbnLFgDp/Y= +github.com/apache/arrow-go/v18 v18.1.0/go.mod h1:tigU/sIgKNXaesf5d7Y95jBBKS5KsxTqYBKXFsvKzo0= +github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE= +github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/colorprofile v0.3.0 h1:KtLh9uuu1RCt+Hml4s6Hz+kB1PfV3wi++1h5ia65yKQ= +github.com/charmbracelet/colorprofile v0.3.0/go.mod h1:oHJ340RS2nmG1zRGPmhJKJ/jf4FPNNk0P39/wBPA1G0= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1 h1:SOylT6+BQzPHEjn15TIzawBPVD0QmhKXbcb3jY0ZIKU= +github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1/go.mod h1:tRlx/Hu0lo/j9viunCN2H+Ze6JrmdjQlXUQvvArgaOc= +github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig= +github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw= +github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= +github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= +github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/flatbuffers v25.1.24+incompatible h1:4wPqL3K7GzBd1CwyhSd3usxLKOaJN/AC6puCca6Jm7o= +github.com/google/flatbuffers v25.1.24+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= +github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/marcboeker/go-duckdb v1.8.5 h1:tkYp+TANippy0DaIOP5OEfBEwbUINqiFqgwMQ44jME0= +github.com/marcboeker/go-duckdb v1.8.5/go.mod h1:6mK7+WQE4P4u5AFLvVBmhFxY5fvhymFptghgJX6B+/8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc= +golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= +golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0= +gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/leak-utils/leak-utils/main.go b/leak-utils/leak-utils/main.go new file mode 100644 index 0000000..8461207 --- /dev/null +++ b/leak-utils/leak-utils/main.go @@ -0,0 +1,145 @@ +package main + +import ( + "database/sql" + "fmt" + "os" + "slices" + "strings" + + "github.com/anotherhadi/eleakxir/leak-utils/misc" + "github.com/anotherhadi/eleakxir/leak-utils/parquet" + "github.com/anotherhadi/eleakxir/leak-utils/settings" + "github.com/charmbracelet/log" + _ "github.com/marcboeker/go-duckdb" + flag "github.com/spf13/pflag" +) + +func main() { + db, err := sql.Open("duckdb", "") + if err != nil { + log.Fatal("Failed to open DuckDB", "error", err) + } + defer db.Close() + lu := settings.LeakUtils{ + Db: db, + } + actions := []string{ + "cleanParquet", + "infoParquet", + // Csv + "csvToParquet", + // Misc + "mergeFiles", + "removeUrlSchemeFromUlp", + } + + if len(os.Args) < 2 { + fmt.Println(settings.Muted.Render("Usage: "), settings.Accent.Render(os.Args[0], "")) + fmt.Println(settings.Muted.Render("Actions: "), settings.Base.Render(strings.Join(actions, ", "))) + return + } + action := os.Args[1] + if !slices.Contains(actions, action) { + log.Fatal("Unknown action", "action", action) + } + + switch action { + case "cleanParquet": + var inputFile *string = flag.StringP("input", "i", "", "Input Parquet file") + var outputFile *string = flag.StringP("output", "o", "", "Output Parquet file") + var compression *string = flag.StringP("compression", "c", "ZSTD", "Compression codec (UNCOMPRESSED, SNAPPY, GZIP, BROTLI, LZ4, ZSTD)") + var skipLineFormating *bool = flag.BoolP("skip-line-formating", "s", false, "Skip line formating") + var deleteFirstRow *bool = flag.Bool("delete-first-row", false, "Delete first row") + var debug *bool = flag.Bool("debug", false, "Debug mode") + var noColors *bool = flag.Bool("no-colors", false, "Remove all colors") + var printQuery *bool = flag.BoolP("print-query", "p", false, "Print the query instead of executing it") + flag.Parse() + if *inputFile == "" || *outputFile == "" { + log.Fatal("Input and output files are required") + } + if *noColors { + settings.DisableColors() + } + lu.Compression = *compression + lu.Debug = *debug + err := parquet.CleanParquet(lu, *inputFile, *outputFile, *skipLineFormating, *deleteFirstRow, *printQuery) + if err != nil { + log.Fatal("Failed to clean Parquet file", "error", err) + } + return + case "infoParquet": + var inputFile *string = flag.StringP("input", "i", "", "Input Parquet file") + var debug *bool = flag.Bool("debug", false, "Debug mode") + var noColors *bool = flag.Bool("no-colors", false, "Remove all colors") + flag.Parse() + if *inputFile == "" { + log.Fatal("Input files are required") + } + if *noColors { + settings.DisableColors() + } + lu.Debug = *debug + err := parquet.InfoParquet(lu, *inputFile) + if err != nil { + log.Fatal("Failed to read Parquet file", "error", err) + } + return + case "csvToParquet": + var inputFile *string = flag.StringP("input", "i", "", "Input Parquet file") + var outputFile *string = flag.StringP("output", "o", "", "Output Parquet file") + var strict *bool = flag.Bool("strict", true, "Strict mode for Duckdb") + var compression *string = flag.StringP("compression", "c", "ZSTD", "Compression codec (UNCOMPRESSED, SNAPPY, GZIP, BROTLI, LZ4, ZSTD)") + var noColors *bool = flag.Bool("no-colors", false, "Remove all colors") + var debug *bool = flag.Bool("debug", false, "Debug mode") + flag.Parse() + if *inputFile == "" || *outputFile == "" { + log.Fatal("Input and output files are required") + } + if *noColors { + settings.DisableColors() + } + lu.Compression = *compression + lu.Debug = *debug + err := misc.CsvToParquet(lu, *inputFile, *outputFile, *strict) + if err != nil { + log.Fatal("Failed to transform Csv file", "error", err) + } + return + case "mergeFiles": + var inputFiles *[]string = flag.StringArrayP("inputs", "i", []string{}, "Input Parquet files") + var outputFile *string = flag.StringP("output", "o", "", "Output Parquet file") + var noColors *bool = flag.Bool("no-colors", false, "Remove all colors") + var debug *bool = flag.Bool("debug", false, "Debug mode") + flag.Parse() + if len(*inputFiles) == 0 || *outputFile == "" { + log.Fatal("Inputs and output files are required") + } + if *noColors { + settings.DisableColors() + } + lu.Debug = *debug + err := misc.MergeFiles(lu, *outputFile, *inputFiles...) + if err != nil { + log.Fatal("Failed to merge files", "error", err) + } + return + case "removeUrlSchemeFromUlp": + var inputFile *string = flag.StringP("input", "i", "", "Input Parquet file") + var noColors *bool = flag.Bool("no-colors", false, "Remove all colors") + var debug *bool = flag.Bool("debug", false, "Debug mode") + flag.Parse() + if *inputFile == "" { + log.Fatal("Input files are required") + } + if *noColors { + settings.DisableColors() + } + lu.Debug = *debug + err := misc.RemoveUrlSchemeFromUlp(lu, *inputFile) + if err != nil { + log.Fatal("Failed to remove ULP Url schemes", "error", err) + } + return + } +} diff --git a/leak-utils/misc/csv.go b/leak-utils/misc/csv.go new file mode 100644 index 0000000..f5750ad --- /dev/null +++ b/leak-utils/misc/csv.go @@ -0,0 +1,173 @@ +package misc + +import ( + "bufio" + "encoding/csv" + "fmt" + "io" + "os" + "slices" + "strings" + + "github.com/anotherhadi/eleakxir/leak-utils/settings" + "github.com/charmbracelet/log" +) + +func CsvToParquet(lu settings.LeakUtils, inputFile string, outputFile string, strict bool) error { + hasHeader, err := csvHasHeader(inputFile) + if err != nil { + return err + } + header := "true" + if !hasHeader { + header = "false" + } + strictMode := "true" + if !strict { + strictMode = "false" + } + + delimiter := getDelimiter(inputFile) + + query := fmt.Sprintf(`CREATE TABLE my_table AS FROM read_csv_auto('%s', HEADER=%s, delim='%s', ignore_errors=true, all_varchar=true, null_padding=true, strict_mode=%s); + COPY my_table TO '%s' (FORMAT 'parquet', COMPRESSION '%s', ROW_GROUP_SIZE 200_000);`, + inputFile, header, delimiter, strictMode, outputFile, lu.Compression) + + if lu.Debug { + log.Info("Detected delimiter", "delimiter", delimiter) + log.Info("CSV header detection", "hasHeader", hasHeader) + log.Info("Executing query", "query", query) + } + + _, err = lu.Db.Exec(query) + + if lu.Debug { + log.Info("Finished executing query") + } + + return err +} + +func getDelimiter(inputFile string) string { + lines, err := getNLine(inputFile, 10, 0) + if err != nil { + log.Warn("Failed to read CSV file to determine delimiter, defaulting to comma", "error", err) + return "," + } + + delimiterCounts := map[string]int{ + ",": 0, + ";": 0, + "\t": 0, + "|": 0, + ":": 0, + } + + for _, line := range lines { + for d := range delimiterCounts { + delimiterCounts[d] += strings.Count(line, d) + } + } + + maxCount := 0 + delimiter := "," + + for d, count := range delimiterCounts { + if count > maxCount { + maxCount = count + delimiter = d + } + } + + return delimiter +} + +func csvHasHeader(inputFile string) (hasHeader bool, err error) { + firstRow, err := getFirstRowCsv(inputFile) + if err != nil { + return false, err + } + for i, col := range firstRow { + col = strings.ReplaceAll(col, "\"", "") + col = strings.ReplaceAll(col, " ", "") + col = strings.ReplaceAll(col, "-", "") + col = strings.ReplaceAll(col, "_", "") + col = strings.ReplaceAll(col, ".", "") + firstRow[i] = strings.ToLower(strings.TrimSpace(col)) + } + knownHeaders := []string{"email", "password", "username", "phone", "lastname", "firstname"} + for _, knownHeader := range knownHeaders { + if slices.Contains(firstRow, knownHeader) { + return true, nil + } + } + return false, nil +} + +func getNLine(inputFile string, n, offset int) (lines []string, err error) { + if n <= 0 { + return nil, nil + } + + if offset < 0 { + offset = 0 + } + + file, err := os.Open(inputFile) + if err != nil { + return nil, err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + currentLine := 0 + + for scanner.Scan() { + currentLine++ + if currentLine <= offset { + continue + } + + lines = append(lines, scanner.Text()) + if len(lines) >= n { + break + } + } + + if err := scanner.Err(); err != nil && err != io.EOF { + return nil, err + } + + return lines, nil +} + +func getFirstRowCsv(inputFile string) (row []string, err error) { + rows, err := getFirstNRowsCsv(inputFile, 1) + if len(rows) == 0 { + return nil, fmt.Errorf("no rows found in CSV") + } + return rows[0], err +} + +func getFirstNRowsCsv(inputFile string, n int) (rows [][]string, err error) { + f, err := os.Open(inputFile) + if err != nil { + return nil, fmt.Errorf("failed to open file: %w", err) + } + defer f.Close() + + reader := csv.NewReader(f) + + for i := 0; i < n; i++ { + row, err := reader.Read() + if err != nil { + if err.Error() == "EOF" { + break + } + return nil, fmt.Errorf("failed to read CSV: %w", err) + } + rows = append(rows, row) + } + + return rows, nil +} diff --git a/leak-utils/misc/misc.go b/leak-utils/misc/misc.go new file mode 100644 index 0000000..983690f --- /dev/null +++ b/leak-utils/misc/misc.go @@ -0,0 +1,31 @@ +package misc + +import ( + "io" + "os" + + "github.com/anotherhadi/eleakxir/leak-utils/settings" +) + +func MergeFiles(lu settings.LeakUtils, outputFile string, inputFiles ...string) error { + out, err := os.Create(outputFile) + if err != nil { + return err + } + defer out.Close() + + for _, inputFile := range inputFiles { + file, err := os.Open(inputFile) + if err != nil { + return err + } + defer file.Close() + + _, err = io.Copy(out, file) + if err != nil { + return err + } + } + + return nil +} diff --git a/leak-utils/misc/ulp.go b/leak-utils/misc/ulp.go new file mode 100644 index 0000000..40d7b04 --- /dev/null +++ b/leak-utils/misc/ulp.go @@ -0,0 +1,67 @@ +package misc + +import ( + "bufio" + "io" + "os" + "strings" + + "github.com/anotherhadi/eleakxir/leak-utils/settings" +) + +func RemoveUrlSchemeFromUlp(lu settings.LeakUtils, inputFile string) error { + file, err := os.Open(inputFile) + if err != nil { + return err + } + defer file.Close() + + outputFile := inputFile + ".clean" + out, err := os.Create(outputFile) + if err != nil { + return err + } + defer out.Close() + + reader := bufio.NewReader(file) + writer := bufio.NewWriter(out) + + for { + line, err := reader.ReadString('\n') + if err != nil && err != io.EOF { + return err + } + + firstColumn := strings.Index(line, ":") + firstScheme := strings.Index(line, "://") + if firstScheme != -1 && firstColumn == firstScheme { + line = line[firstScheme+3:] + } + + _, werr := writer.WriteString(line) + if werr != nil { + return err + } + + if err == io.EOF { + break + } + } + + err = writer.Flush() + if err != nil { + return err + } + + err = os.Remove(inputFile) + if err != nil { + return err + } + + err = os.Rename(outputFile, inputFile) + if err != nil { + return err + } + + return nil +} diff --git a/leak-utils/parquet/format.go b/leak-utils/parquet/format.go new file mode 100644 index 0000000..1579a03 --- /dev/null +++ b/leak-utils/parquet/format.go @@ -0,0 +1,107 @@ +package parquet + +import ( + "fmt" + "strings" + + "github.com/anotherhadi/eleakxir/leak-utils/settings" +) + +// If there is no full_name but there is last_name and first_name, create full_name +// If there is no full_name, no last_name or no first_name, but there is name, rename name to full_name +func addFullname(operations []ColumnOperation) []ColumnOperation { + hasFullName := false + hasFirstName := false + hasLastName := false + hasName := false + for _, op := range operations { + if op.Action != "drop" { + if op.NewName == "full_name" { + hasFullName = true + } else if op.NewName == "first_name" { + hasFirstName = true + } else if op.NewName == "last_name" { + hasLastName = true + } else if op.NewName == "name" { + hasName = true + } + } + } + if hasFullName { + return operations + } + if hasFirstName && hasLastName { + operations = append(operations, ColumnOperation{ + OriginalName: "first_name || ' ' || last_name", + NewName: "full_name", + Action: "rename", + }) + fmt.Println(settings.Muted.Render("\nAdding new column 'full_name' as concatenation of 'first_name' and 'last_name'.")) + return operations + } + if hasName { + for i, op := range operations { + if op.NewName == "name" && op.Action != "drop" { + operations[i].NewName = "full_name" + fmt.Println(settings.Muted.Render("\nRenaming column 'name' to 'full_name'.")) + return operations + } + } + } + if hasFirstName { + operations = append(operations, ColumnOperation{ + OriginalName: "first_name", + NewName: "full_name", + Action: "rename", + }) + fmt.Println(settings.Muted.Render("\nAdding new column 'full_name' from 'first_name'.")) + return operations + } + if hasLastName { + operations = append(operations, ColumnOperation{ + OriginalName: "last_name", + NewName: "full_name", + Action: "rename", + }) + fmt.Println(settings.Muted.Render("\nAdding new column 'full_name' from 'last_name'.")) + return operations + } + + return operations +} + +// formatColumnName formats a column name to be SQL-compliant. +func formatColumnName(columnName string) string { + columnName = strings.TrimSpace(columnName) + columnName = strings.ToLower(columnName) + columnName = strings.Join(strings.Fields(columnName), "_") + columnName = strings.ReplaceAll(columnName, "\"", "") + columnName = strings.ReplaceAll(columnName, "'", "") + columnName = strings.ReplaceAll(columnName, " ", "_") + columnName = strings.ReplaceAll(columnName, "-", "_") + // Only keep a-z, 0-9 and _ + var formatted strings.Builder + for _, r := range columnName { + if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '_' { + formatted.WriteRune(r) + } + } + columnName = formatted.String() + columnName = strings.TrimPrefix(columnName, "_") + columnName = strings.TrimSuffix(columnName, "_") + return columnName +} + +// formatColumns applies specific formatting rules to column operations. +func formatColumns(operations []ColumnOperation) []ColumnOperation { + formatedOperations := []ColumnOperation{} + for _, op := range operations { + if op.NewName == "phone" || strings.HasSuffix(op.NewName, "_phone") { + op.OriginalName = "REGEXP_REPLACE(" + op.OriginalName + ", '[^0-9]', '')" + } else if op.NewName == "email" || strings.HasSuffix(op.NewName, "_email") { + op.OriginalName = "REGEXP_REPLACE(LOWER(TRIM(" + op.OriginalName + ")), '[^a-z0-9._@-]', '')" + } + formatedOperations = append(formatedOperations, op) + } + return formatedOperations +} diff --git a/leak-utils/parquet/parquet.go b/leak-utils/parquet/parquet.go new file mode 100644 index 0000000..538a982 --- /dev/null +++ b/leak-utils/parquet/parquet.go @@ -0,0 +1,276 @@ +package parquet + +import ( + "bufio" + "database/sql" + "fmt" + "os" + "strings" + + "github.com/anotherhadi/eleakxir/leak-utils/settings" + "github.com/charmbracelet/log" +) + +type Parquet struct { + Filepath string + Filename string + Columns []string + Sample [][]string + NRows int64 + Compression string // Compression of the output file (e.g., "SNAPPY", "ZSTD", "NONE" or "") +} + +type ColumnOperation struct { + OriginalName string + NewName string + Action string // "keep", "rename", "drop" +} + +func (parquet Parquet) PrintParquet() { + fmt.Println(settings.Header.Render(parquet.Filename) + "\n") + fmt.Println(settings.Accent.Render("File path:"), settings.Base.Render(parquet.Filepath)) + fmt.Println(settings.Accent.Render("Number of columns:"), settings.Base.Render(fmt.Sprintf("%d", len(parquet.Columns)))) + fmt.Println(settings.Accent.Render("Number of rows:"), settings.Base.Render(formatWithSpaces(parquet.NRows))) + fmt.Println() + fmt.Println(settings.Accent.Render(strings.Join(parquet.Columns, " | "))) + for _, row := range parquet.Sample { + fmt.Println(settings.Base.Render(strings.Join(row, " | "))) + } +} + +func InfoParquet(lu settings.LeakUtils, inputFile string) error { + parquet, err := GetParquet(lu.Db, inputFile) + if err != nil { + return err + } + + parquet.PrintParquet() + return nil +} + +func CleanParquet(lu settings.LeakUtils, inputFile, outputFile string, skipLineFormating, deleteFirstRow, printQuery bool) error { + input, err := GetParquet(lu.Db, inputFile) + if err != nil { + return err + } + input.PrintParquet() + columnOps := configureColumns(*input, skipLineFormating) + output := Parquet{ + Filepath: outputFile, + Compression: lu.Compression, + } + err = transformParquet(lu, *input, output, columnOps, deleteFirstRow, printQuery) + return err +} + +func configureColumns(input Parquet, skipLineFormating bool) []ColumnOperation { + reader := bufio.NewReader(os.Stdin) + var operations []ColumnOperation + + fmt.Println() + fmt.Println(settings.Base.Render("For each column, choose an action:")) + fmt.Println(settings.Base.Render(" [k] Keep")) + fmt.Println(settings.Base.Render(" [r] Rename")) + fmt.Println(settings.Base.Render(" [d] Drop/Delete")) + fmt.Println(settings.Base.Render(" [s] Suggested")) + fmt.Println(settings.Base.Render(" [b] Go back")) + fmt.Println() + + for i := 0; i < len(input.Columns); i++ { + col := input.Columns[i] + suggestion := getSuggestion(col) + + for { + fmt.Println(settings.Muted.Render("\nColumn:"), settings.Accent.Render(col)) + if suggestion != "" { + fmt.Println(settings.Alert.Render("Suggested action: Rename to '" + suggestion + "'")) + } + fmt.Print(settings.Base.Render("[k/r/d/s/b]: ")) + + input, err := reader.ReadString('\n') + if err != nil { + log.Printf("Error reading input: %v", err) + continue + } + input = strings.TrimSpace(strings.ToLower(input)) + + op := ColumnOperation{ + OriginalName: col, + NewName: col, + Action: "keep", + } + + switch input { + case "b", "back": + if i > 0 { + i -= 2 + if len(operations) > 0 { + operations = operations[:len(operations)-1] + } + fmt.Println(settings.Muted.Render("Going back to the previous column...")) + } else { + fmt.Println(settings.Muted.Render("Already at the first column, cannot go back further.")) + continue + } + goto nextColumn + + case "r", "rename": + fmt.Print(settings.Base.Render("Enter new name: ")) + newName, err := reader.ReadString('\n') + if err != nil { + log.Printf("Error reading new name: %v", err) + continue + } + newName = strings.TrimSpace(newName) + if newName != "" { + op.OriginalName = "\"" + op.OriginalName + "\"" + op.NewName = formatColumnName(newName) + op.Action = "rename" + operations = append(operations, op) + goto nextColumn + } else { + fmt.Println(settings.Muted.Render("Invalid name, please try again.")) + continue + } + + case "s", "suggested": + if suggestion != "" { + op.OriginalName = "\"" + op.OriginalName + "\"" + op.NewName = formatColumnName(suggestion) + op.Action = "rename" + } else { + fmt.Println(settings.Muted.Render("No valid suggestion available")) + continue + } + operations = append(operations, op) + goto nextColumn + + case "d", "drop", "delete": + op.Action = "drop" + operations = append(operations, op) + goto nextColumn + + case "k", "keep", "": + op.OriginalName = "\"" + op.OriginalName + "\"" + op.NewName = formatColumnName(op.NewName) + op.Action = "rename" + operations = append(operations, op) + goto nextColumn + + default: + fmt.Println(settings.Muted.Render("Invalid choice, please enter [k/r/d/s/b].")) + continue + } + } + nextColumn: + lastOp := operations[len(operations)-1] + switch lastOp.Action { + case "rename": + if formatColumnName(lastOp.OriginalName) == lastOp.NewName { + fmt.Printf(settings.Muted.Render("Keeping column '%s' as is.\n"), lastOp.OriginalName) + } else { + fmt.Printf(settings.Muted.Render("Renaming column '%s' to '%s'.\n"), lastOp.OriginalName, lastOp.NewName) + } + case "drop": + fmt.Printf(settings.Muted.Render("Dropping column '%s'.\n"), lastOp.OriginalName) + } + } + if !skipLineFormating { + operations = formatColumns(operations) + } + operations = addFullname(operations) + + return operations +} + +func transformParquet(lu settings.LeakUtils, input, output Parquet, operations []ColumnOperation, deleteFirstRow, printQuery bool) error { + var selectClauses []string + hasColumns := false + + for _, op := range operations { + if op.Action != "drop" { + hasColumns = true + if op.Action == "rename" { + selectClauses = append(selectClauses, fmt.Sprintf("%s AS \"%s\"", op.OriginalName, op.NewName)) + } else { + selectClauses = append(selectClauses, op.OriginalName) + } + } + } + + if !hasColumns { + return fmt.Errorf("no columns selected for output") + } + + selectClause := strings.Join(selectClauses, ", ") + compression := "" + if output.Compression != "" { + compression = ", COMPRESSION '" + output.Compression + "'" + } + + columnsLength := []string{} + for _, col := range input.Columns { + columnsLength = append(columnsLength, "COALESCE(LENGTH(\""+col+"\"),0)") + } + allowedRowSize := 30 * len(input.Columns) + offset := "" + if deleteFirstRow { + offset = "OFFSET 1" + } + + query := fmt.Sprintf(` + COPY ( + SELECT %s + FROM read_parquet('%s') + WHERE (%s) < %d + %s + ) TO '%s' (FORMAT PARQUET, ROW_GROUP_SIZE 200_000 %s) + `, selectClause, input.Filepath, strings.Join(columnsLength, "+"), allowedRowSize, offset, output.Filepath, compression) + + if printQuery { + fmt.Println("Query:", query) // TODO: Remove tabs + return nil + } + + fmt.Println(settings.Base.Render("\nTransforming and writing to output parquet...")) + _, err := lu.Db.Exec(query) + if err != nil { + return fmt.Errorf("failed to execute transformation: %w", err) + } + fmt.Println(settings.Base.Render("Transformation complete!\n")) + + newParquet, err := GetParquet(lu.Db, output.Filepath) + if err != nil { + return err + } + newParquet.PrintParquet() + + return nil +} + +func GetParquet(db *sql.DB, inputFile string) (parquet *Parquet, err error) { + parquet = &Parquet{} + parquet.Filepath = inputFile + + parquet.Columns, err = getColumns(db, inputFile) + if err != nil { + return + } + parquet.NRows, err = countRows(db, inputFile) + if err != nil { + return + } + parquet.Sample, err = getFirstNRows(db, inputFile, 6) + if err != nil { + return + } + + n := strings.LastIndex(inputFile, "/") + if n == -1 { + parquet.Filename = inputFile + } else { + parquet.Filename = inputFile[n+1:] + } + + return +} diff --git a/leak-utils/parquet/suggestions.go b/leak-utils/parquet/suggestions.go new file mode 100644 index 0000000..2038c67 --- /dev/null +++ b/leak-utils/parquet/suggestions.go @@ -0,0 +1,81 @@ +package parquet + +import ( + "slices" +) + +func getSuggestion(col string) string { + col = formatColumnName(col) + knownNames := []string{ + "date", + "phone", + "username", + "address", + "email", + "postal_code", + "city", + "country", + "state", + "age", + "gender", + "password", + "password_hash", + "full_name", + "last_name", + "name", // Will be renamed to full_name later + "first_name", + "birth_date", + "url", + "ip", + } + if slices.Contains(knownNames, col) { + return col + } + if col == "user" { + return "username" + } + if col == "login" { + return "username" + } + if col == "sex" { + return "gender" + } + if col == "ip_address" { + return "ip" + } + if col == "password_hashed" { + return "password_hash" + } + if col == "firstname" { + return "first_name" + } + if col == "lastname" { + return "last_name" + } + if col == "fullname" { + return "full_name" + } + if col == "mail" { + return "email" + } + if col == "zip" || col == "postalcode" || col == "zipcode" || col == "postal" || col == "zip_code" { + return "postal_code" + } + if col == "street_address" { + return "address" + } + if col == "hash" || col == "hashed_password" || col == "hash_password" { + return "password_hash" + } + if col == "birthdate" || col == "dob" || col == "date_of_birth" { + return "birth_date" + } + + return "" +} + +// HINTS: +// date: _date +// url: _url, link +// address: _address +// diff --git a/leak-utils/parquet/utils.go b/leak-utils/parquet/utils.go new file mode 100644 index 0000000..e227865 --- /dev/null +++ b/leak-utils/parquet/utils.go @@ -0,0 +1,105 @@ +package parquet + +import ( + "database/sql" + "fmt" + "strconv" + "strings" +) + +// getColumns retrieves the column names from the Parquet file. +func getColumns(db *sql.DB, filepath string) ([]string, error) { + // Create a view from the parquet file + query := fmt.Sprintf("CREATE OR REPLACE VIEW parquet_view AS SELECT * FROM read_parquet('%s')", filepath) + _, err := db.Exec(query) + if err != nil { + return nil, fmt.Errorf("failed to create view: %w", err) + } + + // Get column information + rows, err := db.Query("DESCRIBE parquet_view") + if err != nil { + return nil, fmt.Errorf("failed to describe view: %w", err) + } + defer rows.Close() + + var columns []string + for rows.Next() { + var colName, colType, nullable, key, defaultVal, extra sql.NullString + err := rows.Scan(&colName, &colType, &nullable, &key, &defaultVal, &extra) + if err != nil { + return nil, fmt.Errorf("failed to scan row: %w", err) + } + if colName.Valid { + columns = append(columns, colName.String) + } + } + + return columns, nil +} + +// getFirstNRows retrieves the first N rows from the Parquet file. +func getFirstNRows(db *sql.DB, inputFile string, n int) ([][]string, error) { + query := fmt.Sprintf("SELECT * FROM read_parquet('%s') LIMIT %d", inputFile, n) + rows, err := db.Query(query) + if err != nil { + return nil, fmt.Errorf("failed to query parquet file: %w", err) + } + defer rows.Close() + + cols, err := rows.Columns() + if err != nil { + return nil, fmt.Errorf("failed to get columns: %w", err) + } + + var results [][]string + for rows.Next() { + values := make([]sql.NullString, len(cols)) + valuePtrs := make([]any, len(cols)) + for i := range values { + valuePtrs[i] = &values[i] + } + + err := rows.Scan(valuePtrs...) + if err != nil { + return nil, fmt.Errorf("failed to scan row: %w", err) + } + + var row []string + for _, val := range values { + if val.Valid { + row = append(row, val.String) + } else { + row = append(row, "NULL") + } + } + results = append(results, row) + } + + return results, nil +} + +// countRows counts the number of rows in the Parquet file. +func countRows(db *sql.DB, inputFile string) (int64, error) { + var count int64 + err := db.QueryRow(fmt.Sprintf("SELECT COUNT(*) FROM read_parquet('%s')", inputFile)).Scan(&count) + if err != nil { + return 0, fmt.Errorf("failed to count rows: %w", err) + } + return count, nil +} + +// formatWithSpaces formats an integer with spaces as thousand separators. +func formatWithSpaces(n int64) string { + s := strconv.FormatInt(n, 10) + + var b strings.Builder + l := len(s) + for i, c := range s { + if i != 0 && (l-i)%3 == 0 { + b.WriteRune(' ') + } + b.WriteRune(c) + } + return b.String() +} diff --git a/leak-utils/settings/colors.go b/leak-utils/settings/colors.go new file mode 100644 index 0000000..d3dbf41 --- /dev/null +++ b/leak-utils/settings/colors.go @@ -0,0 +1,27 @@ +package settings + +import ( + "github.com/charmbracelet/lipgloss/v2" +) + +var ( + purple = lipgloss.Color("99") + lightPurple = lipgloss.Color("98") + yellow = lipgloss.Color("220") + gray = lipgloss.Color("245") + lightGray = lipgloss.Color("241") + + Header = lipgloss.NewStyle().Foreground(purple).Bold(true) + Accent = lipgloss.NewStyle().Foreground(lightPurple) + Base = lipgloss.NewStyle().Foreground(lightGray) + Alert = lipgloss.NewStyle().Foreground(yellow).Bold(true) + Muted = lipgloss.NewStyle().Foreground(gray) +) + +func DisableColors() { + Header = lipgloss.NewStyle() + Accent = lipgloss.NewStyle() + Base = lipgloss.NewStyle() + Alert = lipgloss.NewStyle() + Muted = lipgloss.NewStyle() +} diff --git a/leak-utils/settings/settings.go b/leak-utils/settings/settings.go new file mode 100644 index 0000000..6f0ecd9 --- /dev/null +++ b/leak-utils/settings/settings.go @@ -0,0 +1,9 @@ +package settings + +import "database/sql" + +type LeakUtils struct { + Debug bool + Compression string // Compression of the output file (e.g., "SNAPPY", "ZSTD", "NONE" or "") + Db *sql.DB +} diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 0000000..5581a19 --- /dev/null +++ b/netlify.toml @@ -0,0 +1,4 @@ +[build] + base = "front" + publish = "dist" + command = "bun run build" diff --git a/nix/back.nix b/nix/back.nix new file mode 100644 index 0000000..26d5a5e --- /dev/null +++ b/nix/back.nix @@ -0,0 +1,155 @@ +{ + pkgs, + lib, + self, +}: let + name = "eleakxir"; + + package = pkgs.buildGoModule { + pname = name; + version = "0.1.0"; + src = ../back; + vendorHash = ""; + + buildInputs = [ + pkgs.duckdb + pkgs.arrow-cpp + ]; + }; +in { + package = package; + + nixosModule = {config, ...}: let + cfg = config.services."${name}"; + in { + options.services."${name}" = { + enable = lib.mkEnableOption "Enable the ${name} service"; + user = lib.mkOption { + type = lib.types.str; + default = name; + description = "User to run the ${name} service as"; + }; + group = lib.mkOption { + type = lib.types.str; + default = name; + description = "Group to run the ${name} service as"; + }; + port = lib.mkOption { + type = lib.types.port; + default = 9198; + description = "Port for the ${name} service"; + }; + folders = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = []; + description = "Folders to monitor for parquet files"; + }; + cacheFolder = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Cache folder"; + }; + limit = lib.mkOption { + type = lib.types.int; + default = 200; + description = "Limit of results to return"; + }; + password = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Password for auth (empty means no auth)"; + }; + debug = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Debug mode"; + }; + maxCacheDuration = lib.mkOption { + type = lib.types.str; + default = "24h"; + description = "Max result cache duration (30m, 2h, 1d)"; + }; + reloadDataleaksInterval = lib.mkOption { + type = lib.types.str; + default = "1h"; + description = "Interval to reload dataleaks (30m, 2h, 1d)"; + }; + minimumQueryLength = lib.mkOption { + type = lib.types.int; + default = 3; + description = "Minimum query length"; + }; + baseColumns = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = []; + description = "Base columns are used when the column searched is 'all'"; + }; + githubRecon = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Activate the github-recon OSINT module"; + }; + githubToken = lib.mkOption { + type = lib.types.str; + default = ""; + description = "GitHub token to use for Github recon"; + }; + githubDeepMode = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Activate the github-recon deep mode"; + }; + }; + + config = lib.mkIf cfg.enable { + users.users."${cfg.user}" = { + isSystemUser = true; + group = cfg.group; + }; + users.groups."${cfg.group}" = {}; + + systemd.services."${name}" = { + description = "${name} service"; + after = ["network.target"]; + wantedBy = ["multi-user.target"]; + serviceConfig = { + ExecStart = "${self.packages.${pkgs.system}.backend}/bin/cmd"; + Restart = "always"; + User = cfg.user; + Group = cfg.group; + StateDirectory = name; + ReadWritePaths = ["/var/lib/${name}"]; + WorkingDirectory = "/var/lib/${name}"; + + Environment = [ + "PORT=${toString cfg.port}" + "DATALEAKS_FOLDERS=${lib.strings.concatStringsSep "," cfg.folders}" + "DATALEAKS_CACHE_FOLDER=${cfg.cacheFolder}" + "LIMIT=${toString cfg.limit}" + "PASSWORD=${toString cfg.password}" + "DEBUG=${ + if cfg.debug + then "true" + else "false" + }" + "MAX_CACHE_DURATION=${cfg.maxCacheDuration}" + "RELOAD_DATALEAKS_INTERVAL=${cfg.reloadDataleaksInterval}" + "MINIMUM_QUERY_LENGTH=${toString cfg.minimumQueryLength}" + "BASE_COLUMNS=${lib.strings.concatStringsSep "," cfg.baseColumns}" + "GITHUB_RECON=${ + if cfg.githubRecon + then "true" + else "false" + }" + "GITHUB_TOKEN=${cfg.githubToken}" + "GITHUB_DEEP_MODE=${ + if cfg.githubDeepMode + then "true" + else "false" + }" + ]; + }; + }; + }; + }; +} diff --git a/nix/devshell.nix b/nix/devshell.nix new file mode 100644 index 0000000..e3d07f0 --- /dev/null +++ b/nix/devshell.nix @@ -0,0 +1,12 @@ +{pkgs, ...}: { + devShell = pkgs.mkShell { + buildInputs = with pkgs; [ + duckdb + air + # OSINT tools + ghunt + sherlock + holehe + ]; + }; +} diff --git a/nix/leak-utils.nix b/nix/leak-utils.nix new file mode 100644 index 0000000..fbc6744 --- /dev/null +++ b/nix/leak-utils.nix @@ -0,0 +1,21 @@ +{ + pkgs, + lib, + self, +}: let + name = "leak-utils"; + + package = pkgs.buildGoModule { + pname = name; + version = "0.1.0"; + src = ../leak-utils; + vendorHash = "sha256-NDY3T3FhQ2iXJr3v3sxTX9taVTU9LPCLd/emWukHZcs="; + + buildInputs = [ + pkgs.duckdb + pkgs.arrow-cpp + ]; + }; +in { + package = package; +}