init dataleak sample feature
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/anotherhadi/eleakxir/backend/search"
|
"github.com/anotherhadi/eleakxir/backend/search"
|
||||||
|
"github.com/anotherhadi/eleakxir/backend/search/dataleak"
|
||||||
"github.com/anotherhadi/eleakxir/backend/server"
|
"github.com/anotherhadi/eleakxir/backend/server"
|
||||||
"github.com/gin-gonic/gin"
|
"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)
|
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) {
|
func Init(s *server.Server) {
|
||||||
|
|||||||
@@ -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, ", "))
|
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
|
||||||
|
}
|
||||||
|
|||||||
118
front/src/lib/components/index/search/datawell-popup.svelte
Normal file
118
front/src/lib/components/index/search/datawell-popup.svelte
Normal 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>
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Dataleak } from "$src/lib/types";
|
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 FaviconOrIcon from "../../favicon-or-icon.svelte";
|
||||||
|
import DatawellPopup from "./datawell-popup.svelte";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
dataleaks,
|
dataleaks,
|
||||||
@@ -107,8 +108,8 @@
|
|||||||
: ""}
|
: ""}
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th class="text-nowrap">
|
<th class="">
|
||||||
{item.Name}
|
<DatawellPopup dataleak={item}/>
|
||||||
</th>
|
</th>
|
||||||
<td>{item.Length.toLocaleString("fr")}</td>
|
<td>{item.Length.toLocaleString("fr")}</td>
|
||||||
{#if showColumns}
|
{#if showColumns}
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
row,
|
row,
|
||||||
}: { row: Record<string, string> | Array<Record<string, string>> } = $props();
|
}: { row: Record<string, string> | Array<Record<string, string>>| string[][] } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="table">
|
<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])}
|
{@const head = Object.entries(row[0])}
|
||||||
<!-- head -->
|
<!-- head -->
|
||||||
<thead>
|
<thead>
|
||||||
@@ -27,7 +27,9 @@
|
|||||||
<tr>
|
<tr>
|
||||||
{#each Object.entries(item) as [key, value]}
|
{#each Object.entries(item) as [key, value]}
|
||||||
<th class="text-xs whitespace-nowrap font-semibold opacity-60">
|
<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
|
<a
|
||||||
href={value}
|
href={value}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -45,7 +47,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</tbody>
|
||||||
{:else}
|
{:else if row && Object.keys(row).length > 0}
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each Object.entries(row) as [key, value]}
|
{#each Object.entries(row) as [key, value]}
|
||||||
{#if key !== "source" && value !== null && value !== ""}
|
{#if key !== "source" && value !== null && value !== ""}
|
||||||
@@ -56,7 +58,9 @@
|
|||||||
>
|
>
|
||||||
|
|
||||||
<td class="w-fit overflow-x-auto whitespace-nowrap">
|
<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
|
<a
|
||||||
href={value}
|
href={value}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@@ -76,4 +76,5 @@ export type Dataleak = {
|
|||||||
Length: number;
|
Length: number;
|
||||||
Size: number;
|
Size: number;
|
||||||
ModTime: string;
|
ModTime: string;
|
||||||
|
Path: string;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user