init dataleak sample feature

Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
Hadi
2025-10-04 16:33:03 +02:00
parent d462ff791e
commit c036649a70
6 changed files with 193 additions and 8 deletions

View File

@@ -6,6 +6,7 @@ import (
"time"
"github.com/anotherhadi/eleakxir/backend/search"
"github.com/anotherhadi/eleakxir/backend/search/dataleak"
"github.com/anotherhadi/eleakxir/backend/server"
"github.com/gin-gonic/gin"
)
@@ -116,6 +117,20 @@ func routes(s *server.Server, cache *map[string]*search.Result, searchQueue chan
}
c.JSON(http.StatusOK, r)
})
s.Router.GET("/dataleak/sample", func(c *gin.Context) {
path := c.Query("path")
if path == "" {
c.JSON(http.StatusBadRequest, gin.H{"Error": "path is required"})
return
}
sample, err := dataleak.GetDataleakSample(*s, path)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"Error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"Sample": sample})
})
}
func Init(s *server.Server) {

View File

@@ -217,3 +217,49 @@ func getFromClause(s *server.Server) string {
}
return fmt.Sprintf("read_parquet([%s], union_by_name=true, filename=true)", strings.Join(parquets, ", "))
}
func GetDataleakSample(s server.Server, path string) ([][]string, error) {
rowsData := [][]string{}
query := fmt.Sprintf("SELECT * FROM read_parquet('%s') LIMIT 5", path)
rows, err := s.Duckdb.Query(query)
if err != nil {
return rowsData, err
}
defer rows.Close()
cols, err := rows.Columns()
if err != nil {
return rowsData, err
}
rowsData = append(rowsData, cols)
rawResult := make([][]byte, len(cols))
dest := make([]any, len(cols))
for i := range rawResult {
dest[i] = &rawResult[i]
}
for rows.Next() {
if err := rows.Scan(dest...); err != nil {
return rowsData, err
}
row := make([]string, len(cols))
for i := range cols {
if rawResult[i] == nil {
row[i] = ""
} else {
row[i] = string(rawResult[i])
}
}
rowsData = append(rowsData, row)
}
if err = rows.Err(); err != nil {
return rowsData, err
}
return rowsData, nil
}

View File

@@ -0,0 +1,118 @@
<script lang="ts">
import { serverPassword, serverUrl } from "$src/lib/stores/server";
import type { Dataleak } from "$src/lib/types";
import { Info } from "@lucide/svelte";
import axios from "axios";
const { dataleak }: { dataleak: Dataleak } = $props();
let popupOpen = $state(false);
let samples = $state<string[][]>([]);
let copyText = $state("Copy to clipboard")
async function getSample() {
if (!dataleak) return;
await axios
.get(`${$serverUrl}/dataleak/sample`, {
params: { path: dataleak.Path },
headers: {
"X-Password": $serverPassword,
},
})
.then((r) => {
samples = r.data.Sample;
})
.catch((e) => {
console.error("Erreur lors du fetch sample:", e);
});
}
async function copyToClipboard() {
if (!dataleak || !samples || samples.length === 0) {
copyText = "No data to copy";
return;
}
const leakName = dataleak.Name;
const columns = samples[0].join(", ");
const sampleRows = samples.slice(1).map(r => r.join(", ")).join("\n");
const textToCopy = `Leak Name: ${leakName}\nColumns: ${columns}\nSample:\n${sampleRows}`;
try {
await navigator.clipboard.writeText(textToCopy);
copyText = "Copied!";
setTimeout(() => copyText = "Copy Sample", 2000);
} catch (err) {
console.error("Failed to copy: ", err);
copyText = "Copy failed";
setTimeout(() => copyText = "Copy Sample", 2000);
}
}
</script>
<button
class="text-nowrap flex gap-2 items-center hover:text-base-content/70"
onclick={() => {
popupOpen = true;
getSample();
}}
>
{dataleak.Name}
<Info size={12} />
</button>
<dialog class="modal modal-bottom sm:modal-middle" class:modal-open={popupOpen}>
<div class="modal-box">
<form method="dialog">
<button
onclick={() => (popupOpen = 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 mb-6">{dataleak.Name}</h2>
{#if samples && samples.length > 1}
<div
class="overflow-x-auto border border-base-300 rounded-xl shadow-sm"
>
<table class="table table-zebra w-full text-sm">
<thead class="bg-base-200">
<tr>
{#each samples[0] as header, i (i)}
<th
class="font-semibold text-xs whitespace-nowrap capitalize"
>{header}</th
>
{/each}
</tr>
</thead>
<tbody>
{#each samples.slice(1) as row, rowIndex (rowIndex)}
<tr>
{#each row as cell, cellIndex (cellIndex)}
<td class="whitespace-nowrap">{cell}</td>
{/each}
</tr>
{/each}
</tbody>
</table>
</div>
{:else if samples && samples.length === 1}
<p class="text-sm opacity-60">No data available in this file.</p>
{:else}
<p class="text-sm opacity-60 italic">Loading...</p>
{/if}
</div>
</div>
<button class="btn my-6 btn-primary btn-xs w-full" onclick={copyToClipboard}>{copyText}</button>
</div>
<form method="dialog" class="modal-backdrop">
<button onclick={() => (popupOpen = false)}>close</button>
</form>
</dialog>

View File

@@ -1,7 +1,8 @@
<script lang="ts">
import type { Dataleak } from "$src/lib/types";
import { Search } from "@lucide/svelte";
import { Info, Search } from "@lucide/svelte";
import FaviconOrIcon from "../../favicon-or-icon.svelte";
import DatawellPopup from "./datawell-popup.svelte";
let {
dataleaks,
@@ -107,8 +108,8 @@
: ""}
/>
</th>
<th class="text-nowrap">
{item.Name}
<th class="">
<DatawellPopup dataleak={item}/>
</th>
<td>{item.Length.toLocaleString("fr")}</td>
{#if showColumns}

View File

@@ -3,12 +3,12 @@
const {
row,
}: { row: Record<string, string> | Array<Record<string, string>> } = $props();
}: { row: Record<string, string> | Array<Record<string, string>>| string[][] } = $props();
</script>
<div class="overflow-x-auto">
<table class="table">
{#if Array.isArray(row) && row.length !== 0}
{#if Array.isArray(row) && row.length > 0 && row[0]}
{@const head = Object.entries(row[0])}
<!-- head -->
<thead>
@@ -27,7 +27,9 @@
<tr>
{#each Object.entries(item) as [key, value]}
<th class="text-xs whitespace-nowrap font-semibold opacity-60">
{#if ( key.toLowerCase() == "url" || key.toLowerCase().endsWith("_url")) && value !== null && value !== ""}
{#if (key.toLowerCase() == "url" || key
.toLowerCase()
.endsWith("_url")) && value !== null && value !== ""}
<a
href={value}
target="_blank"
@@ -45,7 +47,7 @@
</tr>
{/each}
</tbody>
{:else}
{:else if row && Object.keys(row).length > 0}
<tbody>
{#each Object.entries(row) as [key, value]}
{#if key !== "source" && value !== null && value !== ""}
@@ -56,7 +58,9 @@
>
<td class="w-fit overflow-x-auto whitespace-nowrap">
{#if key.toLowerCase() == "url" || key.toLowerCase().endsWith("_url")}
{#if key.toLowerCase() == "url" || key
.toLowerCase()
.endsWith("_url")}
<a
href={value}
target="_blank"

View File

@@ -76,4 +76,5 @@ export type Dataleak = {
Length: number;
Size: number;
ModTime: string;
Path: string;
};