update security + chart events è anomaly service

This commit is contained in:
Emily
2024-09-24 16:48:23 +02:00
parent 5b7e93bcbb
commit f5edf187fd
18 changed files with 1417 additions and 75 deletions

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

View File

@@ -1,12 +1,7 @@
import { Chart, registerables } from 'chart.js';
import annotaionPlugin from 'chartjs-plugin-annotation';
let registered = false; let registered = false;
export async function registerChartComponents() { export async function registerChartComponents() {
if (registered) return; if (registered) return;
if (process.client) { console.log('registerChartComponents is deprecated. Plugin is now used');
Chart.register(...registerables, annotaionPlugin);
registered = true; registered = true;
}
} }

View File

@@ -12,7 +12,7 @@ export default defineNuxtConfig({
postcss: { postcss: {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {} autoprefixer: {},
} }
}, },
colorMode: { colorMode: {
@@ -60,6 +60,9 @@ export default defineNuxtConfig({
nitro: { nitro: {
plugins: ['~/server/init.ts'] plugins: ['~/server/init.ts']
}, },
plugins: [
{ src: '~/plugins/chartjs.ts', mode: 'client' }
],
...gooleSignInConfig, ...gooleSignInConfig,
modules: ['@nuxt/ui', 'nuxt-vue3-google-signin'], modules: ['@nuxt/ui', 'nuxt-vue3-google-signin'],
devServer: { devServer: {

View File

@@ -16,6 +16,7 @@
"@getbrevo/brevo": "^2.2.0", "@getbrevo/brevo": "^2.2.0",
"@nuxtjs/tailwindcss": "^6.12.0", "@nuxtjs/tailwindcss": "^6.12.0",
"chart.js": "^3.9.1", "chart.js": "^3.9.1",
"chartjs-chart-funnel": "^4.2.1",
"chartjs-plugin-annotation": "^2.2.1", "chartjs-plugin-annotation": "^2.2.1",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"dayjs": "^1.11.11", "dayjs": "^1.11.11",

View File

@@ -1,4 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import EventsFunnelChart from '~/components/events/EventsFunnelChart.vue';
definePageMeta({ layout: 'dashboard' }); 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"> <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> <template #header>
<SelectButton @changeIndex="eventsStackedSelectIndex = $event" <SelectButton @changeIndex="eventsStackedSelectIndex = $event"
:currentIndex="eventsStackedSelectIndex" :options="selectLabelsEvents"> :currentIndex="eventsStackedSelectIndex" :options="selectLabelsEvents">
@@ -41,6 +44,10 @@ const refreshKey = computed(() => `${snapshot.value._id.toString() + activeProje
</div> </div>
<div class="flex">
<EventsFunnelChart :key="refreshKey" class="w-full"></EventsFunnelChart>
</div>
<div class="flex"> <div class="flex">
<EventsUserFlow :key="refreshKey"></EventsUserFlow> <EventsUserFlow :key="refreshKey"></EventsUserFlow>
</div> </div>

View File

@@ -20,6 +20,10 @@ const limitsInfo = ref<{
percent: number percent: number
}>(); }>();
const justLogged = computed(() => {
return route.query.just_logged;
})
onMounted(async () => { onMounted(async () => {
if (route.query.just_logged) return location.href = '/'; 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 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 class="w-full px-4 py-2 gap-2 flex flex-col">
<div v-if="limitsInfo && limitsInfo.limited" <div v-if="limitsInfo && limitsInfo.limited"
@@ -188,13 +193,17 @@ function goToUpgrade() {
</div> </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 && !justLogged">
<div class="text-text/85 mt-8 ml-8 poppis text-[1.2rem]" v-if="projects && projects.length == 0">
Create your first project... Create your first project...
</div> </div>
<div v-if="justLogged" class="text-[2rem]">
The page will refresh soon
</div>
</div> </div>
</template> </template>

View File

@@ -1,5 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { } from '#ui/types/tabs'
definePageMeta({ layout: 'dashboard' }); definePageMeta({ layout: 'dashboard' });
const activeProjectId = useActiveProjectId(); 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> </script>
@@ -41,35 +52,72 @@ function showAnomalyInfoAlert() {
</div> </div>
</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-if="reportList.data.value" class="flex flex-col gap-2">
<div v-for="entry of reportList.data.value"> <div v-for="entry of reportList.data.value" class="flex flex-col gap-4">
<div v-if="entry.type === 'event'" class="flex gap-2"> <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> <div class="text-lyx-text-darker">{{ new Date(entry.data.created_at).toLocaleString() }}</div>
<UBadge class="w-[4rem] flex justify-center"> {{ entry.type }} </UBadge> <UBadge class="w-[4rem] flex justify-center"> {{ entry.type }} </UBadge>
<div class="text-lyx-text-dark"> <div class="text-lyx-text-dark">
Event date: {{ new Date(entry.data.eventDate).toLocaleString() }} Event date: {{ new Date(entry.data.eventDate).toLocaleString() }}
</div> </div>
</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> <div class="text-lyx-text-darker">{{ new Date(entry.data.created_at).toLocaleString() }}</div>
<UBadge class="w-[4rem] flex justify-center"> {{ entry.type }} </UBadge> <UBadge class="w-[4rem] flex justify-center"> {{ entry.type }} </UBadge>
<div class="text-lyx-text-dark"> <div class="text-lyx-text-dark">
Visit date: {{ new Date(entry.data.visitDate).toLocaleString() }} Visit date: {{ new Date(entry.data.visitDate).toLocaleString() }}
</div> </div>
</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> <div class="text-lyx-text-darker">{{ new Date(entry.data.created_at).toLocaleString() }}</div>
<UBadge class="w-[4rem] flex justify-center"> {{ entry.type }} </UBadge> <UBadge class="w-[4rem] flex justify-center"> {{ entry.type }} </UBadge>
<div class="text-lyx-text-dark"> <div class="text-lyx-text-dark">
Domain found: {{ entry.data.domain }} {{ entry.data.domain }}
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> -->
</div> </div>

View 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);
})

View File

@@ -17,6 +17,9 @@ importers:
chart.js: chart.js:
specifier: ^3.9.1 specifier: ^3.9.1
version: 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: chartjs-plugin-annotation:
specifier: ^2.2.1 specifier: ^2.2.1
version: 2.2.1(chart.js@3.9.1) version: 2.2.1(chart.js@3.9.1)
@@ -1181,6 +1184,9 @@ packages:
'@types/argparse@1.0.38': '@types/argparse@1.0.38':
resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} 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': '@types/estree@1.0.5':
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
@@ -1462,20 +1468,20 @@ packages:
'@vue/reactivity@3.4.27': '@vue/reactivity@3.4.27':
resolution: {integrity: sha512-kK0g4NknW6JX2yySLpsm2jlunZJl2/RJGZ0H9ddHdfBVHcNzxmQ0sS0b09ipmBoQpY8JM2KmUw+a6sO8Zo+zIA==} resolution: {integrity: sha512-kK0g4NknW6JX2yySLpsm2jlunZJl2/RJGZ0H9ddHdfBVHcNzxmQ0sS0b09ipmBoQpY8JM2KmUw+a6sO8Zo+zIA==}
'@vue/reactivity@3.5.7': '@vue/reactivity@3.5.8':
resolution: {integrity: sha512-yF0EpokpOHRNXyn/h6abXc9JFIzfdAf0MJHIi92xxCWS0mqrXH6+2aZ+A6EbSrspGzX5MHTd5N8iBA28HnXu9g==} resolution: {integrity: sha512-mlgUyFHLCUZcAYkqvzYnlBRCh0t5ZQfLYit7nukn1GR96gc48Bp4B7OIcSfVSvlG1k3BPfD+p22gi1t2n9tsXg==}
'@vue/runtime-core@3.4.27': '@vue/runtime-core@3.4.27':
resolution: {integrity: sha512-7aYA9GEbOOdviqVvcuweTLe5Za4qBZkUY7SvET6vE8kyypxVgaT1ixHLg4urtOlrApdgcdgHoTZCUuTGap/5WA==} resolution: {integrity: sha512-7aYA9GEbOOdviqVvcuweTLe5Za4qBZkUY7SvET6vE8kyypxVgaT1ixHLg4urtOlrApdgcdgHoTZCUuTGap/5WA==}
'@vue/runtime-core@3.5.7': '@vue/runtime-core@3.5.8':
resolution: {integrity: sha512-OzLpBpKbZEaZVSNfd+hQbfBrDKux+b7Yl5hYhhWWWhHD7fEpF+CdI3Brm5k5GsufHEfvMcjruPxwQZuBN6nFYQ==} resolution: {integrity: sha512-fJuPelh64agZ8vKkZgp5iCkPaEqFJsYzxLk9vSC0X3G8ppknclNDr61gDc45yBGTaN5Xqc1qZWU3/NoaBMHcjQ==}
'@vue/runtime-dom@3.4.27': '@vue/runtime-dom@3.4.27':
resolution: {integrity: sha512-ScOmP70/3NPM+TW9hvVAz6VWWtZJqkbdf7w6ySsws+EsqtHvkhxaWLecrTorFxsawelM5Ys9FnDEMt6BPBDS0Q==} resolution: {integrity: sha512-ScOmP70/3NPM+TW9hvVAz6VWWtZJqkbdf7w6ySsws+EsqtHvkhxaWLecrTorFxsawelM5Ys9FnDEMt6BPBDS0Q==}
'@vue/runtime-dom@3.5.7': '@vue/runtime-dom@3.5.8':
resolution: {integrity: sha512-fL7cETfE27U2jyTgqzE382IGFY6a6uyznErn27KbbEzNctzxxUWYDbaN3B55l9nXh0xW2LRWPuWKOvjtO2UewQ==} resolution: {integrity: sha512-DpAUz+PKjTZPUOB6zJgkxVI3GuYc2iWZiNeeHQUw53kdrparSTG6HeXUrYDjaam8dVsCdvQxDz6ZWxnyjccUjQ==}
'@vue/server-renderer@3.4.27': '@vue/server-renderer@3.4.27':
resolution: {integrity: sha512-dlAMEuvmeA3rJsOMJ2J1kXU7o7pOxgsNHVr9K8hB3ImIkSuBrIdy0vF66h8gf8Tuinf1TK3mPAz2+2sqyf3KzA==} resolution: {integrity: sha512-dlAMEuvmeA3rJsOMJ2J1kXU7o7pOxgsNHVr9K8hB3ImIkSuBrIdy0vF66h8gf8Tuinf1TK3mPAz2+2sqyf3KzA==}
@@ -1485,12 +1491,12 @@ packages:
'@vue/shared@3.4.27': '@vue/shared@3.4.27':
resolution: {integrity: sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==} resolution: {integrity: sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==}
'@vue/shared@3.5.6':
resolution: {integrity: sha512-eidH0HInnL39z6wAt6SFIwBrvGOpDWsDxlw3rCgo1B+CQ1781WzQUSU3YjxgdkcJo9Q8S6LmXTkvI+cLHGkQfA==}
'@vue/shared@3.5.7': '@vue/shared@3.5.7':
resolution: {integrity: sha512-NBE1PBIvzIedxIc2RZiKXvGbJkrZ2/hLf3h8GlS4/sP9xcXEZMFWOazFkNd6aGeUCMaproe5MHVYB3/4AW9q9g==} 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': '@vueuse/components@10.10.0':
resolution: {integrity: sha512-HiA10NQ9HJAGnju+8ZK4TyA8LIc0a6BnJmVWDa/k+TRhaYCVacSDU04k0BQ2otV+gghUDdwu98upf6TDRXpoeg==} resolution: {integrity: sha512-HiA10NQ9HJAGnju+8ZK4TyA8LIc0a6BnJmVWDa/k+TRhaYCVacSDU04k0BQ2otV+gghUDdwu98upf6TDRXpoeg==}
@@ -1863,6 +1869,11 @@ packages:
chart.js@3.9.1: chart.js@3.9.1:
resolution: {integrity: sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w==} 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: chartjs-plugin-annotation@2.2.1:
resolution: {integrity: sha512-RL9UtrFr2SXd7C47zD0MZqn6ZLgrcRt3ySC6cYal2amBdANcYB1QcwFXcpKWAYnO4SGJYRok7P5rKDDNgJMA/w==} resolution: {integrity: sha512-RL9UtrFr2SXd7C47zD0MZqn6ZLgrcRt3ySC6cYal2amBdANcYB1QcwFXcpKWAYnO4SGJYRok7P5rKDDNgJMA/w==}
peerDependencies: peerDependencies:
@@ -1879,6 +1890,9 @@ packages:
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
chroma-js@2.6.0:
resolution: {integrity: sha512-BLHvCB9s8Z1EV4ethr6xnkl/P2YRFOGqfgvuMG/MyCbZPrTA+NeiByY6XvgF0zP4/2deU2CXnWyMa3zu1LqQ3A==}
ci-info@4.0.0: ci-info@4.0.0:
resolution: {integrity: sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==} resolution: {integrity: sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -6661,6 +6675,8 @@ snapshots:
'@types/argparse@1.0.38': {} '@types/argparse@1.0.38': {}
'@types/chroma-js@2.4.4': {}
'@types/estree@1.0.5': {} '@types/estree@1.0.5': {}
'@types/http-proxy@1.17.14': '@types/http-proxy@1.17.14':
@@ -7136,7 +7152,7 @@ snapshots:
'@volar/language-core': 1.11.1 '@volar/language-core': 1.11.1
'@volar/source-map': 1.11.1 '@volar/source-map': 1.11.1
'@vue/compiler-dom': 3.4.27 '@vue/compiler-dom': 3.4.27
'@vue/shared': 3.5.6 '@vue/shared': 3.5.7
computeds: 0.0.1 computeds: 0.0.1
minimatch: 9.0.4 minimatch: 9.0.4
muggle-string: 0.3.1 muggle-string: 0.3.1
@@ -7149,19 +7165,19 @@ snapshots:
dependencies: dependencies:
'@vue/shared': 3.4.27 '@vue/shared': 3.4.27
'@vue/reactivity@3.5.7': '@vue/reactivity@3.5.8':
dependencies: dependencies:
'@vue/shared': 3.5.7 '@vue/shared': 3.5.8
'@vue/runtime-core@3.4.27': '@vue/runtime-core@3.4.27':
dependencies: dependencies:
'@vue/reactivity': 3.4.27 '@vue/reactivity': 3.4.27
'@vue/shared': 3.4.27 '@vue/shared': 3.4.27
'@vue/runtime-core@3.5.7': '@vue/runtime-core@3.5.8':
dependencies: dependencies:
'@vue/reactivity': 3.5.7 '@vue/reactivity': 3.5.8
'@vue/shared': 3.5.7 '@vue/shared': 3.5.8
'@vue/runtime-dom@3.4.27': '@vue/runtime-dom@3.4.27':
dependencies: dependencies:
@@ -7169,11 +7185,11 @@ snapshots:
'@vue/shared': 3.4.27 '@vue/shared': 3.4.27
csstype: 3.1.3 csstype: 3.1.3
'@vue/runtime-dom@3.5.7': '@vue/runtime-dom@3.5.8':
dependencies: dependencies:
'@vue/reactivity': 3.5.7 '@vue/reactivity': 3.5.8
'@vue/runtime-core': 3.5.7 '@vue/runtime-core': 3.5.8
'@vue/shared': 3.5.7 '@vue/shared': 3.5.8
csstype: 3.1.3 csstype: 3.1.3
'@vue/server-renderer@3.4.27(vue@3.4.27(typescript@5.4.2))': '@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.4.27': {}
'@vue/shared@3.5.6': {}
'@vue/shared@3.5.7': {} '@vue/shared@3.5.7': {}
'@vue/shared@3.5.8': {}
'@vueuse/components@10.10.0(vue@3.4.27(typescript@5.4.2))': '@vueuse/components@10.10.0(vue@3.4.27(typescript@5.4.2))':
dependencies: dependencies:
'@vueuse/core': 10.10.0(vue@3.4.27(typescript@5.4.2)) '@vueuse/core': 10.10.0(vue@3.4.27(typescript@5.4.2))
@@ -7575,6 +7591,12 @@ snapshots:
chart.js@3.9.1: {} 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): chartjs-plugin-annotation@2.2.1(chart.js@3.9.1):
dependencies: dependencies:
chart.js: 3.9.1 chart.js: 3.9.1
@@ -7597,6 +7619,8 @@ snapshots:
chownr@2.0.0: {} chownr@2.0.0: {}
chroma-js@2.6.0: {}
ci-info@4.0.0: {} ci-info@4.0.0: {}
citty@0.1.6: 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)): vue-chart-3@3.1.8(chart.js@3.9.1)(vue@3.4.27(typescript@5.4.2)):
dependencies: dependencies:
'@vue/runtime-core': 3.5.7 '@vue/runtime-core': 3.5.8
'@vue/runtime-dom': 3.5.7 '@vue/runtime-dom': 3.5.8
chart.js: 3.9.1 chart.js: 3.9.1
csstype: 3.1.3 csstype: 3.1.3
lodash-es: 4.17.21 lodash-es: 4.17.21

View File

@@ -38,7 +38,7 @@ export default defineEventHandler(async event => {
report.push({ type: 'domain', data: domain }); 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());
}); });

View File

@@ -1,5 +1,6 @@
{ {
"dependencies": { "dependencies": {
"@getbrevo/brevo": "^2.2.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"mongoose": "^8.3.2" "mongoose": "^8.3.2"
}, },

1047
security/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,10 +4,9 @@ import { AnomalyVisitModel } from '@schema/anomalies/AnomalyVisitSchema';
import { AnomalyEventsModel } from '@schema/anomalies/AnomalyEventsSchema'; import { AnomalyEventsModel } from '@schema/anomalies/AnomalyEventsSchema';
import { EventModel } from "@schema/metrics/EventSchema"; import { EventModel } from "@schema/metrics/EventSchema";
import { VisitModel } from '@schema/metrics/VisitSchema' import { VisitModel } from '@schema/metrics/VisitSchema'
import EmailService from "@services/EmailService";
import * as url from 'url'; import * as url from 'url';
import { ProjectModel } from "@schema/ProjectSchema"; import { ProjectModel } from "@schema/ProjectSchema";
import { UserModel } from "@schema/UserSchema";
import { getAggregation } from "./Aggregations"; import { getAggregation } from "./Aggregations";
type TAvgInput = { _id: string, count: number } type TAvgInput = { _id: string, count: number }
@@ -112,7 +111,7 @@ export async function findAnomalies(project_id: string, callback: AnomalyCallbac
visits: [], visits: [],
events: [], events: [],
dns: [], dns: [],
pid: project_id pid: project_id,
} }
for (const visit of visitAnomalies) { for (const visit of visitAnomalies) {
@@ -136,19 +135,6 @@ export async function findAnomalies(project_id: string, callback: AnomalyCallbac
report.dns.push(website); 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); callback(report);
return report; return report;

View File

@@ -1,18 +1,46 @@
import { anomalyCheckAll, AnomalyReport } from "./AnomalyService"; import { anomalyCheckAll, AnomalyReport, findAnomalies } from "./AnomalyService";
import { connectDatabase } from '@services/DatabaseService' import { connectDatabase } from '@services/DatabaseService'
import { requireEnv } from '@utils/requireEnv' 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')); 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) { 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
);
}
}
}); });

View File

@@ -92,7 +92,7 @@ class EmailService {
sendSmtpEmail.to = [{ "email": target }]; sendSmtpEmail.to = [{ "email": target }];
sendSmtpEmail.htmlContent = PURCHASE_EMAIL sendSmtpEmail.htmlContent = PURCHASE_EMAIL
.replace(/\[Project Name\]/, projectName) .replace(/\[Project Name\]/, projectName)
.toString();; .toString();
await this.apiInstance.sendTransacEmail(sendSmtpEmail); await this.apiInstance.sendTransacEmail(sendSmtpEmail);
return true; return true;
} catch (ex) { } 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 { try {
const sendSmtpEmail = new SendSmtpEmail(); const sendSmtpEmail = new SendSmtpEmail();
sendSmtpEmail.subject = "🚨 Unexpected Activity Detected by our AI"; sendSmtpEmail.subject = "🚨 Unexpected Activity Detected by our AI";
@@ -109,7 +113,14 @@ class EmailService {
sendSmtpEmail.to = [{ "email": target }]; sendSmtpEmail.to = [{ "email": target }];
sendSmtpEmail.htmlContent = ANOMALY_VISITS_EVENTS_EMAIL sendSmtpEmail.htmlContent = ANOMALY_VISITS_EVENTS_EMAIL
.replace(/\[Project Name\]/, projectName) .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); await this.apiInstance.sendTransacEmail(sendSmtpEmail);
return true; return true;
} catch (ex) { } catch (ex) {
@@ -118,7 +129,7 @@ class EmailService {
} }
} }
async sendAnomalyDomainEmail(target: string, projectName: string) { async sendAnomalyDomainEmail(target: string, projectName: string, domains: string[]) {
try { try {
const sendSmtpEmail = new SendSmtpEmail(); const sendSmtpEmail = new SendSmtpEmail();
sendSmtpEmail.subject = "🚨 Anomaly detected by our AI"; sendSmtpEmail.subject = "🚨 Anomaly detected by our AI";
@@ -126,7 +137,11 @@ class EmailService {
sendSmtpEmail.to = [{ "email": target }]; sendSmtpEmail.to = [{ "email": target }];
sendSmtpEmail.htmlContent = ANOMALY_DOMAIN_EMAIL sendSmtpEmail.htmlContent = ANOMALY_DOMAIN_EMAIL
.replace(/\[Project Name\]/, projectName) .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); await this.apiInstance.sendTransacEmail(sendSmtpEmail);
return true; return true;
} catch (ex) { } catch (ex) {

View File

@@ -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>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> <h3>What can I do?</h3>

View File

@@ -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>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, theres 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> <p>If this spike in activity is expected, theres 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> <h3>What can I do?</h3>

View File

@@ -18,7 +18,7 @@ export const WELCOME_EMAIL = `
<ol> <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 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;">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> </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> <p>If you have any questions or need support, visit <a href="http://docs.litlyx.com" style="color: #007BFF;">docs.litlyx.com</a>.</p>