add secutiry

This commit is contained in:
Emily
2024-09-19 15:44:27 +02:00
parent ac7ba7abd3
commit f285e92132
13 changed files with 988 additions and 43 deletions

View File

@@ -31,7 +31,7 @@ function showAnomalyInfoAlert() {
<template>
<div
class="w-full px-6 py-2 lg:py-6 font-bold text-text-sub/40 flex flex-col xl:flex-row text-lg lg:text-2xl gap-2 xl:gap-12">
class="w-full px-6 py-2 lg:py-6 font-bold text-text-sub/40 flex flex-col xl:flex-row text-lg gap-2 xl:gap-12 lg:text-2xl">
<div class="flex gap-2 items-center text-text/90 justify-center md:justify-start">
<div class="animate-pulse w-[1rem] h-[1rem] bg-green-400 rounded-full"> </div>

View File

@@ -21,6 +21,7 @@ const sections: Section[] = [
{ label: 'Insights (soon)', to: '#', icon: 'fal fa-lightbulb', disabled: true },
{ label: 'Links (soon)', to: '#', icon: 'fal fa-globe-pointer', disabled: true },
{ label: 'Integrations (soon)', to: '#', icon: 'fal fa-cube', disabled: true },
{ label: 'Security', to: '/security', icon: 'fal fa-lock' },
{ label: 'Settings', to: '/settings', icon: 'fal fa-gear' },
{
grow: true,
@@ -29,7 +30,7 @@ const sections: Section[] = [
},
{
label: 'Slack support', icon: 'fab fa-slack',
to:'#',
to: '#',
premiumOnly: true,
action() {
if (isGuest.value === true) return;

View File

@@ -0,0 +1,50 @@
<script setup lang="ts">
definePageMeta({ layout: 'dashboard' });
const activeProjectId = useActiveProjectId();
const headers = computed(() => {
return {
'Authorization': authorizationHeaderComputed.value,
'x-pid': activeProjectId.data.value || ''
}
});
const reportList = useFetch(`/api/security/list`, { headers });
</script>
<template>
<div class="home w-full h-full px-10 lg:px-0 pt-6 overflow-y-auto">
<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 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 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 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>
</div>
</div>
</div>
</div>
</template>

View File

@@ -1,7 +1,7 @@
import { ProjectModel, TProject } from "@schema/ProjectSchema";
import { ProjectLimitModel } from "@schema/ProjectsLimits";
import { UserSettingsModel } from "@schema/UserSettings";
import { EVENT_LOG_LIMIT_PERCENT } from '@data/broker/Limits';
import { MAX_LOG_LIMIT_PERCENT } from '@data/broker/Limits';
export default defineEventHandler(async event => {
@@ -25,8 +25,8 @@ export default defineEventHandler(async event => {
return {
total: TOTAL_COUNT,
limit: COUNT_LIMIT,
maxLimit: Math.round(COUNT_LIMIT * EVENT_LOG_LIMIT_PERCENT),
limited: TOTAL_COUNT > COUNT_LIMIT * EVENT_LOG_LIMIT_PERCENT,
maxLimit: Math.round(COUNT_LIMIT * MAX_LOG_LIMIT_PERCENT),
limited: TOTAL_COUNT > COUNT_LIMIT * MAX_LOG_LIMIT_PERCENT,
percent: Math.round(100 / COUNT_LIMIT * TOTAL_COUNT)
}

View File

@@ -0,0 +1,44 @@
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
import { AnomalyDomainModel } from '@schema/anomalies/AnomalyDomainSchema';
import { AnomalyEventsModel } from '@schema/anomalies/AnomalyEventsSchema';
import { AnomalyVisitModel } from '@schema/anomalies/AnomalyVisitSchema';
type TSecurityDomainEntry = { type: 'domain', data: { domain: string, created_at: Date } }
type TSecurityVisitEntry = { type: 'visit', data: { visitDate: Date, created_at: Date } }
type TSecurityEventEntry = { type: 'event', data: { eventDate: Date, created_at: Date } }
export type SecutityReport = (TSecurityDomainEntry | TSecurityVisitEntry | TSecurityEventEntry)[];
export default defineEventHandler(async event => {
const project_id = getHeader(event, 'x-pid');
if (!project_id) return;
const user = getRequestUser(event);
const project = await getUserProjectFromId(project_id, user);
if (!project) return;
const visits = await AnomalyVisitModel.find({ project_id }, { _id: 0, project_id: 0 });
const events = await AnomalyEventsModel.find({ project_id }, { _id: 0, project_id: 0 });
const domains = await AnomalyDomainModel.find({ project_id }, { _id: 0, project_id: 0 });
const report: SecutityReport = [];
for (const visit of visits) {
report.push({ type: 'visit', data: visit });
}
for (const event of events) {
report.push({ type: 'event', data: event });
}
for (const domain of domains) {
report.push({ type: 'domain', data: domain });
}
return report.toSorted((a, b) => a.data.created_at.getTime() - b.data.created_at.getTime());
});

View File

@@ -1,152 +0,0 @@
import mongoose from "mongoose";
import { executeTimelineAggregation } from "./TimelineService";
import { VisitModel } from "@schema/metrics/VisitSchema";
import { AnomalyDomainModel } from '@schema/anomalies/AnomalyDomainSchema';
import { AnomalyVisitModel } from '@schema/anomalies/AnomalyVisitSchema';
import { AnomalyEventsModel } from '@schema/anomalies/AnomalyEventsSchema';
import { EventModel } from "@schema/metrics/EventSchema";
import EmailService from "@services/EmailService";
import * as url from 'url';
import { ProjectModel } from "@schema/ProjectSchema";
import { UserModel } from "@schema/UserSchema";
type TAvgInput = { _id: string, count: number }
const anomalyData = { minutes: 0 }
async function anomalyCheckAll() {
const start = performance.now();
console.log('[ANOMALY] START ANOMALY CHECK');
const projects = await ProjectModel.find({}, { _id: 1 });
for (const project of projects) {
await findAnomalies(project.id);
}
const end = performance.now() - start;
console.log('END ANOMALY CHECK', end, 'ms');
}
export function anomalyLoop() {
if (anomalyData.minutes == 60 * 12) {
anomalyCheckAll();
anomalyData.minutes = 0;
}
anomalyData.minutes++;
setTimeout(() => anomalyLoop(), 1000 * 60);
}
function movingAverageAnomaly(visits: TAvgInput[], windowSize: number, threshold: number): TAvgInput[] {
const anomalies: TAvgInput[] = [];
for (let i = windowSize; i < visits.length; i++) {
const window = visits.slice(i - windowSize, i);
const mean = window.reduce((a, b) => a + b.count, 0) / window.length;
const stdDev = Math.sqrt(window.reduce((sum, visit) => sum + Math.pow(visit.count - mean, 2), 0) / window.length);
const currentVisit = visits[i];
if (Math.abs(currentVisit.count - mean) > threshold * stdDev) {
if (currentVisit.count <= mean) continue;
anomalies.push(currentVisit);
}
}
return anomalies;
}
function getUrlFromString(str: string) {
const res = str.startsWith('http') ? str : 'http://' + str;
return res;
}
export async function findAnomalies(project_id: string) {
const THRESHOLD = 6;
const WINDOW_SIZE = 14;
const pid = new mongoose.Types.ObjectId(project_id) as any;
const from = Date.now() - 1000 * 60 * 60 * 24 * 30;
const to = Date.now() - 1000 * 60 * 60 * 24;
const visitsTimelineData = await executeTimelineAggregation({
projectId: pid,
model: VisitModel,
from, to, slice: 'day'
});
const eventsTimelineData = await executeTimelineAggregation({
projectId: pid,
model: EventModel,
from, to, slice: 'day'
});
const websites: { _id: string, count: number }[] = await VisitModel.aggregate([
{ $match: { project_id: pid, created_at: { $gte: new Date(from), $lte: new Date(to) } }, },
{ $group: { _id: "$website", count: { $sum: 1, } } }
]);
const detectedWebsites: string[] = [];
if (websites.length > 0) {
const rootWebsite = websites.reduce((a, e) => {
return a.count > e.count ? a : e;
});
const rootDomain = new url.URL(getUrlFromString(rootWebsite._id)).hostname;
for (const website of websites) {
const websiteDomain = new url.URL(getUrlFromString(website._id)).hostname;
if (websiteDomain === 'localhost') continue;
if (websiteDomain === '127.0.0.1') continue;
if (websiteDomain === '0.0.0.0') continue;
if (!websiteDomain.includes(rootDomain)) { detectedWebsites.push(website._id); }
}
}
const visitAnomalies = movingAverageAnomaly(visitsTimelineData, WINDOW_SIZE, THRESHOLD);
const eventAnomalies = movingAverageAnomaly(eventsTimelineData, WINDOW_SIZE, THRESHOLD);
const shouldSendMail = {
visitsEvents: false,
domains: false
}
for (const visit of visitAnomalies) {
const anomalyAlreadyExist = await AnomalyVisitModel.findOne({ visitDate: visit._id }, { _id: 1 });
if (anomalyAlreadyExist) continue;
await AnomalyVisitModel.create({ project_id: pid, visitDate: visit._id, created_at: Date.now() });
shouldSendMail.visitsEvents = true;
}
for (const event of eventAnomalies) {
const anomalyAlreadyExist = await AnomalyEventsModel.findOne({ eventDate: event._id }, { _id: 1 });
if (anomalyAlreadyExist) continue;
await AnomalyEventsModel.create({ project_id: pid, eventDate: event._id, created_at: Date.now() });
shouldSendMail.visitsEvents = true;
}
for (const website of detectedWebsites) {
const anomalyAlreadyExist = await AnomalyDomainModel.findOne({ domain: website }, { _id: 1 });
if (anomalyAlreadyExist) continue;
await AnomalyDomainModel.create({ project_id: pid, domain: website, created_at: Date.now() });
shouldSendMail.domains = true;
}
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);
}
return { ok: true };
}