mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-10 15:58:38 +01:00
update security + chart events è anomaly service
This commit is contained in:
148
dashboard/components/events/EventsFunnelChart.vue
Normal file
148
dashboard/components/events/EventsFunnelChart.vue
Normal file
@@ -0,0 +1,148 @@
|
||||
<script setup lang="ts">
|
||||
import type { ChartData, ChartOptions } from 'chart.js';
|
||||
import { defineChartComponent } from 'vue-chart-3';
|
||||
registerChartComponents();
|
||||
|
||||
const FunnelChart = defineChartComponent('funnel', 'funnel');
|
||||
|
||||
const chartOptions = ref<ChartOptions<'funnel'>>({
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'nearest',
|
||||
axis: 'x',
|
||||
includeInvisible: true
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
ticks: { display: true },
|
||||
grid: {
|
||||
display: true,
|
||||
drawBorder: false,
|
||||
color: '#CCCCCC22',
|
||||
// borderDash: [5, 10]
|
||||
},
|
||||
},
|
||||
x: {
|
||||
ticks: { display: true },
|
||||
grid: {
|
||||
display: true,
|
||||
drawBorder: false,
|
||||
color: '#CCCCCC22',
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
title: { display: false },
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
titleFont: { size: 16, weight: 'bold' },
|
||||
bodyFont: { size: 14 },
|
||||
padding: 10,
|
||||
cornerRadius: 4,
|
||||
boxPadding: 10,
|
||||
caretPadding: 20,
|
||||
yAlign: 'bottom',
|
||||
xAlign: 'center',
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const chartData = ref<ChartData<'funnel'>>({
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
data: [],
|
||||
backgroundColor: ['#5680F8' + '77'],
|
||||
// borderColor: '#0000CC',
|
||||
// borderWidth: 4,
|
||||
fill: true,
|
||||
tension: 0.45,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 10,
|
||||
hoverBackgroundColor: '#5680F8',
|
||||
// hoverBorderColor: 'white',
|
||||
// hoverBorderWidth: 2,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
// const c = document.createElement('canvas');
|
||||
// const ctx = c.getContext("2d");
|
||||
// let gradient: any = `${'#0000CC'}22`;
|
||||
// if (ctx) {
|
||||
// gradient = ctx.createLinearGradient(0, 25, 0, 300);
|
||||
// gradient.addColorStop(0, `${'#0000CC'}99`);
|
||||
// gradient.addColorStop(0.35, `${'#0000CC'}66`);
|
||||
// gradient.addColorStop(1, `${'#0000CC'}22`);
|
||||
// } else {
|
||||
// console.warn('Cannot get context for gradient');
|
||||
// }
|
||||
|
||||
// chartData.value.datasets[0].backgroundColor = [gradient];
|
||||
|
||||
});
|
||||
|
||||
const activeProjectId = useActiveProjectId();
|
||||
const { safeSnapshotDates } = useSnapshot();
|
||||
|
||||
const eventsCount = await useFetch<{ _id: string, count: number }[]>(`/api/data/query`, {
|
||||
...signHeaders({
|
||||
'x-pid': activeProjectId.data.value || '',
|
||||
'x-schema': 'events',
|
||||
'x-from': safeSnapshotDates.value.from,
|
||||
'x-to': safeSnapshotDates.value.to,
|
||||
'x-query-limit': '1000'
|
||||
}), lazy: true
|
||||
});
|
||||
|
||||
|
||||
const enabledEvents = ref<string[]>([]);
|
||||
|
||||
async function onEventCheck(eventName: string) {
|
||||
const index = enabledEvents.value.indexOf(eventName);
|
||||
if (index == -1) {
|
||||
enabledEvents.value.push(eventName);
|
||||
} else {
|
||||
enabledEvents.value.splice(index, 1);
|
||||
}
|
||||
|
||||
|
||||
chartData.value.labels = enabledEvents.value;
|
||||
chartData.value.datasets[0].data = [];
|
||||
|
||||
for (const enabledEvent of enabledEvents.value) {
|
||||
const target = (eventsCount.data.value ?? []).find(e => e._id == enabledEvent);
|
||||
chartData.value.datasets[0].data.push(target?.count || 0);
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<CardTitled title="Funnel" sub="Funnel events">
|
||||
<div class="flex gap-2 justify-between">
|
||||
<div>
|
||||
<div class="min-w-[20rem]">
|
||||
Select two or more events
|
||||
</div>
|
||||
<div v-for="event of eventsCount.data.value">
|
||||
<UCheckbox @change="onEventCheck(event._id)" :value="enabledEvents.includes(event._id)"
|
||||
:label="event._id">
|
||||
</UCheckbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grow">
|
||||
<FunnelChart :chart-data="chartData" :options="chartOptions"> </FunnelChart>
|
||||
</div>
|
||||
</div>
|
||||
</CardTitled>
|
||||
</template>
|
||||
@@ -1,12 +1,7 @@
|
||||
|
||||
import { Chart, registerables } from 'chart.js';
|
||||
import annotaionPlugin from 'chartjs-plugin-annotation';
|
||||
|
||||
let registered = false;
|
||||
export async function registerChartComponents() {
|
||||
if (registered) return;
|
||||
if (process.client) {
|
||||
Chart.register(...registerables, annotaionPlugin);
|
||||
console.log('registerChartComponents is deprecated. Plugin is now used');
|
||||
registered = true;
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export default defineNuxtConfig({
|
||||
postcss: {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
autoprefixer: {},
|
||||
}
|
||||
},
|
||||
colorMode: {
|
||||
@@ -60,6 +60,9 @@ export default defineNuxtConfig({
|
||||
nitro: {
|
||||
plugins: ['~/server/init.ts']
|
||||
},
|
||||
plugins: [
|
||||
{ src: '~/plugins/chartjs.ts', mode: 'client' }
|
||||
],
|
||||
...gooleSignInConfig,
|
||||
modules: ['@nuxt/ui', 'nuxt-vue3-google-signin'],
|
||||
devServer: {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"@getbrevo/brevo": "^2.2.0",
|
||||
"@nuxtjs/tailwindcss": "^6.12.0",
|
||||
"chart.js": "^3.9.1",
|
||||
"chartjs-chart-funnel": "^4.2.1",
|
||||
"chartjs-plugin-annotation": "^2.2.1",
|
||||
"date-fns": "^3.6.0",
|
||||
"dayjs": "^1.11.11",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import EventsFunnelChart from '~/components/events/EventsFunnelChart.vue';
|
||||
|
||||
|
||||
definePageMeta({ layout: 'dashboard' });
|
||||
|
||||
@@ -22,7 +24,8 @@ const refreshKey = computed(() => `${snapshot.value._id.toString() + activeProje
|
||||
|
||||
<div class="flex gap-6 flex-col xl:flex-row h-full">
|
||||
|
||||
<CardTitled :key="refreshKey" class="p-4 flex-[4] w-full h-full" title="Events" sub="Events stacked bar chart.">
|
||||
<CardTitled :key="refreshKey" class="p-4 flex-[4] w-full h-full" title="Events"
|
||||
sub="Events stacked bar chart.">
|
||||
<template #header>
|
||||
<SelectButton @changeIndex="eventsStackedSelectIndex = $event"
|
||||
:currentIndex="eventsStackedSelectIndex" :options="selectLabelsEvents">
|
||||
@@ -41,6 +44,10 @@ const refreshKey = computed(() => `${snapshot.value._id.toString() + activeProje
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<EventsFunnelChart :key="refreshKey" class="w-full"></EventsFunnelChart>
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<EventsUserFlow :key="refreshKey"></EventsUserFlow>
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,10 @@ const limitsInfo = ref<{
|
||||
percent: number
|
||||
}>();
|
||||
|
||||
const justLogged = computed(() => {
|
||||
return route.query.just_logged;
|
||||
})
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
if (route.query.just_logged) return location.href = '/';
|
||||
@@ -68,7 +72,8 @@ function goToUpgrade() {
|
||||
|
||||
<div class="dashboard w-full h-full overflow-y-auto pb-20 md:pt-4 lg:pt-0">
|
||||
|
||||
<div :key="'home-' + isLiveDemo()" v-if="projects && activeProject && (firstInteraction.data.value === true)">
|
||||
<div :key="'home-' + isLiveDemo()"
|
||||
v-if="projects && activeProject && (firstInteraction.data.value === true) && !justLogged">
|
||||
|
||||
<div class="w-full px-4 py-2 gap-2 flex flex-col">
|
||||
<div v-if="limitsInfo && limitsInfo.limited"
|
||||
@@ -188,13 +193,17 @@ function goToUpgrade() {
|
||||
|
||||
</div>
|
||||
|
||||
<FirstInteraction v-if="!justLogged" :refresh-interaction="firstInteraction.refresh"
|
||||
:first-interaction="(firstInteraction.data.value || false)"></FirstInteraction>
|
||||
|
||||
<FirstInteraction :refresh-interaction="firstInteraction.refresh" :first-interaction="(firstInteraction.data.value || false)"></FirstInteraction>
|
||||
|
||||
<div class="text-text/85 mt-8 ml-8 poppis text-[1.2rem]" v-if="projects && projects.length == 0">
|
||||
<div class="text-text/85 mt-8 ml-8 poppis text-[1.2rem]" v-if="projects && projects.length == 0 && !justLogged">
|
||||
Create your first project...
|
||||
</div>
|
||||
|
||||
<div v-if="justLogged" class="text-[2rem]">
|
||||
The page will refresh soon
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import type { } from '#ui/types/tabs'
|
||||
|
||||
definePageMeta({ layout: 'dashboard' });
|
||||
const activeProjectId = useActiveProjectId();
|
||||
|
||||
@@ -25,6 +27,15 @@ function showAnomalyInfoAlert() {
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const rows = computed(() => reportList.data.value || [])
|
||||
|
||||
const columns = [
|
||||
{ key: 'scan', label: 'Scan date' },
|
||||
{ key: 'type', label: 'Type' },
|
||||
{ key: 'data', label: 'Data' },
|
||||
];
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
@@ -41,35 +52,72 @@ function showAnomalyInfoAlert() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pb-[10rem]">
|
||||
<UTable :rows="rows" :columns="columns">
|
||||
|
||||
|
||||
<div class="w-full h-full py-8 px-12">
|
||||
<template #scan-data="{ row }">
|
||||
<div class="text-lyx-text-dark">
|
||||
{{ new Date(row.data.created_at).toLocaleString() }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #type-data="{ row }">
|
||||
<UBadge color="white" class="w-[4rem] flex justify-center">
|
||||
{{ row.type }}
|
||||
</UBadge>
|
||||
</template>
|
||||
|
||||
<template #data-data="{ row }">
|
||||
<div class="text-lyx-text-dark">
|
||||
<div v-if="row.type === 'domain'">
|
||||
{{ row.data.domain }}
|
||||
</div>
|
||||
<div v-if="row.type === 'visit'">
|
||||
{{ row.data.visit }}
|
||||
</div>
|
||||
<div v-if="row.type === 'event'">
|
||||
{{ row.data.event }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- <template #actions-data="{ row }">
|
||||
<UDropdown :items="items(row)">
|
||||
<UButton color="gray" variant="ghost" icon="i-heroicons-ellipsis-horizontal-20-solid" />
|
||||
</UDropdown>
|
||||
</template> -->
|
||||
|
||||
</UTable>
|
||||
</div>
|
||||
|
||||
<!-- <div class="w-full py-8 px-12 pb-[10rem]">
|
||||
<div v-if="reportList.data.value" class="flex flex-col gap-2">
|
||||
<div v-for="entry of reportList.data.value">
|
||||
<div v-if="entry.type === 'event'" class="flex gap-2">
|
||||
<div v-for="entry of reportList.data.value" class="flex flex-col gap-4">
|
||||
<div v-if="entry.type === 'event'" class="flex gap-2 flex-col lg:flex-row items-center lg:items-start">
|
||||
<div class="text-lyx-text-darker">{{ new Date(entry.data.created_at).toLocaleString() }}</div>
|
||||
<UBadge class="w-[4rem] flex justify-center"> {{ entry.type }} </UBadge>
|
||||
<div class="text-lyx-text-dark">
|
||||
Event date: {{ new Date(entry.data.eventDate).toLocaleString() }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="entry.type === 'visit'" class="flex gap-2">
|
||||
<div v-if="entry.type === 'visit'" class="flex gap-2 flex-col lg:flex-row items-center lg:items-start">
|
||||
<div class="text-lyx-text-darker">{{ new Date(entry.data.created_at).toLocaleString() }}</div>
|
||||
<UBadge class="w-[4rem] flex justify-center"> {{ entry.type }} </UBadge>
|
||||
<div class="text-lyx-text-dark">
|
||||
Visit date: {{ new Date(entry.data.visitDate).toLocaleString() }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="entry.type === 'domain'" class="flex gap-2">
|
||||
<div v-if="entry.type === 'domain'" class="flex gap-2 flex-col py-2 lg:flex-row items-center lg:items-start">
|
||||
<div class="text-lyx-text-darker">{{ new Date(entry.data.created_at).toLocaleString() }}</div>
|
||||
<UBadge class="w-[4rem] flex justify-center"> {{ entry.type }} </UBadge>
|
||||
<div class="text-lyx-text-dark">
|
||||
Domain found: {{ entry.data.domain }}
|
||||
</div>
|
||||
{{ entry.data.domain }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
10
dashboard/plugins/chartjs.ts
Normal file
10
dashboard/plugins/chartjs.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
import { Chart, registerables } from 'chart.js';
|
||||
import annotaionPlugin from 'chartjs-plugin-annotation';
|
||||
import 'chartjs-chart-funnel';
|
||||
|
||||
import { FunnelController, FunnelChart, TrapezoidElement } from 'chartjs-chart-funnel';
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
Chart.register(...registerables, annotaionPlugin, FunnelController, FunnelChart, TrapezoidElement);
|
||||
})
|
||||
70
dashboard/pnpm-lock.yaml
generated
70
dashboard/pnpm-lock.yaml
generated
@@ -17,6 +17,9 @@ importers:
|
||||
chart.js:
|
||||
specifier: ^3.9.1
|
||||
version: 3.9.1
|
||||
chartjs-chart-funnel:
|
||||
specifier: ^4.2.1
|
||||
version: 4.2.1(chart.js@3.9.1)
|
||||
chartjs-plugin-annotation:
|
||||
specifier: ^2.2.1
|
||||
version: 2.2.1(chart.js@3.9.1)
|
||||
@@ -1181,6 +1184,9 @@ packages:
|
||||
'@types/argparse@1.0.38':
|
||||
resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==}
|
||||
|
||||
'@types/chroma-js@2.4.4':
|
||||
resolution: {integrity: sha512-/DTccpHTaKomqussrn+ciEvfW4k6NAHzNzs/sts1TCqg333qNxOhy8TNIoQCmbGG3Tl8KdEhkGAssb1n3mTXiQ==}
|
||||
|
||||
'@types/estree@1.0.5':
|
||||
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
|
||||
|
||||
@@ -1462,20 +1468,20 @@ packages:
|
||||
'@vue/reactivity@3.4.27':
|
||||
resolution: {integrity: sha512-kK0g4NknW6JX2yySLpsm2jlunZJl2/RJGZ0H9ddHdfBVHcNzxmQ0sS0b09ipmBoQpY8JM2KmUw+a6sO8Zo+zIA==}
|
||||
|
||||
'@vue/reactivity@3.5.7':
|
||||
resolution: {integrity: sha512-yF0EpokpOHRNXyn/h6abXc9JFIzfdAf0MJHIi92xxCWS0mqrXH6+2aZ+A6EbSrspGzX5MHTd5N8iBA28HnXu9g==}
|
||||
'@vue/reactivity@3.5.8':
|
||||
resolution: {integrity: sha512-mlgUyFHLCUZcAYkqvzYnlBRCh0t5ZQfLYit7nukn1GR96gc48Bp4B7OIcSfVSvlG1k3BPfD+p22gi1t2n9tsXg==}
|
||||
|
||||
'@vue/runtime-core@3.4.27':
|
||||
resolution: {integrity: sha512-7aYA9GEbOOdviqVvcuweTLe5Za4qBZkUY7SvET6vE8kyypxVgaT1ixHLg4urtOlrApdgcdgHoTZCUuTGap/5WA==}
|
||||
|
||||
'@vue/runtime-core@3.5.7':
|
||||
resolution: {integrity: sha512-OzLpBpKbZEaZVSNfd+hQbfBrDKux+b7Yl5hYhhWWWhHD7fEpF+CdI3Brm5k5GsufHEfvMcjruPxwQZuBN6nFYQ==}
|
||||
'@vue/runtime-core@3.5.8':
|
||||
resolution: {integrity: sha512-fJuPelh64agZ8vKkZgp5iCkPaEqFJsYzxLk9vSC0X3G8ppknclNDr61gDc45yBGTaN5Xqc1qZWU3/NoaBMHcjQ==}
|
||||
|
||||
'@vue/runtime-dom@3.4.27':
|
||||
resolution: {integrity: sha512-ScOmP70/3NPM+TW9hvVAz6VWWtZJqkbdf7w6ySsws+EsqtHvkhxaWLecrTorFxsawelM5Ys9FnDEMt6BPBDS0Q==}
|
||||
|
||||
'@vue/runtime-dom@3.5.7':
|
||||
resolution: {integrity: sha512-fL7cETfE27U2jyTgqzE382IGFY6a6uyznErn27KbbEzNctzxxUWYDbaN3B55l9nXh0xW2LRWPuWKOvjtO2UewQ==}
|
||||
'@vue/runtime-dom@3.5.8':
|
||||
resolution: {integrity: sha512-DpAUz+PKjTZPUOB6zJgkxVI3GuYc2iWZiNeeHQUw53kdrparSTG6HeXUrYDjaam8dVsCdvQxDz6ZWxnyjccUjQ==}
|
||||
|
||||
'@vue/server-renderer@3.4.27':
|
||||
resolution: {integrity: sha512-dlAMEuvmeA3rJsOMJ2J1kXU7o7pOxgsNHVr9K8hB3ImIkSuBrIdy0vF66h8gf8Tuinf1TK3mPAz2+2sqyf3KzA==}
|
||||
@@ -1485,12 +1491,12 @@ packages:
|
||||
'@vue/shared@3.4.27':
|
||||
resolution: {integrity: sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==}
|
||||
|
||||
'@vue/shared@3.5.6':
|
||||
resolution: {integrity: sha512-eidH0HInnL39z6wAt6SFIwBrvGOpDWsDxlw3rCgo1B+CQ1781WzQUSU3YjxgdkcJo9Q8S6LmXTkvI+cLHGkQfA==}
|
||||
|
||||
'@vue/shared@3.5.7':
|
||||
resolution: {integrity: sha512-NBE1PBIvzIedxIc2RZiKXvGbJkrZ2/hLf3h8GlS4/sP9xcXEZMFWOazFkNd6aGeUCMaproe5MHVYB3/4AW9q9g==}
|
||||
|
||||
'@vue/shared@3.5.8':
|
||||
resolution: {integrity: sha512-mJleSWbAGySd2RJdX1RBtcrUBX6snyOc0qHpgk3lGi4l9/P/3ny3ELqFWqYdkXIwwNN/kdm8nD9ky8o6l/Lx2A==}
|
||||
|
||||
'@vueuse/components@10.10.0':
|
||||
resolution: {integrity: sha512-HiA10NQ9HJAGnju+8ZK4TyA8LIc0a6BnJmVWDa/k+TRhaYCVacSDU04k0BQ2otV+gghUDdwu98upf6TDRXpoeg==}
|
||||
|
||||
@@ -1863,6 +1869,11 @@ packages:
|
||||
chart.js@3.9.1:
|
||||
resolution: {integrity: sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w==}
|
||||
|
||||
chartjs-chart-funnel@4.2.1:
|
||||
resolution: {integrity: sha512-S1eqYMDXefl7k7uuQc5MA83ZS9zjclt4bbYXbmPJ5GEvB6HMBb7tt892R62AtzoKXbt/VfDNy9Sq3L785sWvdQ==}
|
||||
peerDependencies:
|
||||
chart.js: '>=3.7.0'
|
||||
|
||||
chartjs-plugin-annotation@2.2.1:
|
||||
resolution: {integrity: sha512-RL9UtrFr2SXd7C47zD0MZqn6ZLgrcRt3ySC6cYal2amBdANcYB1QcwFXcpKWAYnO4SGJYRok7P5rKDDNgJMA/w==}
|
||||
peerDependencies:
|
||||
@@ -1879,6 +1890,9 @@ packages:
|
||||
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
chroma-js@2.6.0:
|
||||
resolution: {integrity: sha512-BLHvCB9s8Z1EV4ethr6xnkl/P2YRFOGqfgvuMG/MyCbZPrTA+NeiByY6XvgF0zP4/2deU2CXnWyMa3zu1LqQ3A==}
|
||||
|
||||
ci-info@4.0.0:
|
||||
resolution: {integrity: sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -6661,6 +6675,8 @@ snapshots:
|
||||
|
||||
'@types/argparse@1.0.38': {}
|
||||
|
||||
'@types/chroma-js@2.4.4': {}
|
||||
|
||||
'@types/estree@1.0.5': {}
|
||||
|
||||
'@types/http-proxy@1.17.14':
|
||||
@@ -7136,7 +7152,7 @@ snapshots:
|
||||
'@volar/language-core': 1.11.1
|
||||
'@volar/source-map': 1.11.1
|
||||
'@vue/compiler-dom': 3.4.27
|
||||
'@vue/shared': 3.5.6
|
||||
'@vue/shared': 3.5.7
|
||||
computeds: 0.0.1
|
||||
minimatch: 9.0.4
|
||||
muggle-string: 0.3.1
|
||||
@@ -7149,19 +7165,19 @@ snapshots:
|
||||
dependencies:
|
||||
'@vue/shared': 3.4.27
|
||||
|
||||
'@vue/reactivity@3.5.7':
|
||||
'@vue/reactivity@3.5.8':
|
||||
dependencies:
|
||||
'@vue/shared': 3.5.7
|
||||
'@vue/shared': 3.5.8
|
||||
|
||||
'@vue/runtime-core@3.4.27':
|
||||
dependencies:
|
||||
'@vue/reactivity': 3.4.27
|
||||
'@vue/shared': 3.4.27
|
||||
|
||||
'@vue/runtime-core@3.5.7':
|
||||
'@vue/runtime-core@3.5.8':
|
||||
dependencies:
|
||||
'@vue/reactivity': 3.5.7
|
||||
'@vue/shared': 3.5.7
|
||||
'@vue/reactivity': 3.5.8
|
||||
'@vue/shared': 3.5.8
|
||||
|
||||
'@vue/runtime-dom@3.4.27':
|
||||
dependencies:
|
||||
@@ -7169,11 +7185,11 @@ snapshots:
|
||||
'@vue/shared': 3.4.27
|
||||
csstype: 3.1.3
|
||||
|
||||
'@vue/runtime-dom@3.5.7':
|
||||
'@vue/runtime-dom@3.5.8':
|
||||
dependencies:
|
||||
'@vue/reactivity': 3.5.7
|
||||
'@vue/runtime-core': 3.5.7
|
||||
'@vue/shared': 3.5.7
|
||||
'@vue/reactivity': 3.5.8
|
||||
'@vue/runtime-core': 3.5.8
|
||||
'@vue/shared': 3.5.8
|
||||
csstype: 3.1.3
|
||||
|
||||
'@vue/server-renderer@3.4.27(vue@3.4.27(typescript@5.4.2))':
|
||||
@@ -7184,10 +7200,10 @@ snapshots:
|
||||
|
||||
'@vue/shared@3.4.27': {}
|
||||
|
||||
'@vue/shared@3.5.6': {}
|
||||
|
||||
'@vue/shared@3.5.7': {}
|
||||
|
||||
'@vue/shared@3.5.8': {}
|
||||
|
||||
'@vueuse/components@10.10.0(vue@3.4.27(typescript@5.4.2))':
|
||||
dependencies:
|
||||
'@vueuse/core': 10.10.0(vue@3.4.27(typescript@5.4.2))
|
||||
@@ -7575,6 +7591,12 @@ snapshots:
|
||||
|
||||
chart.js@3.9.1: {}
|
||||
|
||||
chartjs-chart-funnel@4.2.1(chart.js@3.9.1):
|
||||
dependencies:
|
||||
'@types/chroma-js': 2.4.4
|
||||
chart.js: 3.9.1
|
||||
chroma-js: 2.6.0
|
||||
|
||||
chartjs-plugin-annotation@2.2.1(chart.js@3.9.1):
|
||||
dependencies:
|
||||
chart.js: 3.9.1
|
||||
@@ -7597,6 +7619,8 @@ snapshots:
|
||||
|
||||
chownr@2.0.0: {}
|
||||
|
||||
chroma-js@2.6.0: {}
|
||||
|
||||
ci-info@4.0.0: {}
|
||||
|
||||
citty@0.1.6:
|
||||
@@ -11249,8 +11273,8 @@ snapshots:
|
||||
|
||||
vue-chart-3@3.1.8(chart.js@3.9.1)(vue@3.4.27(typescript@5.4.2)):
|
||||
dependencies:
|
||||
'@vue/runtime-core': 3.5.7
|
||||
'@vue/runtime-dom': 3.5.7
|
||||
'@vue/runtime-core': 3.5.8
|
||||
'@vue/runtime-dom': 3.5.8
|
||||
chart.js: 3.9.1
|
||||
csstype: 3.1.3
|
||||
lodash-es: 4.17.21
|
||||
|
||||
@@ -38,7 +38,7 @@ export default defineEventHandler(async event => {
|
||||
report.push({ type: 'domain', data: domain });
|
||||
}
|
||||
|
||||
return report.toSorted((a, b) => a.data.created_at.getTime() - b.data.created_at.getTime());
|
||||
return report.toSorted((a, b) => b.data.created_at.getTime() - a.data.created_at.getTime());
|
||||
|
||||
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@getbrevo/brevo": "^2.2.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"mongoose": "^8.3.2"
|
||||
},
|
||||
|
||||
1047
security/pnpm-lock.yaml
generated
1047
security/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -4,10 +4,9 @@ import { AnomalyVisitModel } from '@schema/anomalies/AnomalyVisitSchema';
|
||||
import { AnomalyEventsModel } from '@schema/anomalies/AnomalyEventsSchema';
|
||||
import { EventModel } from "@schema/metrics/EventSchema";
|
||||
import { VisitModel } from '@schema/metrics/VisitSchema'
|
||||
import EmailService from "@services/EmailService";
|
||||
|
||||
import * as url from 'url';
|
||||
import { ProjectModel } from "@schema/ProjectSchema";
|
||||
import { UserModel } from "@schema/UserSchema";
|
||||
import { getAggregation } from "./Aggregations";
|
||||
|
||||
type TAvgInput = { _id: string, count: number }
|
||||
@@ -112,7 +111,7 @@ export async function findAnomalies(project_id: string, callback: AnomalyCallbac
|
||||
visits: [],
|
||||
events: [],
|
||||
dns: [],
|
||||
pid: project_id
|
||||
pid: project_id,
|
||||
}
|
||||
|
||||
for (const visit of visitAnomalies) {
|
||||
@@ -136,19 +135,6 @@ export async function findAnomalies(project_id: string, callback: AnomalyCallbac
|
||||
report.dns.push(website);
|
||||
}
|
||||
|
||||
// const project = await ProjectModel.findById(pid);
|
||||
// if (!project) return { ok: false, error: 'Cannot find project with id ' + pid.toString() }
|
||||
// const user = await UserModel.findById(project.owner);
|
||||
// if (!user) return { ok: false, error: 'Cannot find user with id ' + project.owner.toString() }
|
||||
|
||||
// if (shouldSendMail.visitsEvents === true) {
|
||||
// await EmailService.sendAnomalyVisitsEventsEmail(user.email, project.name);
|
||||
// }
|
||||
// if (shouldSendMail.domains === true) {
|
||||
// await EmailService.sendAnomalyDomainEmail(user.email, project.name);
|
||||
// }
|
||||
|
||||
|
||||
callback(report);
|
||||
return report;
|
||||
|
||||
|
||||
@@ -1,18 +1,46 @@
|
||||
import { anomalyCheckAll, AnomalyReport } from "./AnomalyService";
|
||||
import { anomalyCheckAll, AnomalyReport, findAnomalies } from "./AnomalyService";
|
||||
import { connectDatabase } from '@services/DatabaseService'
|
||||
import { requireEnv } from '@utils/requireEnv'
|
||||
|
||||
import EmailService from "@services/EmailService";
|
||||
|
||||
import { ProjectModel } from "@schema/ProjectSchema";
|
||||
import { UserModel } from "@schema/UserSchema";
|
||||
|
||||
EmailService.init(requireEnv('BREVO_API_KEY'));
|
||||
connectDatabase(requireEnv('MONGO_CONNECTION_STRING'));
|
||||
|
||||
anomalyCheckAll(async report => {
|
||||
|
||||
import fs from 'fs';
|
||||
|
||||
const reports: AnomalyReport[] = [];
|
||||
|
||||
anomalyCheckAll(report => {
|
||||
if (report.visits.length > 0 || report.events.length > 0 || report.dns.length > 0) {
|
||||
reports.push(report);
|
||||
|
||||
const project = await ProjectModel.findById(report.pid);
|
||||
if (!project) return { ok: false, error: 'Cannot find project with id ' + report.pid.toString() }
|
||||
const user = await UserModel.findById(project.owner);
|
||||
if (!user) return { ok: false, error: 'Cannot find user with id ' + project.owner.toString() }
|
||||
|
||||
if (report.visits.length > 0 || report.events.length > 0) {
|
||||
|
||||
await EmailService.sendAnomalyVisitsEventsEmail(
|
||||
user.email,
|
||||
project.name,
|
||||
{ visits: report.visits, events: report.events }
|
||||
);
|
||||
|
||||
}
|
||||
}).then(e => {
|
||||
fs.writeFileSync('security-report.json', JSON.stringify(reports));
|
||||
|
||||
if (report.visits.length > 0) {
|
||||
|
||||
await EmailService.sendAnomalyDomainEmail(
|
||||
user.email,
|
||||
project.name,
|
||||
report.dns
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
@@ -92,7 +92,7 @@ class EmailService {
|
||||
sendSmtpEmail.to = [{ "email": target }];
|
||||
sendSmtpEmail.htmlContent = PURCHASE_EMAIL
|
||||
.replace(/\[Project Name\]/, projectName)
|
||||
.toString();;
|
||||
.toString();
|
||||
await this.apiInstance.sendTransacEmail(sendSmtpEmail);
|
||||
return true;
|
||||
} catch (ex) {
|
||||
@@ -101,7 +101,11 @@ class EmailService {
|
||||
}
|
||||
}
|
||||
|
||||
async sendAnomalyVisitsEventsEmail(target: string, projectName: string) {
|
||||
async sendAnomalyVisitsEventsEmail(target: string, projectName: string,
|
||||
data: {
|
||||
visits: { _id: string, count: number }[],
|
||||
events: { _id: string, count: number }[]
|
||||
}) {
|
||||
try {
|
||||
const sendSmtpEmail = new SendSmtpEmail();
|
||||
sendSmtpEmail.subject = "🚨 Unexpected Activity Detected by our AI";
|
||||
@@ -109,7 +113,14 @@ class EmailService {
|
||||
sendSmtpEmail.to = [{ "email": target }];
|
||||
sendSmtpEmail.htmlContent = ANOMALY_VISITS_EVENTS_EMAIL
|
||||
.replace(/\[Project Name\]/, projectName)
|
||||
.toString();;
|
||||
.replace(/\[ENTRIES\]/,
|
||||
[
|
||||
...data.visits.map(e => (`<li> Visits in date ${e._id} [ ${e.count} ] </li>`)),
|
||||
...data.events.map(e => (`<li> Events in date ${e._id} [ ${e.count} ] </li>`))
|
||||
]
|
||||
.join('<br>')
|
||||
)
|
||||
.toString();
|
||||
await this.apiInstance.sendTransacEmail(sendSmtpEmail);
|
||||
return true;
|
||||
} catch (ex) {
|
||||
@@ -118,7 +129,7 @@ class EmailService {
|
||||
}
|
||||
}
|
||||
|
||||
async sendAnomalyDomainEmail(target: string, projectName: string) {
|
||||
async sendAnomalyDomainEmail(target: string, projectName: string, domains: string[]) {
|
||||
try {
|
||||
const sendSmtpEmail = new SendSmtpEmail();
|
||||
sendSmtpEmail.subject = "🚨 Anomaly detected by our AI";
|
||||
@@ -126,7 +137,11 @@ class EmailService {
|
||||
sendSmtpEmail.to = [{ "email": target }];
|
||||
sendSmtpEmail.htmlContent = ANOMALY_DOMAIN_EMAIL
|
||||
.replace(/\[Project Name\]/, projectName)
|
||||
.toString();;
|
||||
.replace(/\[CURRENT_DATE\]/, new Date().toLocaleDateString('en-EN'))
|
||||
.replace(/\[DNS_ENTRIES\]/,
|
||||
domains.map(e => (`<li> ${e} </li>`)).join('<br>')
|
||||
)
|
||||
.toString();
|
||||
await this.apiInstance.sendTransacEmail(sendSmtpEmail);
|
||||
return true;
|
||||
} catch (ex) {
|
||||
|
||||
@@ -14,7 +14,19 @@ export const ANOMALY_DOMAIN_EMAIL = `<!DOCTYPE html>
|
||||
|
||||
<p>We wanted to let you know that <strong>[Project Name]</strong> on <strong>Litlyx</strong> has an anomaly that our AI agent detected.</p>
|
||||
|
||||
<p>You can analyze a suspicious DNS on your Litlyx dashboard. We put a symbol next to each suspicious DNS to let users know something might be wrong!</p>
|
||||
<p> <strong>Anomaly</strong>: Suspicious DNS </p>
|
||||
|
||||
<p> Message: </p>
|
||||
|
||||
<ul>
|
||||
[DNS_ENTRIES]
|
||||
</ul>
|
||||
|
||||
<p> Date: [CURRENT_DATE] </p>
|
||||
|
||||
<p> Are logging data in your project. Is that you? </p>
|
||||
|
||||
<p>You can analyze a suspicious DNS on your Litlyx dashboard. Visit the Security tab to find out more.</p>
|
||||
|
||||
<h3>What can I do?</h3>
|
||||
|
||||
|
||||
@@ -14,6 +14,14 @@ export const ANOMALY_VISITS_EVENTS_EMAIL = `<!DOCTYPE html>
|
||||
|
||||
<p>We wanted to let you know that <strong>[Project Name]</strong> on <strong>Litlyx</strong> is receiving an unexpected amount of visits or events. This could indicate unusual activity that might require your attention.</p>
|
||||
|
||||
<p> <strong>Anomaly</strong>: Unexpected usage </p>
|
||||
|
||||
<p> Message: </p>
|
||||
|
||||
<ul>
|
||||
[ENTRIES]
|
||||
</ul>
|
||||
|
||||
<p>If this spike in activity is expected, there’s no need to worry. However, if you believe this could be unexpected or suspicious, we recommend taking a closer look at your data on the <strong>Litlyx Dashboard</strong>. You can analyze your recent traffic and event logs to identify any irregularities or potential issues.</p>
|
||||
|
||||
<h3>What can I do?</h3>
|
||||
|
||||
@@ -18,7 +18,7 @@ export const WELCOME_EMAIL = `
|
||||
<ol>
|
||||
<li><strong><a href="https://dashboard.litlyx.com" style="color: #007BFF; text-decoration: none;">Create a new project</a></strong> – by just naming it</li>
|
||||
<li><strong><a style="color: #0a0a0a; text-decoration: none;">Copy the universal Script</a></strong> – we provide you the snippets to copy in your index.html file and start instantly to track metrics on your website or web app.</li>
|
||||
<li><strong><a style="color: #0a0a0a; text-decoration: none;">Third Step</a></strong> – Encourage engagement or interaction.</li>
|
||||
<li><strong><a style="color: #0a0a0a; text-decoration: none;">Deploy</a></strong> – Litlyx is production ready.</li>
|
||||
</ol>
|
||||
|
||||
<p>If you have any questions or need support, visit <a href="http://docs.litlyx.com" style="color: #007BFF;">docs.litlyx.com</a>.</p>
|
||||
|
||||
Reference in New Issue
Block a user