init
This commit is contained in:
BIN
.github/assets/banner.png
vendored
Normal file
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
BIN
.github/assets/logo.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.2 KiB |
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
result/
|
||||
testdata/
|
||||
13
CONTRIBUTING.md
Normal file
13
CONTRIBUTING.md
Normal 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
21
LICENSE
Normal 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
117
README.md
Normal 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
21
back/.air.toml
Normal 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
1
back/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
tmp/
|
||||
174
back/api/api.go
Normal file
174
back/api/api.go
Normal 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
21
back/cmd/main.go
Normal 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
70
back/go.mod
Normal 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
179
back/go.sum
Normal 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=
|
||||
191
back/search/dataleak/dataleak.go
Normal file
191
back/search/dataleak/dataleak.go
Normal 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
|
||||
}
|
||||
90
back/search/osint/github.go
Normal file
90
back/search/osint/github.go
Normal 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
68
back/search/search.go
Normal 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
111
back/server/dataleak.go
Normal 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
61
back/server/server.go
Normal 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
129
back/server/settings.go
Normal 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
131
back/server/utils.go
Normal 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
43
flake.lock
generated
Normal 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
58
flake.nix
Normal 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
26
front/.gitignore
vendored
Normal 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
377
front/bun.lock
Normal 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
14
front/index.html
Normal 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
34
front/package.json
Normal 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
3
front/public/favicon.svg
Normal 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
BIN
front/public/l.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 426 KiB |
6
front/src/App.svelte
Normal file
6
front/src/App.svelte
Normal 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
94
front/src/app.css
Normal 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;
|
||||
}
|
||||
79
front/src/lib/components/accordion.svelte
Normal file
79
front/src/lib/components/accordion.svelte
Normal 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}
|
||||
29
front/src/lib/components/dark-mode-toggle.svelte
Normal file
29
front/src/lib/components/dark-mode-toggle.svelte
Normal 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>
|
||||
104
front/src/lib/components/index/AnimatedBeam.svelte
Normal file
104
front/src/lib/components/index/AnimatedBeam.svelte
Normal 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>
|
||||
125
front/src/lib/components/index/AnimatedBeamMultiple.svelte
Normal file
125
front/src/lib/components/index/AnimatedBeamMultiple.svelte
Normal 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>
|
||||
19
front/src/lib/components/index/Circle.svelte
Normal file
19
front/src/lib/components/index/Circle.svelte
Normal 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>
|
||||
115
front/src/lib/components/index/search/datawells.svelte
Normal file
115
front/src/lib/components/index/search/datawells.svelte
Normal 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>
|
||||
130
front/src/lib/components/index/search/history.svelte
Normal file
130
front/src/lib/components/index/search/history.svelte
Normal 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>
|
||||
46
front/src/lib/components/index/search/howToSearch.svelte
Normal file
46
front/src/lib/components/index/search/howToSearch.svelte
Normal 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>
|
||||
313
front/src/lib/components/index/search/id/githubResult.svelte
Normal file
313
front/src/lib/components/index/search/id/githubResult.svelte
Normal 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}
|
||||
100
front/src/lib/components/index/search/id/row.svelte
Normal file
100
front/src/lib/components/index/search/id/row.svelte
Normal 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}
|
||||
72
front/src/lib/components/index/search/id/rows.svelte
Normal file
72
front/src/lib/components/index/search/id/rows.svelte
Normal 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}
|
||||
54
front/src/lib/components/index/search/id/stats.svelte
Normal file
54
front/src/lib/components/index/search/id/stats.svelte
Normal 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>
|
||||
104
front/src/lib/components/index/search/searchbar.svelte
Normal file
104
front/src/lib/components/index/search/searchbar.svelte
Normal 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>
|
||||
83
front/src/lib/components/index/search/services.svelte
Normal file
83
front/src/lib/components/index/search/services.svelte
Normal 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>
|
||||
46
front/src/lib/components/index/search/stats.svelte
Normal file
46
front/src/lib/components/index/search/stats.svelte
Normal 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>
|
||||
16
front/src/lib/components/logo.svelte
Normal file
16
front/src/lib/components/logo.svelte
Normal 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>
|
||||
144
front/src/lib/components/server-dialog.svelte
Normal file
144
front/src/lib/components/server-dialog.svelte
Normal 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>
|
||||
79
front/src/lib/components/table.svelte
Normal file
79
front/src/lib/components/table.svelte
Normal 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>
|
||||
30
front/src/lib/navigation/sidebar-menu-item.svelte
Normal file
30
front/src/lib/navigation/sidebar-menu-item.svelte
Normal 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>
|
||||
57
front/src/lib/navigation/sidebar.svelte
Normal file
57
front/src/lib/navigation/sidebar.svelte
Normal 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>
|
||||
47
front/src/lib/navigation/topbar.svelte
Normal file
47
front/src/lib/navigation/topbar.svelte
Normal 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>
|
||||
15
front/src/lib/stores/server.ts
Normal file
15
front/src/lib/stores/server.ts
Normal 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
78
front/src/lib/types.ts
Normal 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
80
front/src/lib/utils.ts
Normal 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
5
front/src/main.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { mount } from "svelte";
|
||||
import App from "./App.svelte";
|
||||
import "sv-router/generated";
|
||||
|
||||
mount(App, { target: document.querySelector("#app")! });
|
||||
57
front/src/routes/dataleaks/index.svelte
Normal file
57
front/src/routes/dataleaks/index.svelte
Normal 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>
|
||||
137
front/src/routes/index.svelte
Normal file
137
front/src/routes/index.svelte
Normal 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, I’m able to store 25
|
||||
billion rows in just over 600 GB on entry-level hardware. A query might
|
||||
take around an hour to complete, but the key point is that it’s actually
|
||||
possible to run such searches at home — something that would be completely
|
||||
out of reach if I had to maintain Elasticsearch’s much larger index
|
||||
footprint.
|
||||
</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>
|
||||
28
front/src/routes/layout.svelte
Normal file
28
front/src/routes/layout.svelte
Normal 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>
|
||||
18
front/src/routes/leak-utils/index.svelte
Normal file
18
front/src/routes/leak-utils/index.svelte
Normal 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>
|
||||
18
front/src/routes/parquet/index.svelte
Normal file
18
front/src/routes/parquet/index.svelte
Normal 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>
|
||||
226
front/src/routes/search/[id]/index.svelte
Normal file
226
front/src/routes/search/[id]/index.svelte
Normal 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>
|
||||
94
front/src/routes/search/index.svelte
Normal file
94
front/src/routes/search/index.svelte
Normal 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
2
front/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/// <reference types="svelte" />
|
||||
/// <reference types="vite/client" />
|
||||
6
front/svelte.config.js
Normal file
6
front/svelte.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
/** @type {import('@sveltejs/vite-plugin-svelte').SvelteConfig} */
|
||||
export default {
|
||||
compilerOptions: {
|
||||
runes: true,
|
||||
},
|
||||
};
|
||||
21
front/tsconfig.json
Normal file
21
front/tsconfig.json
Normal 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
15
front/vite.config.ts
Normal 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"),
|
||||
},
|
||||
},
|
||||
});
|
||||
138
leak-utils/DATALEAKS-NORMALIZATION.md
Normal file
138
leak-utils/DATALEAKS-NORMALIZATION.md
Normal 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
107
leak-utils/README.md
Normal 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
42
leak-utils/go.mod
Normal 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
94
leak-utils/go.sum
Normal 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=
|
||||
145
leak-utils/leak-utils/main.go
Normal file
145
leak-utils/leak-utils/main.go
Normal 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
173
leak-utils/misc/csv.go
Normal 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
31
leak-utils/misc/misc.go
Normal 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
67
leak-utils/misc/ulp.go
Normal 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
|
||||
}
|
||||
107
leak-utils/parquet/format.go
Normal file
107
leak-utils/parquet/format.go
Normal 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
|
||||
}
|
||||
276
leak-utils/parquet/parquet.go
Normal file
276
leak-utils/parquet/parquet.go
Normal 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
|
||||
}
|
||||
81
leak-utils/parquet/suggestions.go
Normal file
81
leak-utils/parquet/suggestions.go
Normal 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
105
leak-utils/parquet/utils.go
Normal 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()
|
||||
}
|
||||
27
leak-utils/settings/colors.go
Normal file
27
leak-utils/settings/colors.go
Normal 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()
|
||||
}
|
||||
9
leak-utils/settings/settings.go
Normal file
9
leak-utils/settings/settings.go
Normal 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
4
netlify.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
[build]
|
||||
base = "front"
|
||||
publish = "dist"
|
||||
command = "bun run build"
|
||||
155
nix/back.nix
Normal file
155
nix/back.nix
Normal 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
12
nix/devshell.nix
Normal 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
21
nix/leak-utils.nix
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user