This commit is contained in:
Hadi
2025-09-24 17:20:03 +02:00
commit b9fbed9a54
83 changed files with 6241 additions and 0 deletions

BIN
.github/assets/banner.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
.github/assets/logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
result/
testdata/

13
CONTRIBUTING.md Normal file
View File

@@ -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.

21
LICENSE Normal file
View File

@@ -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.

117
README.md Normal file
View File

@@ -0,0 +1,117 @@
<div align="center">
<img alt="nixy logo" src="https://raw.githubusercontent.com/anotherhadi/eleakxir/main/.github/assets/logo.png" width="120px" />
</div>
<br>
# 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.

21
back/.air.toml Normal file
View File

@@ -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

1
back/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
tmp/

174
back/api/api.go Normal file
View File

@@ -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()
}
}

21
back/cmd/main.go Normal file
View File

@@ -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)
}
}

70
back/go.mod Normal file
View File

@@ -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
)

179
back/go.sum Normal file
View File

@@ -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=

View File

@@ -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
}

View File

@@ -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
}

68
back/search/search.go Normal file
View File

@@ -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))
}

111
back/server/dataleak.go Normal file
View File

@@ -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),
}
}

61
back/server/server.go Normal file
View File

@@ -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
}

129
back/server/settings.go Normal file
View File

@@ -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
}

131
back/server/utils.go Normal file
View File

@@ -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
}

43
flake.lock generated Normal file
View File

@@ -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
}

58
flake.nix Normal file
View File

@@ -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;
};
};
}

26
front/.gitignore vendored Normal file
View File

@@ -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

377
front/bun.lock Normal file
View File

@@ -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=="],
}
}

14
front/index.html Normal file
View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Eleakxir</title>
<meta name="description" content="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.">
</head>
<body>
<div id="app" style="display: contents"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

34
front/package.json Normal file
View File

@@ -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"
}
}

3
front/public/favicon.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="141" height="205" viewBox="0 0 141 205" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M69.7444 0C84.49 25.1637 100.559 49.4708 117.95 72.9219C126.451 85.0149 133.113 98.1035 137.934 112.188C144.168 135.735 140.195 157.355 126.014 177.046C109.938 196.591 89.1945 205.53 63.7844 203.865C36.717 200.867 17.4935 187.019 6.11353 162.321C-1.9191 142.522 -2.03583 122.655 5.76294 102.722C9.71019 93.7604 14.3849 85.2295 19.7864 77.1289C35.0593 56.0531 49.5504 34.4338 63.259 12.2705C65.6086 8.27249 67.7699 4.18207 69.7444 0ZM100.596 81.3359C102.957 92.649 102.198 103.751 98.3176 114.642C93.9276 124.99 87.7338 134.105 79.7366 141.987C77.6951 144.434 75.8254 147.005 74.1272 149.7C70.5033 155.43 68.5745 161.682 68.342 168.456C68.1692 175.079 70.6236 180.455 75.7043 184.583C89.1062 183.345 100.267 177.678 109.186 167.58C123.101 149.518 125.672 129.885 116.899 108.682C112.25 99.0223 106.815 89.9071 100.596 81.3359ZM70.095 36.4609C59.5617 53.4794 48.4018 70.0738 36.6145 86.2441C30.2619 94.8335 25.2366 104.183 21.5393 114.291C15.2642 135.159 19.2377 153.74 33.4592 170.034C39.7381 176.533 47.2756 180.798 56.0715 182.83C55.7506 182.699 55.4583 182.524 55.1956 182.305C40.6021 163.32 39.6671 143.687 52.3909 123.406C58.721 114.622 64.9148 105.74 70.9719 96.7617C72.559 94.0549 73.9614 91.2501 75.179 88.3477C81.8825 70.1929 80.1876 52.8971 70.095 36.4609Z" fill="#DDD6DF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
front/public/l.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 KiB

6
front/src/App.svelte Normal file
View File

@@ -0,0 +1,6 @@
<script lang="ts">
import './app.css'
import { Router } from 'sv-router';
</script>
<Router />

94
front/src/app.css Normal file
View File

@@ -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;
}

View File

@@ -0,0 +1,79 @@
<script lang="ts">
import {
ChevronDown,
ChevronUp,
type Icon as IconType,
} from "@lucide/svelte";
import type { Snippet } from "svelte";
import { cn } from "../utils";
let isOpen = $state<boolean>(false);
const {
imageUrl,
icon,
title,
subtitle,
children,
}: {
imageUrl?: string | null;
icon: typeof IconType;
title: string;
subtitle?: string;
children?: Snippet;
} = $props();
</script>
<button
class={cn("list-row text-left bg-base-200/40",
children != null ? "cursor-pointer hover:bg-base-300/75" : ""
)}
class:bg-base-300={isOpen}
class:rounded-b-none={isOpen}
onclick={() => {
if (children != null) {
isOpen = !isOpen;
}
}}
>
<div>
{#if imageUrl && imageUrl.length > 0}
<img
src="https://icons.duckduckgo.com/ip3/{imageUrl}.ico"
class="size-10 rounded-box bg-neutral"
alt="Favicon of {imageUrl}"
/>
{:else}
{@const Icon = icon}
<div
class="size-10 rounded-box bg-neutral items-center justify-center flex"
>
<Icon />
</div>
{/if}
</div>
<div class="flex flex-col justify-center">
<div class="font-semibold">{title}</div>
{#if subtitle != null && subtitle.length !== 0}
<div class="text-xs uppercase font-semibold opacity-60">
{subtitle}
</div>
{/if}
</div>
{#if children != null}
<div class="btn btn-square btn-ghost">
{#if isOpen}
<ChevronUp size={12} />
{:else}
<ChevronDown size={12} />
{/if}
</div>
{/if}
</button>
{#if children != null}
{#if isOpen}
<li class="list-row bg-base-200 rounded-t-none mb-2">
{@render children()}
</li>
{/if}
{/if}

View File

@@ -0,0 +1,29 @@
<script lang="ts">
import { Moon, Sun, SunMoon } from "@lucide/svelte";
</script>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-sm btn-square m-1">
<SunMoon size={16 }/>
</div>
<div
class="dropdown-content bg-base-300 rounded-box z-1 w-52 p-2 shadow-2xl grid gap-2"
>
<button
data-set-theme="light"
class="theme-controller btn btn-sm btn-block btn-ghost justify-start"
aria-label="Light"
>
<Sun />
Light
</button>
<button
data-set-theme="dark"
class="theme-controller btn btn-sm btn-block btn-ghost justify-start"
aria-label="Dark"
>
<Moon />
Dark
</button>
</div>
</div>

View File

@@ -0,0 +1,104 @@
<script lang="ts">
import { cn } from "$lib/utils";
import { onMount, tick } from "svelte";
let {
containerRef = $bindable(),
class:className = "",
fromRef = $bindable(),
toRef = $bindable(),
curvature = 0,
reverse = false, // Include the reverse prop
pathColor = "gray",
pathWidth = 2,
pathOpacity = 0.2,
startXOffset = 0,
startYOffset = 0,
endXOffset = 0,
endYOffset = 0,
}= $props();
let id = crypto.randomUUID().slice(0, 8);
let pathD = $state("");
let svgDimensions = { width: 0, height: 0 };
let updatePath = () => {
if (!containerRef || !fromRef || !toRef) {
return;
}
let containerRect = containerRef?.getBoundingClientRect();
let rectA = fromRef?.getBoundingClientRect();
let rectB = toRef?.getBoundingClientRect();
let svgWidth = containerRect.width;
let svgHeight = containerRect.height;
svgDimensions.width = svgWidth;
svgDimensions.height = svgHeight;
let startX =
rectA.left - containerRect.left + rectA.width / 2 + startXOffset;
let startY =
rectA.top - containerRect.top + rectA.height / 2 + startYOffset;
let endX = rectB.left - containerRect.left + rectB.width / 2 + endXOffset;
let endY = rectB.top - containerRect.top + rectB.height / 2 + endYOffset;
let controlY = startY - curvature;
let d = `M ${startX},${startY} Q ${
(startX + endX) / 2
},${controlY} ${endX},${endY}`;
pathD = d;
};
onMount(async () => {
await tick().then(() => {
updatePath();
const resizeObserver = new ResizeObserver((entries) => {
// For all entries, recalculate the path
for (let entry of entries) {
updatePath();
}
});
// Observe the container element
if (containerRef) {
resizeObserver.observe(containerRef);
}
});
});
</script>
<svg
fill="none"
width={svgDimensions.width}
height={svgDimensions.height}
xmlns="http://www.w3.org/2000/svg"
class={cn(
"pointer-events-none absolute left-0 top-0 transform-gpu stroke-2 animate-pulse",
className,
)}
viewBox={`0 0 ${svgDimensions.width} ${svgDimensions.height}`}
>
<path
d={pathD}
stroke={pathColor}
stroke-width={pathWidth}
stroke-opacity={pathOpacity}
stroke-linecap="round"
/>
<path
d={pathD}
stroke-width={pathWidth}
stroke={`url(#${id})`}
stroke-opacity="1"
stroke-linecap="round"
/>
<defs>
<linearGradient {id} gradientUnits="userSpaceOnUse" class="transform-gpu">
<stop class="[stop-color:var(--color-primary)]" stop-opacity="0"></stop>
<stop class="[stop-color:var(--color-primary)]"></stop>
<stop offset="32.5%" class="[stop-color:var(--color-primary)]"></stop>
<stop offset="100%" class="[stop-color:var(--color-primary)]" stop-opacity="0"
></stop>
</linearGradient>
</defs>
</svg>

View File

@@ -0,0 +1,125 @@
<script lang="ts">
import { cn } from "$lib/utils";
import { Github, IdCard, Key, User } from "@lucide/svelte";
import AnimatedBeam from "./AnimatedBeam.svelte";
import Circle from "./Circle.svelte";
let containerRef = $state();
let div1Ref = $state();
let div2Ref = $state();
let div3Ref = $state();
let div4Ref = $state();
let div5Ref = $state();
let div6Ref = $state();
let div7Ref = $state();
let className: any = $state("");
export { className as class };
</script>
<div
bind:this={containerRef}
class={cn("relative flex w-full items-center justify-center ", className)}
>
<div
class="flex h-full w-full flex-row justify-between gap-10 max-w-lg items-center"
>
<div class="flex flex-col justify-center gap-2">
<!-- Div 1 -->
<Circle bind:ref={div1Ref}>
<div class="tooltip" data-tip="Leak of user's informations">
<User />
</div>
</Circle>
<!-- Div 2 -->
<Circle bind:ref={div2Ref}>
<div class="tooltip" data-tip="Leak of user's passwords">
<Key />
</div>
</Circle>
<!-- Div 3 -->
<Circle bind:ref={div3Ref}>
<div class="tooltip" data-tip="Leak of personal informations">
<IdCard />
</div>
</Circle>
<!-- Div 4 -->
<Circle bind:ref={div4Ref}>
<div class="tooltip" data-tip="Github recon">
<Github />
</div>
</Circle>
<!-- Div 5 -->
<Circle bind:ref={div5Ref}>
<div class="tooltip" data-tip="Google hunt">
<svg
width="16"
viewBox="0 0 256 262"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid"
><path
d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622 38.755 30.023 2.685.268c24.659-22.774 38.875-56.282 38.875-96.027"
fill="#fff"
/><path
d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055-34.523 0-63.824-22.773-74.269-54.25l-1.531.13-40.298 31.187-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1"
fill="#fff"
/><path
d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82 0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602l42.356-32.782"
fill="#fff"
/><path
d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0 79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251"
fill="#fff"
/></svg
>
</div>
</Circle>
</div>
<div class="flex flex-col justify-center">
<!-- Div 6 -->
<Circle bind:ref={div6Ref}>
<div class="tooltip" data-tip="Your eleakxir backend">
<svg
width={24}
viewBox="0 0 141 205"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class={cn("fill-primary", className)}
>
<path
d="M69.7444 0C84.49 25.1637 100.559 49.4708 117.95 72.9219C126.451 85.0149 133.113 98.1035 137.934 112.188C144.168 135.735 140.195 157.355 126.014 177.046C109.938 196.591 89.1945 205.53 63.7844 203.865C36.717 200.867 17.4935 187.019 6.11353 162.321C-1.9191 142.522 -2.03583 122.655 5.76294 102.722C9.71019 93.7604 14.3849 85.2295 19.7864 77.1289C35.0593 56.0531 49.5504 34.4338 63.259 12.2705C65.6086 8.27249 67.7699 4.18207 69.7444 0ZM100.596 81.3359C102.957 92.649 102.198 103.751 98.3176 114.642C93.9276 124.99 87.7338 134.105 79.7366 141.987C77.6951 144.434 75.8254 147.005 74.1272 149.7C70.5033 155.43 68.5745 161.682 68.342 168.456C68.1692 175.079 70.6236 180.455 75.7043 184.583C89.1062 183.345 100.267 177.678 109.186 167.58C123.101 149.518 125.672 129.885 116.899 108.682C112.25 99.0223 106.815 89.9071 100.596 81.3359ZM70.095 36.4609C59.5617 53.4793 48.4018 70.0738 36.6145 86.2441C30.2619 94.8335 25.2366 104.183 21.5393 114.291C15.2642 135.159 19.2377 153.74 33.4592 170.034C39.7381 176.533 47.2756 180.798 56.0715 182.83C55.7506 182.699 55.4583 182.524 55.1956 182.305C40.6021 163.32 39.6671 143.687 52.3909 123.406C58.721 114.622 64.9149 105.74 70.9719 96.7617C72.559 94.0549 73.9614 91.2501 75.179 88.3477C81.8825 70.1929 80.1876 52.8971 70.095 36.4609Z"
/>
</svg>
</div>
</Circle>
</div>
<div class="flex flec-col justify-center">
<!-- Div 7 -->
<Circle bind:ref={div7Ref}>
<div class="tooltip" data-tip="This web client">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-monitor-icon lucide-monitor"
><rect width="20" height="14" x="2" y="3" rx="2" /><line
x1="8"
x2="16"
y1="21"
y2="21"
/><line x1="12" x2="12" y1="17" y2="21" /></svg
>
</div>
</Circle>
</div>
</div>
<AnimatedBeam bind:containerRef bind:fromRef={div1Ref} bind:toRef={div6Ref} />
<AnimatedBeam bind:containerRef bind:fromRef={div2Ref} bind:toRef={div6Ref} />
<AnimatedBeam bind:containerRef bind:fromRef={div3Ref} bind:toRef={div6Ref} />
<AnimatedBeam bind:containerRef bind:fromRef={div4Ref} bind:toRef={div6Ref} />
<AnimatedBeam bind:containerRef bind:fromRef={div5Ref} bind:toRef={div6Ref} />
<AnimatedBeam bind:containerRef bind:fromRef={div6Ref} bind:toRef={div7Ref} />
</div>

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import { cn } from "$lib/utils";
let { children, label ="", ref=$bindable()} = $props();
let className: any = $state("");
export { className as class };
</script>
<div class="tooltip z-10" data-tip={label}>
<div
bind:this={ref}
class={cn(
"bg-base-100 hover:bg-base-200 border-base-200 z-10 flex h-12 w-12 items-center justify-center rounded-full border-2 p-3 transition-all duration-200 cursor-pointer",
className,
)}
>
{@render children()}
</div>
</div>

View File

@@ -0,0 +1,115 @@
<script lang="ts">
import type { Dataleak } from "$src/lib/types";
import { Replace, Search } from "@lucide/svelte";
let {
dataleaks,
perPage = 5,
showColumns = false,
}: {
dataleaks: Dataleak[];
perPage?: number;
showColumns?: boolean;
} = $props();
let page = $state(1);
let filter = $state("");
let filteredDataleaks = $state<Dataleak[]>(dataleaks);
let paginatedDataleaks = $state<Dataleak[]>([]);
let totalPages = $state(0);
$effect(() => {
if (filter.trim() === "") {
filteredDataleaks = dataleaks;
} else {
const lowerFilter = filter.toLowerCase();
filteredDataleaks = dataleaks.filter((item) =>
item.Name.toLowerCase().includes(lowerFilter),
);
}
page = 1;
});
$effect(() => {
if (filteredDataleaks) {
totalPages = Math.ceil(filteredDataleaks.length / perPage);
const start = (page - 1) * perPage;
const end = start + perPage;
paginatedDataleaks = filteredDataleaks.slice(start, end);
if (page > totalPages) {
page = totalPages > 0 ? totalPages : 1;
}
}
});
function previousPage() {
if (page > 1) {
page--;
}
}
function nextPage() {
if (page < totalPages) {
page++;
}
}
</script>
<div class="my-4 flex flex-col gap-2">
<label class="input input-xs w-full">
<Search size={12} />
<input class="grow" placeholder="Filter" bind:value={filter} />
</label>
<div class="overflow-x-auto">
<table class="table">
<!-- head -->
<thead>
<tr>
<th>Name</th>
<th>Number of rows</th>
{#if showColumns}
<th>Columns</th>
{/if}
</tr>
</thead>
<tbody>
{#if paginatedDataleaks.length > 0}
{#each paginatedDataleaks as item}
<tr class="hover:bg-base-300">
<th>
{item.Name}
</th>
<td>{item.Length.toLocaleString("fr")}</td>
{#if showColumns}
<td class="capitalize">
{item.Columns.map((col) => col.replace(/_/g, " ")).join(", ")}
</td>
{/if}
</tr>
{/each}
{:else}
<tr class="hover:bg-base-300">
<td colspan="2" class="text-center leading-9"
><span class="text-3xl">(·.·)</span><br />No data wells found</td
>
</tr>
{/if}
</tbody>
</table>
</div>
{#if totalPages > 1}
<div class="join m-auto mt-5">
<button class="join-item btn" onclick={previousPage} disabled={page === 1}
>«</button
>
<button class="join-item btn">Page {page} / {totalPages}</button>
<button
class="join-item btn"
onclick={nextPage}
disabled={page === totalPages}>»</button
>
</div>
{/if}
</div>

View File

@@ -0,0 +1,130 @@
<script lang="ts">
import type { History } from "$src/lib/types";
import { formatDate } from "$src/lib/utils";
import { Search } from "@lucide/svelte";
import { navigate, p } from "sv-router/generated";
let { history, perPage = 5 }: { history: History; perPage?: number } =
$props();
let page = $state(1);
let filter = $state("");
let filteredHistory = $state<History>(history);
let paginatedHistory = $state<History>([]);
let totalPages = $state(0);
$effect(() => {
if (filter.trim() === "") {
filteredHistory = history;
} else {
const lowerFilter = filter.toLowerCase();
filteredHistory = history.filter((item) =>
item.Query.Text.toLowerCase().includes(lowerFilter),
);
}
page = 1;
});
$effect(() => {
if (filteredHistory) {
totalPages = Math.ceil(filteredHistory.length / perPage);
const start = (page - 1) * perPage;
const end = start + perPage;
paginatedHistory = filteredHistory.slice(start, end);
if (page > totalPages) {
page = totalPages > 0 ? totalPages : 1;
}
}
});
function previousPage() {
if (page > 1) {
page--;
}
}
function nextPage() {
if (page < totalPages) {
page++;
}
}
</script>
<div class="my-4 flex flex-col gap-2">
<label class="input input-xs w-full">
<Search size={12} />
<input class="grow" placeholder="Filter" bind:value={filter} />
</label>
<div class="overflow-x-auto">
<table class="table">
<!-- head -->
<thead>
<tr>
<th>Query</th>
<th>Results</th>
<th>Status</th>
<th>Date</th>
<th></th>
</tr>
</thead>
<tbody>
{#if paginatedHistory.length > 0}
{#each paginatedHistory as item}
<tr class="hover:bg-base-300">
<th>
<button
onclick={() => {
navigate(`/search/:id`, { params: { id: item.Id } });
}}
class="btn btn-link p-0 no-underline text-base-content"
>
{item.Query.Text}
</button>
</th>
<td>{item.Results}</td>
<td
><div
class="badge badge-xs"
class:badge-success={item.Status === "completed"}
class:badge-warning={item.Status === "pending"}
>
{item.Status}
</div></td
>
<td>{formatDate(item.Date)}</td>
<td
onclick={() => {
navigate(`/search/:id`, { params: { id: item.Id } });
}}
><button class="btn btn-xs btn-square"
><Search size={11} /></button
></td
>
</tr>
{/each}
{:else}
<tr class="hover:bg-base-300">
<td colspan="5" class="text-center leading-9"
><span class="text-3xl">(·.·)</span><br />No history found</td
>
</tr>
{/if}
</tbody>
</table>
</div>
{#if totalPages > 1}
<div class="join m-auto mt-5">
<button class="join-item btn" onclick={previousPage} disabled={page === 1}
>«</button
>
<button class="join-item btn">Page {page} / {totalPages}</button>
<button
class="join-item btn"
onclick={nextPage}
disabled={page === totalPages}>»</button
>
</div>
{/if}
</div>

View File

@@ -0,0 +1,46 @@
<script lang="ts"></script>
<div>
<p>
Eleakxir's search engine is designed to be both fast and flexible, letting
you find what you need in multiple ways.
</p>
<h3 class="h3 mt-4 mb-2">Search Modes</h3>
<p>
<span class="text-primary font-semibold">All:</span> 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.
</p>
<p>
<span class="text-primary font-semibold">Specific column:</span>
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.
</p>
<p>
<span class="text-primary font-semibold">Full Text:</span> 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.
</p>
<h3 class="h3 mt-4 mb-2">Query Matching</h3>
<p>
<span class="text-primary font-semibold">Standard Search:</span> 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.
</p>
<p>
<span class="text-primary font-semibold">Exact Match:</span> 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.
</p>
</div>

View File

@@ -0,0 +1,313 @@
<script lang="ts">
import Accordion from "$src/lib/components/accordion.svelte";
import Table from "$src/lib/components/table.svelte";
import type { GithubResult } from "$src/lib/types";
import { FlattenObject } from "$src/lib/utils";
import {
Building,
ExternalLink,
GitCommitVertical,
Handshake,
Key,
Mail,
UserRoundPen,
} from "@lucide/svelte";
const { githubResult }: { githubResult: GithubResult } = $props();
</script>
{#if githubResult.UsernameResult}
<div class="w-full">
<div class="flex flex-wrap gap-5">
<div class="avatar">
<div class="w-24 h-24 rounded-xl">
<img
src={githubResult.UsernameResult.User.AvatarURL}
alt="Avatar of {githubResult.UsernameResult.User.Username}"
/>
</div>
</div>
<div class="flex flex-col gap-2">
<div class="flex flex-col">
<h3 class="h3">{githubResult.UsernameResult.User.Name}</h3>
<p class="text-base-content/60">
@{githubResult.UsernameResult.User.Username}
</p>
</div>
<p class="max-w-sm">{githubResult.UsernameResult.User.Bio}</p>
</div>
</div>
<div class="card card-border border-neutral shadow my-8">
<div class="grid">
<Table
row={{
publicRepos: githubResult.UsernameResult.User.PublicRepos,
followers: githubResult.UsernameResult.User.Followers,
following: githubResult.UsernameResult.User.Following,
createdAt: new Date(
githubResult.UsernameResult.User.CreatedAt,
).toLocaleDateString(),
email: githubResult.UsernameResult.User.Email,
location: githubResult.UsernameResult.User.Location,
company: githubResult.UsernameResult.User.Company,
url:
"https://github.com/" + githubResult.UsernameResult.User.Username,
}}
/>
</div>
</div>
{#if githubResult.UsernameResult.Socials && githubResult.UsernameResult.Socials.length > 0}
<div class="mt-4">
<h4 class="h4 mb-2">Social Links</h4>
<ul class="flex gap-4 flex-col mt-4 mb-6">
{#each githubResult.UsernameResult.Socials as social}
<a href={social.URL} target="_blank" rel="noopener noreferrer">
<div class="badge bg-base-300">
<ExternalLink size={12} />
{social.URL}
</div>
</a>
{/each}
</ul>
</div>
{/if}
{#if githubResult.UsernameResult.CloseFriends && githubResult.UsernameResult.CloseFriends.length > 0}
<div class="mt-4">
<ul class="list bg-base-100 rounded-box shadow-md">
<Accordion
icon={Handshake}
title={"Close Friends"}
subtitle={ githubResult.UsernameResult.CloseFriends.length + " close friends found"}
>
<Table
row={githubResult.UsernameResult.CloseFriends}
/>
</Accordion>
</ul>
</div>
{/if}
{#if githubResult.UsernameResult.Orgs && githubResult.UsernameResult.Orgs.length > 0}
<div class="mt-4">
<h4 class="h4 mb-2">Organizations</h4>
<ul class="list bg-base-100 rounded-box shadow-md">
<Accordion
icon={Building}
title="Organizations"
subtitle={"Found " + githubResult.UsernameResult.Orgs.length + " organizations"}
>
<Table
row={githubResult.UsernameResult.Orgs}
/>
</Accordion>
</ul>
</div>
{/if}
{#if githubResult.UsernameResult.Commits && githubResult.UsernameResult.Commits.length > 0}
<div class="mt-4">
<h4 class="h4 mb-2">Commits</h4>
<ul class="list bg-base-100 rounded-box shadow-md">
{#each githubResult.UsernameResult.Commits as commit}
<Accordion
icon={GitCommitVertical}
title={commit.Name + " <" + commit.Email + ">"}
subtitle={"Occurrences: " + commit.Occurrences}
>
<Table
row={{
name: commit.Name,
email: commit.Email,
url: "https://github.com/" + commit.FirstFoundIn,
occurrences: commit.Occurrences,
}}
/>
</Accordion>
{/each}
</ul>
</div>
{/if}
{#if githubResult.UsernameResult.SshKeys && githubResult.UsernameResult.SshKeys.length > 0}
<div class="mt-4">
<h4 class="h4 mb-2">SSH Keys</h4>
<ul class="list bg-base-100 rounded-box shadow-md">
{#each githubResult.UsernameResult.SshKeys as key}
<Accordion
icon={Key}
title={"Created At: " +
new Date(key.CreatedAt).toLocaleDateString()}
subtitle={"Last Used: " +
(key.LastUsed !== "0001-01-01 00:00:00 +0000 UTC"
? new Date(key.LastUsed).toLocaleDateString()
: "Never")}
>
<pre class="overflow-x-auto p-2 bg-base-200 rounded"><code
class="break-all">{key.Key}</code
></pre>
</Accordion>
{/each}
</ul>
</div>
{/if}
{#if githubResult.UsernameResult.SshSigningKeys && githubResult.UsernameResult.SshSigningKeys.length > 0}
<div class="mt-4">
<h4 class="h4 mb-2">SSH Signing Keys</h4>
<ul class="list bg-base-100 rounded-box shadow-md">
{#each githubResult.UsernameResult.SshSigningKeys as key}
<Accordion
icon={Key}
title={key.Title}
subtitle={"Created At: " + key.CreatedAt}
>
<pre class="overflow-x-auto p-2 bg-base-200 rounded"><code
class="break-all">{key.Key}</code
></pre>
</Accordion>
{/each}
</ul>
</div>
{/if}
{#if githubResult.UsernameResult.GpgKeys && githubResult.UsernameResult.GpgKeys.length > 0}
<div class="mt-4">
<h4 class="h4 mb-2">GPG Keys</h4>
<ul class="list bg-base-100 rounded-box shadow-md">
{#each githubResult.UsernameResult.GpgKeys as key}
<Accordion
icon={Key}
title={key.Emails && key.Emails.length > 0 ? key.Emails[0].Email : key.KeyID}
subtitle={"Created At: " + key.CreatedAt}
>
<Table
row={FlattenObject(key)}
/>
</Accordion>
{/each}
</ul>
</div>
{/if}
{#if githubResult.UsernameResult.DeepScan}
{#if githubResult.UsernameResult.DeepScan.Authors && githubResult.UsernameResult.DeepScan.Authors.length > 0}
<div class="mt-4">
<h4 class="h4 mb-2">Deep scan authors</h4>
<ul class="list bg-base-100 rounded-box shadow-md">
<Accordion
icon={UserRoundPen}
title="Authors"
subtitle={"Found " + githubResult.UsernameResult.DeepScan.Authors.length + " authors"
}
>
<Table
row={githubResult.UsernameResult.DeepScan.Authors}
/>
</Accordion>
</ul>
</div>
{/if}
{#if githubResult.UsernameResult.DeepScan.Emails && githubResult.UsernameResult.DeepScan.Emails.length > 0}
<div class="mt-4">
<h4 class="h4 mb-2">Deep scan emails</h4>
<ul class="list bg-base-100 rounded-box shadow-md">
<Accordion
icon={Mail}
title="Emails"
subtitle={"Found " + githubResult.UsernameResult.DeepScan.Emails.length + " emails"
}
>
<Table
row={githubResult.UsernameResult.DeepScan.Emails}
/>
</Accordion>
</ul>
</div>
{/if}
{#if githubResult.UsernameResult.DeepScan.Secrets && githubResult.UsernameResult.DeepScan.Secrets.length > 0}
{@const flattenedSecrets = githubResult.UsernameResult.DeepScan.Secrets.map(FlattenObject)}
<div class="mt-4">
<h4 class="h4 mb-2">Deep scan secrets</h4>
<ul class="list bg-base-100 rounded-box shadow-md">
<Accordion
icon={Mail}
title="Secrets"
subtitle={"Found " + githubResult.UsernameResult.DeepScan.Secrets.length + " secrets"
}
>
<Table
row={flattenedSecrets}
/>
</Accordion>
</ul>
</div>
{/if}
{/if}
</div>
{:else if githubResult.EmailResult}
<div class="w-full">
{#if githubResult.EmailResult.Spoofing}
<h4 class="h4 mb-4">From spoofing</h4>
<div class="flex flex-wrap gap-5">
<div class="avatar">
<div class="w-24 h-24 rounded-xl">
<img
src={githubResult.EmailResult.Spoofing.AvatarURL}
alt="Avatar of {githubResult.EmailResult.Spoofing.Username}"
/>
</div>
</div>
<div class="flex flex-col gap-2">
<div class="flex flex-col gap-2">
<h4 class="h4">@{githubResult.EmailResult.Spoofing.Username}</h4>
{#if githubResult.EmailResult.Spoofing.Name}
<p>
<strong>Name:</strong>
{githubResult.EmailResult.Spoofing.Name}
</p>
{/if}
{#if githubResult.EmailResult.Spoofing.Email}
<p>
<strong>Public email:</strong>
{githubResult.EmailResult.Spoofing.Email}
</p>
{/if}
{#if githubResult.EmailResult.Target}
<p class="break-all">
<strong>Primary email:</strong>
{githubResult.EmailResult.Target}
</p>
{/if}
<a
href={githubResult.EmailResult.Spoofing.Url}
class="link link-primary flex gap-2 items-center"
target="_blank"
>
{githubResult.EmailResult.Spoofing.Url}
<ExternalLink size={12} />
</a>
</div>
</div>
</div>
{/if}
{#if githubResult.EmailResult.Commits}
<div class="mt-4">
<h4 class="h4 mb-2">Commits</h4>
<ul class="list bg-base-100 rounded-box shadow-md">
{#each githubResult.EmailResult.Commits as commit}
<Accordion
icon={GitCommitVertical}
title={commit.Username && commit.Username !== ""
? commit.Name + " (@" + commit.Username + ")"
: commit.Name}
subtitle={"Occurrences: " + commit.Occurrences}
>
<Table
row={{
name: commit.Name,
username: commit.Username,
email: commit.Email,
first_found_in: commit.FirstFoundIn,
occurrences: commit.Occurrences,
}}
/>
</Accordion>
{/each}
</ul>
</div>
{/if}
</div>
{/if}

View File

@@ -0,0 +1,100 @@
<script lang="ts">
import Table from "$src/lib/components/table.svelte";
import { ChevronDown, ChevronUp, Database, Key, Mail } from "@lucide/svelte";
const { row }: { row: Record<string, string> } = $props();
let isOpen = $state<boolean>(false);
function getDomain(dataleakName: string) {
const firstPart = dataleakName.split(" ")[0].toLowerCase();
const domainRegex =
/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/;
if (domainRegex.test(firstPart)) {
return firstPart;
}
return null;
}
function getHighlightedContent(row: Record<string, string>): string {
const prioritizedKeys = [
"email",
"username",
"full_name",
"first_name",
"last_name",
"phone",
"password",
"address",
];
for (const key of prioritizedKeys) {
if (row[key]) {
return row[key];
}
}
for (const key in row) {
if (row[key]) {
return row[key];
}
}
return "No content";
}
</script>
<button
class="list-row hover:bg-base-300/75 text-left"
class:bg-base-300={isOpen}
class:rounded-b-none={isOpen}
onclick={() => {
isOpen = !isOpen;
}}
>
<div>
{#if getDomain(row["source"])}
<img
src="https://icons.duckduckgo.com/ip3/{getDomain(row['source'])}.ico"
class="size-10 rounded-box bg-neutral"
alt="Favicon de {getDomain(row['source'])}"
/>
{:else if row["password"] !== null}
<div
class="size-10 rounded-box bg-neutral items-center justify-center flex"
>
<Key />
</div>
{:else if row["email"] !== null}
<div
class="size-10 rounded-box bg-neutral items-center justify-center flex"
>
<Mail />
</div>
{:else}
<div
class="size-10 rounded-box bg-neutral items-center justify-center flex"
>
<Database />
</div>
{/if}
</div>
<div>
<div>{getHighlightedContent(row)}</div>
<div class="text-xs uppercase font-semibold opacity-60">
{row["source"]}
</div>
</div>
<div class="btn btn-square btn-ghost">
{#if isOpen}
<ChevronUp size={12} />
{:else}
<ChevronDown size={12} />
{/if}
</div>
</button>
{#if isOpen}
<li class="list-row flex bg-base-200 rounded-t-none mb-2">
<Table {row} />
</li>
{/if}

View File

@@ -0,0 +1,72 @@
<script lang="ts">
import type { Result } from "$src/lib/types";
import Row from "./row.svelte";
const { result }: { result: Result } = $props();
let page = $state(1);
let totalPages = $state(0);
const perPage = 20;
let paginated = $state<Record<string, string>[]>([]);
$effect(() => {
if (result && result.LeakResult.Rows) {
totalPages = Math.ceil(result.LeakResult.Rows.length / perPage);
const start = (page - 1) * perPage;
const end = start + perPage;
paginated = result.LeakResult.Rows.slice(start, end);
if (page > totalPages) {
page = totalPages > 0 ? totalPages : 1;
}
}
});
function goToFirstPage() {
page = 1;
top.scrollIntoView();
}
function previousPage() {
if (page > 1) {
page--;
top.scrollIntoView();
}
}
function nextPage() {
if (page < totalPages) {
page++;
top.scrollIntoView();
}
}
let top: any = $state();
</script>
<div bind:this={top} class="absolute -mt-[100px]"></div>
{#if result}
<ul class="list bg-base-100 rounded-box shadow-md">
{#each paginated as row (row)}
<Row {row} />
{/each}
</ul>
{#if totalPages > 1}
<div class="join m-auto mt-5">
<button class="join-item btn" onclick={previousPage} disabled={page === 1}
>«</button
>
<button class="join-item btn" onclick={goToFirstPage}
>Page {page} / {totalPages}</button
>
<button
class="join-item btn"
onclick={nextPage}
disabled={page === totalPages}>»</button
>
</div>
{/if}
{:else}
No result
{/if}

View File

@@ -0,0 +1,54 @@
<script lang="ts">
import type { Result } from "$src/lib/types";
import { formatDate } from "$src/lib/utils";
import { BadgeInfo, Clock, File } from "@lucide/svelte";
const { result }: { result: Result } = $props();
let nresult = $state(0);
$effect(() => {
const r = [
result.LeakResult.Rows?.length | 0,
result.GithubResult.EmailResult?.Commits?.length | 0,
result.GithubResult.EmailResult?.Spoofing ? 1 : 0,
result.GithubResult.UsernameResult?.Commits?.length | 0,
];
nresult = r.reduce((a, b) => a + b, 0);
});
</script>
<div class="stats stats-vertical md:stats-horizontal">
<div class="stat">
<div class="stat-figure text-secondary">
<File />
</div>
<div class="stat-title">Results</div>
<div class="stat-value" class:animate-pulse={result.Status === "pending"}>
{nresult.toLocaleString("fr")}
{#if result.Status === "pending"}
<span class="loading loading-dots loading-xs ml-2"></span>
{/if}
</div>
</div>
<div class="stat">
<div class="stat-figure text-secondary">
<Clock />
</div>
<div class="stat-title">Date</div>
<div class="stat-value">
{formatDate(result.Date)}
</div>
</div>
<div class="stat">
<div class="stat-figure text-secondary">
<BadgeInfo />
</div>
<div class="stat-title">Status</div>
<div class="stat-value" class:animate-pulse={result.Status === "pending"}>
{result.Status}
{#if result.Status === "pending"}
<span class="loading loading-dots loading-xs ml-2"></span>
{/if}
</div>
</div>
</div>

View File

@@ -0,0 +1,104 @@
<script lang="ts">
import { serverPassword, serverUrl } from "$src/lib/stores/server";
import { cn } from "$src/lib/utils";
import { Equal, EqualNot, Search } from "@lucide/svelte";
import axios from "axios";
import { navigate } from "sv-router/generated";
import { toast } from "svelte-sonner";
const {
initialQuery = "",
initialFilter = "all",
initialExactMatch = false,
}: {
initialQuery?: string;
initialFilter?: string;
initialExactMatch?: boolean;
} = $props();
let filters = [
"all",
"username",
"email",
"name",
"phone",
"url",
"password",
"password hash",
"full_text",
];
let activeFilter = $state<string>(initialFilter);
let query = $state<string>(initialQuery);
let exactMatch = $state<boolean>(initialExactMatch);
function NewSearch() {
axios
.post(
`${$serverUrl}/search`,
{ Text: query, Column: activeFilter, ExactMatch: exactMatch },
{
headers: {
"Content-Type": "application/json",
"X-Password": $serverPassword,
},
},
)
.then((r) => {
const id = r.data.Id;
window.location.href = `/search/${id}`;
})
.catch((e) => {
if (e.response.data.Error !== undefined) {
toast.error(e.response.data.Error);
} else {
toast.error("An error occurred");
}
});
}
</script>
<div class="flex gap-5 flex-col">
<div
class="flex gap-3 justify-start items-center w-full overflow-y-hidden overflow-x-auto"
>
{#each filters as filter}
<button
class={cn(
"btn btn-md capitalize",
activeFilter === filter
? "btn-primary"
: "btn-ghost btn-neutral text-base-content/80 hover:text-neutral-content",
)}
onclick={() => (activeFilter = filter)}>{filter.replace("_", " ")}</button
>
{/each}
</div>
<form
class="join w-full"
onsubmit={(e) => {
e.preventDefault();
NewSearch();
}}
>
<label class="grow input input-xl input-primary join-item w-full">
<Search size={16} />
<input
class="grow input-xl"
type="text"
bind:value={query}
placeholder="Search..."
required
/>
<div class="tooltip" data-tip="Exact Match">
<label class="toggle text-base-content toggle-xs">
<input type="checkbox" bind:checked={exactMatch} />
<EqualNot aria-label="disable" size={12} />
<Equal aria-label="enabled" size={12} />
</label>
</div>
</label>
<button class="btn btn-primary btn-xl join-item">Search</button>
</form>
</div>

View File

@@ -0,0 +1,83 @@
<script lang="ts">
import type { Server } from "$src/lib/types";
let { serverInfo }: { serverInfo: Server } = $props();
</script>
<div class="my-4">
<div class="overflow-x-auto">
<table class="table">
<!-- head -->
<thead>
<tr>
<th>Service</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr class="hover:bg-base-300">
<th> Data wells lookup </th>
<td>
{#if serverInfo.Dataleaks.length !== 0}
<div class="inline-grid *:[grid-area:1/1] mr-2">
<div class="status status-success"></div>
<div class="status status-success"></div>
</div>
Active
{:else}
<div class="inline-grid *:[grid-area:1/1] mr-2">
<div class="status status-error animate-ping"></div>
<div class="status status-error"></div>
</div>
Inactive
{/if}
</td>
</tr>
<tr class="hover:bg-base-300">
<th class="flex flex-wrap gap-2 items-center">
Github recon
{#if serverInfo.Settings.GithubTokenLoaded === true}
<div class="badge badge-xs badge-neutral">Token</div>
{/if}
{#if serverInfo.Settings.GithubDeepMode === true}
<div class="badge badge-xs badge-neutral">Deep Mode</div>
{/if}
</th>
<td>
{#if serverInfo.Settings.GithubRecon === true}
<div class="inline-grid *:[grid-area:1/1] mr-2">
<div class="status status-success"></div>
<div class="status status-success"></div>
</div>
Active
{:else}
<div class="inline-grid *:[grid-area:1/1] mr-2">
<div class="status status-error animate-ping"></div>
<div class="status status-error"></div>
</div>
Inactive
{/if}
</td>
</tr>
<tr class="hover:bg-base-300">
<th>Google hunt</th>
<td>
{#if serverInfo.Settings.GithubRecon === true}
<div class="inline-grid *:[grid-area:1/1] mr-2">
<div class="status status-success"></div>
<div class="status status-success"></div>
</div>
Active
{:else}
<div class="inline-grid *:[grid-area:1/1] mr-2">
<div class="status status-error animate-ping"></div>
<div class="status status-error"></div>
</div>
Inactive
{/if}
</td>
</tr>
</tbody>
</table>
</div>
</div>

View File

@@ -0,0 +1,46 @@
<script lang="ts">
import type { Server } from "$src/lib/types";
import { Database, File, Save } from "@lucide/svelte";
const { serverInfo }: { serverInfo: Server | null } = $props();
function mbToGb(mb: number): number {
return Math.round((mb / 1024) * 100) / 100;
}
</script>
<div class="stats stats-vertical md:stats-horizontal">
<div class="stat">
<div class="stat-figure text-secondary">
<File />
</div>
<div class="stat-title">Rows available</div>
<div class="stat-value">
{serverInfo?.TotalRows
? serverInfo.TotalRows.toLocaleString("fr")
: "-- --- --- ---"}
</div>
</div>
<div class="stat">
<div class="stat-figure text-secondary">
<Database />
</div>
<div class="stat-title">Data wells available</div>
<div class="stat-value">
{serverInfo?.TotalDataleaks
? serverInfo.TotalDataleaks.toLocaleString("fr")
: "---"}
</div>
</div>
<div class="stat">
<div class="stat-figure text-secondary">
<Save />
</div>
<div class="stat-title">Storage used</div>
<div class="stat-value">
{serverInfo?.TotalSize
? mbToGb(serverInfo.TotalSize).toLocaleString("fr") + " Gb"
: "--- Gb"}
</div>
</div>
</div>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import { cn } from "$lib/utils";
const { class: className = "", size = 25 } = $props();
</script>
<svg
width={size}
viewBox="0 0 141 205"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class={cn("fill-primary", className)}
>
<path
d="M69.7444 0C84.49 25.1637 100.559 49.4708 117.95 72.9219C126.451 85.0149 133.113 98.1035 137.934 112.188C144.168 135.735 140.195 157.355 126.014 177.046C109.938 196.591 89.1945 205.53 63.7844 203.865C36.717 200.867 17.4935 187.019 6.11353 162.321C-1.9191 142.522 -2.03583 122.655 5.76294 102.722C9.71019 93.7604 14.3849 85.2295 19.7864 77.1289C35.0593 56.0531 49.5504 34.4338 63.259 12.2705C65.6086 8.27249 67.7699 4.18207 69.7444 0ZM100.596 81.3359C102.957 92.649 102.198 103.751 98.3176 114.642C93.9276 124.99 87.7338 134.105 79.7366 141.987C77.6951 144.434 75.8254 147.005 74.1272 149.7C70.5033 155.43 68.5745 161.682 68.342 168.456C68.1692 175.079 70.6236 180.455 75.7043 184.583C89.1062 183.345 100.267 177.678 109.186 167.58C123.101 149.518 125.672 129.885 116.899 108.682C112.25 99.0223 106.815 89.9071 100.596 81.3359ZM70.095 36.4609C59.5617 53.4793 48.4018 70.0738 36.6145 86.2441C30.2619 94.8335 25.2366 104.183 21.5393 114.291C15.2642 135.159 19.2377 153.74 33.4592 170.034C39.7381 176.533 47.2756 180.798 56.0715 182.83C55.7506 182.699 55.4583 182.524 55.1956 182.305C40.6021 163.32 39.6671 143.687 52.3909 123.406C58.721 114.622 64.9149 105.74 70.9719 96.7617C72.559 94.0549 73.9614 91.2501 75.179 88.3477C81.8825 70.1929 80.1876 52.8971 70.095 36.4609Z"
/>
</svg>

View File

@@ -0,0 +1,144 @@
<script lang="ts">
import { Key, Link, RefreshCw, Server } from "@lucide/svelte";
import { cn } from "../utils";
import { serverUrl, serverPassword } from "$lib/stores/server";
import { toast } from "svelte-sonner";
import axios from "axios";
let { text = "", class: className = "" } = $props();
let isModalOpen = $state(false);
let needToTest = $state(true);
let url = $state($serverUrl || "https://");
let password = $state($serverPassword);
let working = $state<boolean | null>(null);
function save() {
isModalOpen = false;
$serverUrl = url;
$serverPassword = password;
toast.success("Server settings saved!");
}
function test() {
axios
.get(`${url}/`)
.then(() => {
toast.success("Server is working!");
needToTest = false;
working = true;
})
.catch(() => {
toast.error("Server is not working!");
needToTest = true;
working = false;
});
}
function reset() {
$serverUrl = "";
$serverPassword = "";
url = "https://";
password = "";
needToTest = true;
working = null;
}
$effect(() => {
if (isModalOpen) {
url = $serverUrl || "https://";
needToTest = true;
working = null;
}
});
</script>
<div class="indicator">
<span class="indicator-item">
<div class="inline-grid *:[grid-area:1/1]">
{#if $serverUrl !== ""}
<div class="status status-success"></div>
{:else}
<div class="status status-error animate-ping"></div>
<div class="status status-error"></div>
{/if}
</div>
</span>
<button
onclick={() => {
isModalOpen = !isModalOpen;
}}
class={cn(className, "btn btn-ghost btn-primary")}
>
<Server size={16} />
{text}
</button>
</div>
<dialog
class="modal modal-bottom sm:modal-middle"
class:modal-open={isModalOpen}
>
<div class="modal-box">
<form method="dialog">
<button
onclick={() => (isModalOpen = false)}
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button
>
</form>
<div class="flex flex-col gap-5">
<div>
<h2 class="card-title">Connect to your server</h2>
<p>
You can connect to your own Eleakxir server by providing the server
URL and an optional password.
</p>
</div>
<label
class="input w-full"
class:input-error={working === false}
class:input-success={working === true}
>
<Link size={16} />
<input
class="grow"
type="url"
required
placeholder="https://"
bind:value={url}
/>
<button class="btn btn-xs btn-square btn-ghost" onclick={reset}
><RefreshCw size={8} /></button
>
</label>
<label class="input w-full">
<Key />
<input
type="password"
class="grow"
placeholder="Password"
bind:value={password}
/>
<span class="badge badge-neutral badge-xs">Optional</span>
</label>
<div class="card-actions flex gap-2">
<button onclick={test} class="btn btn-primary btn-outline">Test</button>
<button
onclick={save}
disabled={needToTest}
class="btn btn-primary grow">Save</button
>
</div>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button onclick={() => (isModalOpen = false)}>close</button>
</form>
</dialog>

View File

@@ -0,0 +1,79 @@
<script lang="ts">
import { ExternalLink } from "@lucide/svelte";
const {
row,
}: { row: Record<string, string> | Array<Record<string, string>> } = $props();
</script>
<div class="overflow-x-auto">
<table class="table">
{#if Array.isArray(row) && row.length !== 0}
{@const head = Object.entries(row[0])}
<!-- head -->
<thead>
<tr>
{#each head as [key, _]}
<th
class="text-xs whitespace-nowrap font-semibold opacity-60 capitalize"
>
{key}
</th>
{/each}
</tr>
</thead>
<tbody>
{#each row as item}
<tr>
{#each Object.entries(item) as [key, value]}
<th class="text-xs whitespace-nowrap font-semibold opacity-60">
{#if key.toLowerCase() === "url" && value !== "" && value !== null}
<a
href={value}
target="_blank"
rel="noopener noreferrer"
class="link link-primary gap-2 items-center flex"
>
{value}
<ExternalLink size={12} />
</a>
{:else}
{value}
{/if}
</th>
{/each}
</tr>
{/each}
</tbody>
{:else}
<tbody>
{#each Object.entries(row) as [key, value]}
{#if key !== "source" && value !== "" && value !== null}
<tr class="">
<th
class="text-xs whitespace-nowrap font-semibold opacity-60 capitalize"
>{key.replace(/_/g, " ")}</th
>
<td class="w-fit overflow-x-auto whitespace-nowrap">
{#if key.toLowerCase() === "url"}
<a
href={value}
target="_blank"
rel="noopener noreferrer"
class="link link-primary gap-2 items-center flex"
>
{value}
<ExternalLink size={12} />
</a>
{:else}
{value}
{/if}
</td>
</tr>
{/if}
{/each}
</tbody>
{/if}
</table>
</div>

View File

@@ -0,0 +1,30 @@
<script lang="ts">
import { isActiveLink } from 'sv-router';
let { item } = $props();
import Self from './sidebar-menu-item.svelte';
</script>
<li>
{#if item.items}
<details open>
<summary class="flex gap-2 items-center">
{#if item.icon}
<item.icon size={16} />
{/if}
{item.title}
</summary>
<ul>
{#each item.items as subitem}
<Self item={subitem} />
{/each}
</ul>
</details>
{:else}
<a href={item.url} class="flex gap-2 items-center" use:isActiveLink={{ className: 'menu-active' }}>
{#if item.icon}
<item.icon size={16} />
{/if}
{item.title}
</a>
{/if}
</li>

View File

@@ -0,0 +1,57 @@
<script lang="ts">
import { BrushCleaning, Database, FileSearch, Home, Scale, Search } from "@lucide/svelte";
import SidebarMenuItem from "./sidebar-menu-item.svelte";
interface NavItem {
title?: string;
url?: string;
items?: NavItem[];
icon?: any;
type?: "link" | "parent" | "divider";
}
const Nav: NavItem[] = [
{
title: "Home",
url: "/",
icon: Home,
},
{
title: "Search",
url: "/search",
icon: Search,
},
{
title: "Data wells",
url: "/dataleaks",
icon: Database,
},
{ type: "divider" },
{
title: "Parquet files & Rules",
url: "/parquet",
icon: Scale,
},
{
title: "Leak utils",
url: "/leak-utils",
icon: BrushCleaning,
},
];
</script>
<div class="drawer-side z-[101]">
<label for="menu-toggle" aria-label="close sidebar" class="drawer-overlay"
></label>
<ul class="menu bg-base-200 text-base-content min-h-full w-56 p-4 gap-2">
{#each Nav as item}
{#if item.type === "divider"}
<li class="menu-title pt-2">
<div class="divider"></div>
</li>
{:else}
<SidebarMenuItem {item} />
{/if}
{/each}
</ul>
</div>

View File

@@ -0,0 +1,47 @@
<script lang="ts">
import { Github, Menu, Search } from "@lucide/svelte";
import Logo from "../components/logo.svelte";
import DarkModeToggle from "../components/dark-mode-toggle.svelte";
import { cn } from "../utils";
import ServerDialog from "../components/server-dialog.svelte";
let y = $state(0);
</script>
<svelte:window bind:scrollY={y} />
<nav
class={cn(
"w-full h-20 flex gap-5 items-center justify-between px-6 fixed transition-colors duration-1000 z-[100]",
y === 0 || "bg-base-200",
)}
>
<div class="flex gap-2 items-center">
<label
for="menu-toggle"
class="btn btn-ghost btn-sm btn-square drawer-button"
>
<Menu size={16} />
</label>
<a href="/" class="flex gap-2 items-center">
<Logo size={16} />
<p>Eleakxir</p>
</a>
</div>
<div class="flex gap-2 items-center">
<a href="/search">
<button class="btn btn-sm btn-ghost btn-square">
<Search size={16} />
</button>
</a>
<a href="https://github.com/anotherhadi/eleakxir">
<button class="btn btn-sm btn-ghost btn-square">
<Github size={16} />
</button>
</a>
<DarkModeToggle />
<ServerDialog class="btn-sm btn-square" />
</div>
</nav>

View File

@@ -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", "");

78
front/src/lib/types.ts Normal file
View File

@@ -0,0 +1,78 @@
type Query = {
Text: string;
Column: string;
ExactMatch: boolean;
};
type LeakResult = {
Duration: number;
Error: string;
Rows: Array<Record<string, string>>;
};
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,
};

80
front/src/lib/utils.ts Normal file
View File

@@ -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;
}

5
front/src/main.ts Normal file
View File

@@ -0,0 +1,5 @@
import { mount } from "svelte";
import App from "./App.svelte";
import "sv-router/generated";
mount(App, { target: document.querySelector("#app")! });

View File

@@ -0,0 +1,57 @@
<script lang="ts">
import Datawells from "$src/lib/components/index/search/datawells.svelte";
import Stats from "$src/lib/components/index/search/stats.svelte";
import { serverPassword, serverUrl } from "$src/lib/stores/server";
import type { Server } from "$src/lib/types";
import axios from "axios";
import { navigate } from "sv-router/generated";
import { onMount } from "svelte";
import { toast } from "svelte-sonner";
let serverInfo = $state<Server | null>(null);
onMount(() => {
if ($serverUrl === "") {
toast.error("Please, configure your server first!");
navigate("/");
return;
}
axios
.get(`${$serverUrl}/`, {
headers: {
"X-Password": $serverPassword,
},
})
.then((r) => {
serverInfo = r.data;
console.log(serverInfo);
})
.catch((e) => {
toast.error(
"Failed to fetch server info. Please, change your server configuration!",
);
console.log(e);
navigate("/");
});
});
</script>
<main>
<header class="flex flex-col gap-2 mb-8">
<h1 class="h1"><span class="text-2xl align-middle">🗃️</span> Data wells</h1>
<p>List of data wells (databases) available on the connected server.</p>
</header>
{#if serverInfo}
<div class="card card-border border-neutral shadow col-span-full mb-5">
<Stats {serverInfo} />
</div>
<Datawells
dataleaks={serverInfo.Dataleaks}
showColumns={true}
perPage={20}
/>
{:else}
<p>Loading...</p>
{/if}
</main>

View File

@@ -0,0 +1,137 @@
<script lang="ts">
import AnimatedBeamMultiple from "$src/lib/components/index/AnimatedBeamMultiple.svelte";
import Logo from "$src/lib/components/logo.svelte";
import ServerDialog from "$src/lib/components/server-dialog.svelte";
import { serverUrl } from "$src/lib/stores/server";
import { ArrowRight, Github, Search } from "@lucide/svelte";
import { onMount } from "svelte";
let open = $state(false);
onMount(() => {
open = true;
});
</script>
<header
class="min-h-[80vh] relative flex justify-center items-center px-6 py-10"
>
<div
class="absolute top-0 left-0 z-[-10] w-full h-full bg-top transition-opacity duration-[2000ms]"
class:opacity-0={!open}
class:opacity-100={open}
style="
background-image:
linear-gradient(to bottom, rgba(255,255,255,0) 50%, var(--color-base-100) 100%),
url('https://lovable.dev/img/background/gradient-optimized.svg');
"
></div>
<div class="mx-auto max-w-3xl flex gap-8 flex-col">
<a href="https://github.com/anotherhadi/eleakxir" target="_blank">
<span class="badge badge-lg hover:opacity-90"
>✨ Check the Github repo <ArrowRight size={16} /></span
>
</a>
<div class="flex gap-6 items-center">
<Logo size={46} class="fill-primary" />
<h1 class="font-bold text-7xl">Eleakxir</h1>
</div>
<p>
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.
</p>
<div class="flex gap-6 items-center">
<a href="/search">
<button class="btn btn-primary">
<Search size={16} />
Let's search</button
>
</a>
<ServerDialog text="Connect to my server" />
</div>
</div>
</header>
<main class="flex flex-col gap-24 max-w-7xl m-auto mt-10">
<div class="card card-dash bg-base-300">
<div class="card-body flex flex-col gap-10 lg:flex-row">
<div class="flex gap-5 flex-col">
<h2 class="card-title text-3xl">⚙️ How Eleakxir works?</h2>
<p>
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
<br />
<br />
And it's open source!
</p>
<div class="flex items-center gap-2">
{#if $serverUrl === "https://" || $serverUrl === ""}
<ServerDialog
text="Connect your server"
class="grow btn-outline btn btn-accent btn-sm"
/>
{/if}
<a href="https://github.com/anotherhadi/eleakxir">
<button class="btn btn-outline btn-sm hover:bg-base-200 grow"
><Github size={16} /> Check the Github repo</button
>
</a>
</div>
</div>
<AnimatedBeamMultiple />
</div>
</div>
<div>
<h2 class="text-3xl font-bold text-center mb-10">🚀 Features</h2>
<div
class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 justify-center m-auto gap-5"
>
{#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}
<div class="card bg-base-200 shadow-sm">
<div class="card-body">
<h2 class="card-title">{value.title}</h2>
<p>
{value.content}
</p>
</div>
</div>
{/each}
</div>
</div>
<div>
<h2 class="text-3xl font-bold text-center mb-10">🐢 Speed</h2>
<p class="max-w-2xl m-auto">
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, Im 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 its actually
possible to run such searches at home — something that would be completely
out of reach if I had to maintain Elasticsearchs much larger index
footprint.
</p>
</div>
<div>
<h2 class="text-3xl font-bold text-center mb-10">🚨 Disclaimer</h2>
<p class="max-w-lg m-auto">
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.
</p>
</div>
</main>
<div class="pb-24"></div>

View File

@@ -0,0 +1,28 @@
<script lang="ts">
import Sidebar from "$src/lib/navigation/sidebar.svelte";
import Topbar from "$src/lib/navigation/topbar.svelte";
import { onMount, type Snippet } from "svelte";
import { Toaster } from 'svelte-sonner'
import { themeChange } from "theme-change";
let { children }: { children: Snippet } = $props();
onMount(() => {
themeChange(false);
});
</script>
<Toaster
toastOptions={{
class: '!bg-base-300 !text-base-content !border-base-200',
}}
/>
<div class="drawer min-h-svh">
<input id="menu-toggle" type="checkbox" class="drawer-toggle" />
<div class="drawer-content">
<Topbar />
<div class="mt-20"></div>
{@render children()}
</div>
<Sidebar />
</div>

View File

@@ -0,0 +1,18 @@
<script lang="ts">
import axios from "axios";
import { marked } from "marked";
import { onMount } from "svelte";
const url = "https://raw.githubusercontent.com/anotherhadi/eleakxir-temp/refs/heads/main/leak-utils/README.md"
let text = $state<string>("");
onMount(() => {
axios.get(url).then((r) => {
text = r.data;
});
});
</script>
<main class="prose max-w-7xl pt-16 pb-28">
{@html marked(text)}
</main>

View File

@@ -0,0 +1,18 @@
<script lang="ts">
import axios from "axios";
import { marked } from "marked";
import { onMount } from "svelte";
const url = "https://raw.githubusercontent.com/anotherhadi/eleakxir-temp/refs/heads/main/leak-utils/DATALEAKS-NORMALIZATION.md"
let text = $state<string>("");
onMount(() => {
axios.get(url).then((r) => {
text = r.data;
});
});
</script>
<main class="prose max-w-7xl pt-16 pb-28">
{@html marked(text)}
</main>

View File

@@ -0,0 +1,226 @@
<script lang="ts">
import type { Result } from "$src/lib/types";
import axios from "axios";
import { navigate, route } from "sv-router/generated";
import { serverPassword, serverUrl } from "$src/lib/stores/server";
import Searchbar from "$src/lib/components/index/search/searchbar.svelte";
import { toast } from "svelte-sonner";
import { onMount } from "svelte";
import Stats from "$src/lib/components/index/search/id/stats.svelte";
import Rows from "$src/lib/components/index/search/id/rows.svelte";
import {
ChevronDown,
CircleAlert,
CircleCheck,
CircleMinus,
CircleX,
Database,
Github,
} from "@lucide/svelte";
import { convertNanoSeconds } from "$src/lib/utils";
import GithubResult from "$src/lib/components/index/search/id/githubResult.svelte";
route.getParams("/search/:id");
let { id } = route.params;
let result = $state<Result | null>(null);
function loadData() {
if (id === undefined) {
return;
}
if (id === "") {
return;
}
axios
.get(`${$serverUrl}/search/${id}`, {
headers: {
"X-Password": $serverPassword,
},
})
.then((r) => {
result = r.data;
console.log(r.data);
if (result && result.Status !== "pending") {
clearInterval(intervalId);
}
})
.catch((e) => {
toast.error("Failed to fetch search result!");
clearInterval(intervalId);
navigate("/search");
});
}
let intervalId: ReturnType<typeof setInterval>;
let elapsedTime = 0;
let pollingInterval = 10000; // Start with a 10-second interval
onMount(() => {
if ($serverUrl === "") {
toast.error("Please, configure your server first!");
navigate("/");
return;
}
loadData();
intervalId = setInterval(() => {
elapsedTime += pollingInterval;
// Check for status change inside the interval
if (result && result.Status !== "pending") {
clearInterval(intervalId);
return;
}
// Change polling frequency based on elapsed time
if (elapsedTime >= 120000 && pollingInterval !== 10000) {
clearInterval(intervalId);
pollingInterval = 15000;
intervalId = setInterval(loadData, pollingInterval);
return;
} else if (elapsedTime >= 600000 && pollingInterval !== 30000) {
clearInterval(intervalId);
pollingInterval = 30000;
intervalId = setInterval(loadData, pollingInterval);
return;
}
loadData();
}, pollingInterval);
return () => {
clearInterval(intervalId);
};
});
</script>
<main>
{#if result}
<header class="flex gap-5 flex-col">
<a href="/search">
<h1 class="h1"><span class="text-2xl align-middle">🔍</span> Search</h1>
</a>
<Searchbar
initialQuery={result.Query.Text}
initialFilter={result.Query.Column}
initialExactMatch={result.Query.ExactMatch}
/>
</header>
<div class="my-10"></div>
<div class="grid grid-cols-1 gap-5 [&>div]:border-neutral">
<div class="card card-border shadow col-span-full">
<Stats {result} />
</div>
<div class="collapse collapse-arrow bg-base-100 border">
<input type="radio" name="my-accordion-2" checked={true} />
<div
class="collapse-title font-semibold text-xl flex justify-between items-center"
>
<div class="flex items-center gap-2">
<Database size={18} class="text-base-content/60" />
Data wells lookup
</div>
{#if result.LeakResult.Error !== ""}
<CircleX size={16} class="text-error" />
{:else if result.LeakResult.Duration === 0}
<span class="loading loading-dots loading-xs"></span>
{:else if result.LeakResult.Rows.length > 0}
<CircleCheck size={16} class="text-success" />
{:else}
<CircleMinus size={16} class="text-base-content/60" />
{/if}
</div>
<div class="collapse-content">
{#if result.LeakResult.Error !== ""}
<div role="alert" class="alert alert-soft alert-error">
<CircleAlert size={20} />
<span>Error! {result.LeakResult.Error}</span>
</div>
{:else if result.LeakResult.Duration === 0}
<ul class="list rounded-box">
{#each Array(5) as _}
<div class="list-row text-left">
<div>
<div
class="skeleton size-10 rounded-box items-center justify-center flex"
></div>
</div>
<div>
<div class="skeleton h-5 mb-1 w-52"></div>
<div
class="text-xs skeleton h-4 w-34 uppercase font-semibold opacity-60"
></div>
</div>
<div class="btn btn-square btn-ghost">
<ChevronDown size={12} />
</div>
</div>
{/each}
</ul>
{:else}
<p class="text-base-content/60">
{result.LeakResult.Rows.length} results in {convertNanoSeconds(
result.LeakResult.Duration,
)}
</p>
<Rows {result} />
{/if}
</div>
</div>
<div class="collapse collapse-arrow bg-base-100 border">
<input type="radio" name="my-accordion-2" />
<div
class="collapse-title font-semibold text-xl flex justify-between items-center"
>
<div class="flex items-center gap-2">
<Github size={18} class="text-base-content/60" />
Github Recon
</div>
{#if result.GithubResult.Error !== ""}
<CircleX size={16} class="text-error" />
{:else if result.GithubResult.Duration === 0}
<span class="loading loading-dots loading-xs"></span>
{:else if !result.GithubResult.EmailResult?.Commits && !result.GithubResult.EmailResult?.Spoofing && !result.GithubResult.UsernameResult?.User}
<CircleMinus size={16} class="text-base-content/60" />
{:else if result.GithubResult.UsernameResult || result.GithubResult.EmailResult}
<CircleCheck size={16} class="text-success" />
{/if}
</div>
<div class="collapse-content">
{#if result.GithubResult.Error !== ""}
<div role="alert" class="alert alert-soft alert-error">
<CircleAlert size={20} />
<span>Error! {result.GithubResult.Error}</span>
</div>
{:else if result.GithubResult.Duration === 0}
<div role="alert" class="alert alert-soft">
<span class="loading loading-dots loading-sm"></span>
<span>Loading...</span>
</div>
{:else if !result.GithubResult.EmailResult?.Commits && !result.GithubResult.EmailResult?.Spoofing && !result.GithubResult.UsernameResult?.User}
<div role="alert" class="alert alert-soft">
<CircleMinus size={20} />
<span>No result</span>
</div>
{:else}
<p class="text-base-content/60 mb-4">
Found a result in {convertNanoSeconds(
result.GithubResult.Duration,
)}
</p>
<GithubResult githubResult={result.GithubResult} />
{/if}
</div>
</div>
</div>
{/if}
<div class="mb-10"></div>
</main>

View File

@@ -0,0 +1,94 @@
<script lang="ts">
import Datawells from "$src/lib/components/index/search/datawells.svelte";
import History from "$src/lib/components/index/search/history.svelte";
import HowToSearch from "$src/lib/components/index/search/howToSearch.svelte";
import Searchbar from "$src/lib/components/index/search/searchbar.svelte";
import Services from "$src/lib/components/index/search/services.svelte";
import Stats from "$src/lib/components/index/search/stats.svelte";
import { serverPassword, serverUrl } from "$src/lib/stores/server";
import type { Server, History as HistoryT } from "$src/lib/types";
import axios from "axios";
import { navigate } from "sv-router/generated";
import { onMount } from "svelte";
import { toast } from "svelte-sonner";
let serverInfo = $state<Server | null>(null);
let history = $state<HistoryT>([]);
onMount(() => {
if ($serverUrl === "") {
toast.error("Please, configure your server first!");
navigate("/");
return;
}
axios
.get(`${$serverUrl}/`, {
headers: {
"X-Password": $serverPassword,
},
})
.then((r) => {
serverInfo = r.data;
console.log(serverInfo);
})
.catch((e) => {
toast.error(
"Failed to fetch server info. Please, change your server configuration!",
);
navigate("/");
});
axios
.get(`${$serverUrl}/history`, {
headers: {
"X-Password": $serverPassword,
},
})
.then((r) => {
history = r.data.History;
})
.catch((e) => {
toast.error("Failed to fetch history");
});
});
</script>
<main>
<header class="flex gap-5 flex-col">
<h1 class="h1"><span class="text-2xl align-middle">🔍</span> Search</h1>
<Searchbar />
</header>
<div class="my-10"></div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<div class="card card-border border-neutral shadow col-span-full">
<Stats {serverInfo} />
</div>
<div class="card card-border border-neutral shadow card-body">
<h2 class="h2">History</h2>
<History {history} />
</div>
<div class="card card-border border-neutral shadow card-body">
<h2 class="h2">Active services</h2>
<div class="overflow-x-auto">
{#if !serverInfo}
<p>Loading...</p>
{:else}
<Services {serverInfo} />
{/if}
</div>
</div>
<div class="card card-border border-neutral shadow card-body">
<h2 class="h2">Last data wells added</h2>
<div class="overflow-x-auto">
<Datawells dataleaks={serverInfo?.Dataleaks || []} />
</div>
</div>
<div class="card card-border border-neutral shadow card-body">
<h2 class="h2">How to search</h2>
<HowToSearch />
</div>
</div>
<div class="mb-10"></div>
</main>

2
front/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />

6
front/svelte.config.js Normal file
View File

@@ -0,0 +1,6 @@
/** @type {import('@sveltejs/vite-plugin-svelte').SvelteConfig} */
export default {
compilerOptions: {
runes: true,
},
};

21
front/tsconfig.json Normal file
View File

@@ -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"],
}
}
}

15
front/vite.config.ts Normal file
View File

@@ -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"),
},
},
});

View File

@@ -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.

107
leak-utils/README.md Normal file
View File

@@ -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
<details>
<summary>Click to expand</summary>
**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
];
```
</details>
### 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.

42
leak-utils/go.mod Normal file
View File

@@ -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
)

94
leak-utils/go.sum Normal file
View File

@@ -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=

View File

@@ -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], "<action>"))
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
}
}

173
leak-utils/misc/csv.go Normal file
View File

@@ -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
}

31
leak-utils/misc/misc.go Normal file
View File

@@ -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
}

67
leak-utils/misc/ulp.go Normal file
View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
//

105
leak-utils/parquet/utils.go Normal file
View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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
}

4
netlify.toml Normal file
View File

@@ -0,0 +1,4 @@
[build]
base = "front"
publish = "dist"
command = "bun run build"

155
nix/back.nix Normal file
View File

@@ -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"
}"
];
};
};
};
};
}

12
nix/devshell.nix Normal file
View File

@@ -0,0 +1,12 @@
{pkgs, ...}: {
devShell = pkgs.mkShell {
buildInputs = with pkgs; [
duckdb
air
# OSINT tools
ghunt
sherlock
holehe
];
};
}

21
nix/leak-utils.nix Normal file
View File

@@ -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;
}