From 9f2d306903a08ae944070bd5728de14212c48a62 Mon Sep 17 00:00:00 2001 From: nikitita Date: Thu, 8 Oct 2026 12:42:10 +0200 Subject: [PATCH] refactor backend structure and add Drizzle(36) --- backend/drizzle.config.ts | 37 + backend/drizzle/0000_init.sql | 151 ++ backend/drizzle/hvaErDrizzle.md | 1 - backend/drizzle/meta/0000_snapshot.json | 1032 ++++++++ backend/drizzle/meta/_journal.json | 13 + backend/package.json | 2 + backend/pnpm-lock.yaml | 666 +++++ backend/src/db/connection.ts | 26 + backend/src/db/schema.ts | 242 ++ backend/src/graphql/resolvers/index.ts | 23 + .../src/graphql/resolvers/softwareResolver.ts | 12 + .../src/graphql/resolvers/tacticResolver.ts | 12 + .../graphql/resolvers/techniqueResolver.ts | 12 + .../graphql/resolvers/threatActorResolver.ts | 46 + backend/src/graphql/typeDefs.ts | 28 + backend/src/index.ts | 187 +- .../src/repositories/softwareRepository.ts | 16 + backend/src/repositories/tacticRepository.ts | 16 + .../src/repositories/techniqueRepository.ts | 17 + .../src/repositories/threatActorRepository.ts | 86 + backend/src/test-db.ts | 25 - database/import-mitre.ts | 2132 ++++++++++------- docs/local-database-setup.md | 53 +- pnpm-lock.yaml | 668 +++++- 24 files changed, 4483 insertions(+), 1020 deletions(-) create mode 100644 backend/drizzle.config.ts create mode 100644 backend/drizzle/0000_init.sql delete mode 100644 backend/drizzle/hvaErDrizzle.md create mode 100644 backend/drizzle/meta/0000_snapshot.json create mode 100644 backend/drizzle/meta/_journal.json create mode 100644 backend/src/db/connection.ts create mode 100644 backend/src/db/schema.ts create mode 100644 backend/src/graphql/resolvers/index.ts create mode 100644 backend/src/graphql/resolvers/softwareResolver.ts create mode 100644 backend/src/graphql/resolvers/tacticResolver.ts create mode 100644 backend/src/graphql/resolvers/techniqueResolver.ts create mode 100644 backend/src/graphql/resolvers/threatActorResolver.ts create mode 100644 backend/src/graphql/typeDefs.ts create mode 100644 backend/src/repositories/softwareRepository.ts create mode 100644 backend/src/repositories/tacticRepository.ts create mode 100644 backend/src/repositories/techniqueRepository.ts create mode 100644 backend/src/repositories/threatActorRepository.ts delete mode 100644 backend/src/test-db.ts diff --git a/backend/drizzle.config.ts b/backend/drizzle.config.ts new file mode 100644 index 0000000..57c2807 --- /dev/null +++ b/backend/drizzle.config.ts @@ -0,0 +1,37 @@ +/** + * Denne fila forteller Drizzle: + * *Hvilken DB vi bruker + * *Hvor skjemaet for DB ligger (schema.ts) + * *Hvordan drizzle kobler seg til SQL + */ +import process from "node:process"; +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + /** + * Databasen vår er MySQL. + */ + dialect: "mysql", + + /** + * Her ligger Drizzle-definisjonen av tabellene våre. + */ + schema: "./src/db/schema.ts", + + /** + * Her skal Drizzle legge genererte migrations. + */ + out: "./drizzle", + + /** + * Samme databaseinformasjon som vi bruker + * i connection.ts. + */ + dbCredentials: { + host: process.env.DB_HOST ?? "localhost", + port: Number(process.env.DB_PORT ?? 3306), + user: process.env.DB_USER ?? "cyberuser", + password: process.env.DB_PASSWORD ?? "cyberpass", + database: process.env.DB_NAME ?? "cyberdb", + }, +}); \ No newline at end of file diff --git a/backend/drizzle/0000_init.sql b/backend/drizzle/0000_init.sql new file mode 100644 index 0000000..f6eeea7 --- /dev/null +++ b/backend/drizzle/0000_init.sql @@ -0,0 +1,151 @@ +CREATE TABLE `SavedItem` ( + `user_id` varchar(100) NOT NULL, + `item_id` varchar(100) NOT NULL, + `item_type` enum('THREAT_ACTOR','TECHNIQUE','SOFTWARE') NOT NULL, + `saved_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + CONSTRAINT `SavedItem_user_id_item_id_item_type_pk` PRIMARY KEY(`user_id`,`item_id`,`item_type`) +); +--> statement-breakpoint +CREATE TABLE `Software` ( + `software_id` varchar(100) NOT NULL, + `mitre_id` varchar(20) NOT NULL, + `name` varchar(255) NOT NULL, + `description` longtext NOT NULL, + `software_type` enum('MALWARE','TOOL') NOT NULL, + `software_entry_created` datetime(3) NOT NULL, + `x_mitre_deprecated` boolean NOT NULL DEFAULT false, + CONSTRAINT `Software_software_id` PRIMARY KEY(`software_id`), + CONSTRAINT `Software_mitre_id_unique` UNIQUE(`mitre_id`) +); +--> statement-breakpoint +CREATE TABLE `SoftwareAlias` ( + `software_id` varchar(100) NOT NULL, + `alias` varchar(255) NOT NULL, + CONSTRAINT `SoftwareAlias_software_id_alias_pk` PRIMARY KEY(`software_id`,`alias`) +); +--> statement-breakpoint +CREATE TABLE `SoftwarePlatform` ( + `software_id` varchar(100) NOT NULL, + `platform` varchar(100) NOT NULL, + CONSTRAINT `SoftwarePlatform_software_id_platform_pk` PRIMARY KEY(`software_id`,`platform`) +); +--> statement-breakpoint +CREATE TABLE `Tactic` ( + `tactic_id` varchar(100) NOT NULL, + `mitre_id` varchar(20) NOT NULL, + `name` varchar(255) NOT NULL, + `description` longtext NOT NULL, + `tactic_entry_created` datetime(3) NOT NULL, + `x_mitre_deprecated` boolean NOT NULL DEFAULT false, + CONSTRAINT `Tactic_tactic_id` PRIMARY KEY(`tactic_id`), + CONSTRAINT `Tactic_mitre_id_unique` UNIQUE(`mitre_id`) +); +--> statement-breakpoint +CREATE TABLE `TacticTechnique` ( + `technique_id` varchar(100) NOT NULL, + `tactic_id` varchar(100) NOT NULL, + CONSTRAINT `TacticTechnique_technique_id_tactic_id_pk` PRIMARY KEY(`technique_id`,`tactic_id`) +); +--> statement-breakpoint +CREATE TABLE `Technique` ( + `technique_id` varchar(100) NOT NULL, + `mitre_id` varchar(20) NOT NULL, + `name` varchar(255) NOT NULL, + `description` longtext NOT NULL, + `technique_entry_created` datetime(3) NOT NULL, + `parent_technique_id` varchar(100), + `x_mitre_deprecated` boolean NOT NULL DEFAULT false, + CONSTRAINT `Technique_technique_id` PRIMARY KEY(`technique_id`), + CONSTRAINT `Technique_mitre_id_unique` UNIQUE(`mitre_id`) +); +--> statement-breakpoint +CREATE TABLE `TechniquePlatform` ( + `technique_id` varchar(100) NOT NULL, + `platform` varchar(100) NOT NULL, + CONSTRAINT `TechniquePlatform_technique_id_platform_pk` PRIMARY KEY(`technique_id`,`platform`) +); +--> statement-breakpoint +CREATE TABLE `TechniqueSoftware` ( + `technique_id` varchar(100) NOT NULL, + `software_id` varchar(100) NOT NULL, + CONSTRAINT `TechniqueSoftware_technique_id_software_id_pk` PRIMARY KEY(`technique_id`,`software_id`) +); +--> statement-breakpoint +CREATE TABLE `ThreatActor` ( + `threat_id` varchar(100) NOT NULL, + `mitre_id` varchar(20) NOT NULL, + `name` varchar(255) NOT NULL, + `description` longtext NOT NULL, + `threat_entry_created` datetime(3) NOT NULL, + `country` varchar(255), + `motivation` varchar(255), + `region` varchar(255), + `first_seen` date, + `last_seen` date, + `status` enum('ACTIVE','INACTIVE','UNKNOWN') NOT NULL, + `danger_level` enum('LOW','MODERATE','HIGH','CRITICAL') NOT NULL, + `x_mitre_deprecated` boolean NOT NULL DEFAULT false, + CONSTRAINT `ThreatActor_threat_id` PRIMARY KEY(`threat_id`), + CONSTRAINT `ThreatActor_mitre_id_unique` UNIQUE(`mitre_id`) +); +--> statement-breakpoint +CREATE TABLE `ThreatActorAlias` ( + `threat_id` varchar(100) NOT NULL, + `alias` varchar(255) NOT NULL, + CONSTRAINT `ThreatActorAlias_threat_id_alias_pk` PRIMARY KEY(`threat_id`,`alias`) +); +--> statement-breakpoint +CREATE TABLE `ThreatActorSector` ( + `threat_id` varchar(100) NOT NULL, + `sector` varchar(255) NOT NULL, + CONSTRAINT `ThreatActorSector_threat_id_sector_pk` PRIMARY KEY(`threat_id`,`sector`) +); +--> statement-breakpoint +CREATE TABLE `ThreatActorSoftware` ( + `threat_id` varchar(100) NOT NULL, + `software_id` varchar(100) NOT NULL, + CONSTRAINT `ThreatActorSoftware_threat_id_software_id_pk` PRIMARY KEY(`threat_id`,`software_id`) +); +--> statement-breakpoint +CREATE TABLE `ThreatActorTechnique` ( + `threat_id` varchar(100) NOT NULL, + `technique_id` varchar(100) NOT NULL, + CONSTRAINT `ThreatActorTechnique_threat_id_technique_id_pk` PRIMARY KEY(`threat_id`,`technique_id`) +); +--> statement-breakpoint +CREATE TABLE `User` ( + `user_id` varchar(100) NOT NULL, + `username` varchar(255) NOT NULL, + CONSTRAINT `User_user_id` PRIMARY KEY(`user_id`), + CONSTRAINT `User_username_unique` UNIQUE(`username`) +); +--> statement-breakpoint +ALTER TABLE `SavedItem` ADD CONSTRAINT `SavedItem_user_id_User_user_id_fk` FOREIGN KEY (`user_id`) REFERENCES `User`(`user_id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `SoftwareAlias` ADD CONSTRAINT `SoftwareAlias_software_id_Software_software_id_fk` FOREIGN KEY (`software_id`) REFERENCES `Software`(`software_id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `SoftwarePlatform` ADD CONSTRAINT `SoftwarePlatform_software_id_Software_software_id_fk` FOREIGN KEY (`software_id`) REFERENCES `Software`(`software_id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `TacticTechnique` ADD CONSTRAINT `TacticTechnique_technique_id_Technique_technique_id_fk` FOREIGN KEY (`technique_id`) REFERENCES `Technique`(`technique_id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `TacticTechnique` ADD CONSTRAINT `TacticTechnique_tactic_id_Tactic_tactic_id_fk` FOREIGN KEY (`tactic_id`) REFERENCES `Tactic`(`tactic_id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `Technique` ADD CONSTRAINT `fk_technique_parent` FOREIGN KEY (`parent_technique_id`) REFERENCES `Technique`(`technique_id`) ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `TechniquePlatform` ADD CONSTRAINT `TechniquePlatform_technique_id_Technique_technique_id_fk` FOREIGN KEY (`technique_id`) REFERENCES `Technique`(`technique_id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `TechniqueSoftware` ADD CONSTRAINT `TechniqueSoftware_technique_id_Technique_technique_id_fk` FOREIGN KEY (`technique_id`) REFERENCES `Technique`(`technique_id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `TechniqueSoftware` ADD CONSTRAINT `TechniqueSoftware_software_id_Software_software_id_fk` FOREIGN KEY (`software_id`) REFERENCES `Software`(`software_id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `ThreatActorAlias` ADD CONSTRAINT `ThreatActorAlias_threat_id_ThreatActor_threat_id_fk` FOREIGN KEY (`threat_id`) REFERENCES `ThreatActor`(`threat_id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `ThreatActorSector` ADD CONSTRAINT `ThreatActorSector_threat_id_ThreatActor_threat_id_fk` FOREIGN KEY (`threat_id`) REFERENCES `ThreatActor`(`threat_id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `ThreatActorSoftware` ADD CONSTRAINT `ThreatActorSoftware_threat_id_ThreatActor_threat_id_fk` FOREIGN KEY (`threat_id`) REFERENCES `ThreatActor`(`threat_id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `ThreatActorSoftware` ADD CONSTRAINT `ThreatActorSoftware_software_id_Software_software_id_fk` FOREIGN KEY (`software_id`) REFERENCES `Software`(`software_id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `ThreatActorTechnique` ADD CONSTRAINT `ThreatActorTechnique_threat_id_ThreatActor_threat_id_fk` FOREIGN KEY (`threat_id`) REFERENCES `ThreatActor`(`threat_id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `ThreatActorTechnique` ADD CONSTRAINT `ThreatActorTechnique_technique_id_Technique_technique_id_fk` FOREIGN KEY (`technique_id`) REFERENCES `Technique`(`technique_id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX `idx_saved_item_type` ON `SavedItem` (`item_type`);--> statement-breakpoint +CREATE INDEX `idx_software_name` ON `Software` (`name`);--> statement-breakpoint +CREATE INDEX `idx_software_type` ON `Software` (`software_type`);--> statement-breakpoint +CREATE INDEX `idx_tactic_name` ON `Tactic` (`name`);--> statement-breakpoint +CREATE INDEX `idx_technique_name` ON `Technique` (`name`);--> statement-breakpoint +CREATE INDEX `idx_technique_parent` ON `Technique` (`parent_technique_id`);--> statement-breakpoint +CREATE INDEX `idx_threat_actor_name` ON `ThreatActor` (`name`);--> statement-breakpoint +CREATE INDEX `idx_threat_actor_country` ON `ThreatActor` (`country`);--> statement-breakpoint +CREATE INDEX `idx_threat_actor_motivation` ON `ThreatActor` (`motivation`);--> statement-breakpoint +CREATE INDEX `idx_threat_actor_region` ON `ThreatActor` (`region`);--> statement-breakpoint +CREATE INDEX `idx_threat_actor_first_seen` ON `ThreatActor` (`first_seen`);--> statement-breakpoint +CREATE INDEX `idx_threat_actor_last_seen` ON `ThreatActor` (`last_seen`);--> statement-breakpoint +CREATE INDEX `idx_threat_actor_status` ON `ThreatActor` (`status`);--> statement-breakpoint +CREATE INDEX `idx_threat_actor_danger_level` ON `ThreatActor` (`danger_level`); \ No newline at end of file diff --git a/backend/drizzle/hvaErDrizzle.md b/backend/drizzle/hvaErDrizzle.md deleted file mode 100644 index 6f327c9..0000000 --- a/backend/drizzle/hvaErDrizzle.md +++ /dev/null @@ -1 +0,0 @@ -Dette er en mappe hvor vi lagrer SQL queries! Inspirert av Elise. \ No newline at end of file diff --git a/backend/drizzle/meta/0000_snapshot.json b/backend/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..5002f1a --- /dev/null +++ b/backend/drizzle/meta/0000_snapshot.json @@ -0,0 +1,1032 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "ee8e316b-12cd-4de3-8a7b-797a959ecb07", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "SavedItem": { + "name": "SavedItem", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "item_id": { + "name": "item_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "item_type": { + "name": "item_type", + "type": "enum('THREAT_ACTOR','TECHNIQUE','SOFTWARE')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "saved_at": { + "name": "saved_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "idx_saved_item_type": { + "name": "idx_saved_item_type", + "columns": [ + "item_type" + ], + "isUnique": false + } + }, + "foreignKeys": { + "SavedItem_user_id_User_user_id_fk": { + "name": "SavedItem_user_id_User_user_id_fk", + "tableFrom": "SavedItem", + "tableTo": "User", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "SavedItem_user_id_item_id_item_type_pk": { + "name": "SavedItem_user_id_item_id_item_type_pk", + "columns": [ + "user_id", + "item_id", + "item_type" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "Software": { + "name": "Software", + "columns": { + "software_id": { + "name": "software_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mitre_id": { + "name": "mitre_id", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "longtext", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "software_type": { + "name": "software_type", + "type": "enum('MALWARE','TOOL')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "software_entry_created": { + "name": "software_entry_created", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "x_mitre_deprecated": { + "name": "x_mitre_deprecated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "idx_software_name": { + "name": "idx_software_name", + "columns": [ + "name" + ], + "isUnique": false + }, + "idx_software_type": { + "name": "idx_software_type", + "columns": [ + "software_type" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "Software_software_id": { + "name": "Software_software_id", + "columns": [ + "software_id" + ] + } + }, + "uniqueConstraints": { + "Software_mitre_id_unique": { + "name": "Software_mitre_id_unique", + "columns": [ + "mitre_id" + ] + } + }, + "checkConstraint": {} + }, + "SoftwareAlias": { + "name": "SoftwareAlias", + "columns": { + "software_id": { + "name": "software_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alias": { + "name": "alias", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "SoftwareAlias_software_id_Software_software_id_fk": { + "name": "SoftwareAlias_software_id_Software_software_id_fk", + "tableFrom": "SoftwareAlias", + "tableTo": "Software", + "columnsFrom": [ + "software_id" + ], + "columnsTo": [ + "software_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "SoftwareAlias_software_id_alias_pk": { + "name": "SoftwareAlias_software_id_alias_pk", + "columns": [ + "software_id", + "alias" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "SoftwarePlatform": { + "name": "SoftwarePlatform", + "columns": { + "software_id": { + "name": "software_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform": { + "name": "platform", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "SoftwarePlatform_software_id_Software_software_id_fk": { + "name": "SoftwarePlatform_software_id_Software_software_id_fk", + "tableFrom": "SoftwarePlatform", + "tableTo": "Software", + "columnsFrom": [ + "software_id" + ], + "columnsTo": [ + "software_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "SoftwarePlatform_software_id_platform_pk": { + "name": "SoftwarePlatform_software_id_platform_pk", + "columns": [ + "software_id", + "platform" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "Tactic": { + "name": "Tactic", + "columns": { + "tactic_id": { + "name": "tactic_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mitre_id": { + "name": "mitre_id", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "longtext", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tactic_entry_created": { + "name": "tactic_entry_created", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "x_mitre_deprecated": { + "name": "x_mitre_deprecated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "idx_tactic_name": { + "name": "idx_tactic_name", + "columns": [ + "name" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "Tactic_tactic_id": { + "name": "Tactic_tactic_id", + "columns": [ + "tactic_id" + ] + } + }, + "uniqueConstraints": { + "Tactic_mitre_id_unique": { + "name": "Tactic_mitre_id_unique", + "columns": [ + "mitre_id" + ] + } + }, + "checkConstraint": {} + }, + "TacticTechnique": { + "name": "TacticTechnique", + "columns": { + "technique_id": { + "name": "technique_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tactic_id": { + "name": "tactic_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "TacticTechnique_technique_id_Technique_technique_id_fk": { + "name": "TacticTechnique_technique_id_Technique_technique_id_fk", + "tableFrom": "TacticTechnique", + "tableTo": "Technique", + "columnsFrom": [ + "technique_id" + ], + "columnsTo": [ + "technique_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "TacticTechnique_tactic_id_Tactic_tactic_id_fk": { + "name": "TacticTechnique_tactic_id_Tactic_tactic_id_fk", + "tableFrom": "TacticTechnique", + "tableTo": "Tactic", + "columnsFrom": [ + "tactic_id" + ], + "columnsTo": [ + "tactic_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "TacticTechnique_technique_id_tactic_id_pk": { + "name": "TacticTechnique_technique_id_tactic_id_pk", + "columns": [ + "technique_id", + "tactic_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "Technique": { + "name": "Technique", + "columns": { + "technique_id": { + "name": "technique_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mitre_id": { + "name": "mitre_id", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "longtext", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "technique_entry_created": { + "name": "technique_entry_created", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_technique_id": { + "name": "parent_technique_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "x_mitre_deprecated": { + "name": "x_mitre_deprecated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "idx_technique_name": { + "name": "idx_technique_name", + "columns": [ + "name" + ], + "isUnique": false + }, + "idx_technique_parent": { + "name": "idx_technique_parent", + "columns": [ + "parent_technique_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "fk_technique_parent": { + "name": "fk_technique_parent", + "tableFrom": "Technique", + "tableTo": "Technique", + "columnsFrom": [ + "parent_technique_id" + ], + "columnsTo": [ + "technique_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "Technique_technique_id": { + "name": "Technique_technique_id", + "columns": [ + "technique_id" + ] + } + }, + "uniqueConstraints": { + "Technique_mitre_id_unique": { + "name": "Technique_mitre_id_unique", + "columns": [ + "mitre_id" + ] + } + }, + "checkConstraint": {} + }, + "TechniquePlatform": { + "name": "TechniquePlatform", + "columns": { + "technique_id": { + "name": "technique_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform": { + "name": "platform", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "TechniquePlatform_technique_id_Technique_technique_id_fk": { + "name": "TechniquePlatform_technique_id_Technique_technique_id_fk", + "tableFrom": "TechniquePlatform", + "tableTo": "Technique", + "columnsFrom": [ + "technique_id" + ], + "columnsTo": [ + "technique_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "TechniquePlatform_technique_id_platform_pk": { + "name": "TechniquePlatform_technique_id_platform_pk", + "columns": [ + "technique_id", + "platform" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "TechniqueSoftware": { + "name": "TechniqueSoftware", + "columns": { + "technique_id": { + "name": "technique_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "software_id": { + "name": "software_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "TechniqueSoftware_technique_id_Technique_technique_id_fk": { + "name": "TechniqueSoftware_technique_id_Technique_technique_id_fk", + "tableFrom": "TechniqueSoftware", + "tableTo": "Technique", + "columnsFrom": [ + "technique_id" + ], + "columnsTo": [ + "technique_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "TechniqueSoftware_software_id_Software_software_id_fk": { + "name": "TechniqueSoftware_software_id_Software_software_id_fk", + "tableFrom": "TechniqueSoftware", + "tableTo": "Software", + "columnsFrom": [ + "software_id" + ], + "columnsTo": [ + "software_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "TechniqueSoftware_technique_id_software_id_pk": { + "name": "TechniqueSoftware_technique_id_software_id_pk", + "columns": [ + "technique_id", + "software_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ThreatActor": { + "name": "ThreatActor", + "columns": { + "threat_id": { + "name": "threat_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mitre_id": { + "name": "mitre_id", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "longtext", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "threat_entry_created": { + "name": "threat_entry_created", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "country": { + "name": "country", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motivation": { + "name": "motivation", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "region": { + "name": "region", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_seen": { + "name": "first_seen", + "type": "date", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_seen": { + "name": "last_seen", + "type": "date", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('ACTIVE','INACTIVE','UNKNOWN')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "danger_level": { + "name": "danger_level", + "type": "enum('LOW','MODERATE','HIGH','CRITICAL')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "x_mitre_deprecated": { + "name": "x_mitre_deprecated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "idx_threat_actor_name": { + "name": "idx_threat_actor_name", + "columns": [ + "name" + ], + "isUnique": false + }, + "idx_threat_actor_country": { + "name": "idx_threat_actor_country", + "columns": [ + "country" + ], + "isUnique": false + }, + "idx_threat_actor_motivation": { + "name": "idx_threat_actor_motivation", + "columns": [ + "motivation" + ], + "isUnique": false + }, + "idx_threat_actor_region": { + "name": "idx_threat_actor_region", + "columns": [ + "region" + ], + "isUnique": false + }, + "idx_threat_actor_first_seen": { + "name": "idx_threat_actor_first_seen", + "columns": [ + "first_seen" + ], + "isUnique": false + }, + "idx_threat_actor_last_seen": { + "name": "idx_threat_actor_last_seen", + "columns": [ + "last_seen" + ], + "isUnique": false + }, + "idx_threat_actor_status": { + "name": "idx_threat_actor_status", + "columns": [ + "status" + ], + "isUnique": false + }, + "idx_threat_actor_danger_level": { + "name": "idx_threat_actor_danger_level", + "columns": [ + "danger_level" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "ThreatActor_threat_id": { + "name": "ThreatActor_threat_id", + "columns": [ + "threat_id" + ] + } + }, + "uniqueConstraints": { + "ThreatActor_mitre_id_unique": { + "name": "ThreatActor_mitre_id_unique", + "columns": [ + "mitre_id" + ] + } + }, + "checkConstraint": {} + }, + "ThreatActorAlias": { + "name": "ThreatActorAlias", + "columns": { + "threat_id": { + "name": "threat_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alias": { + "name": "alias", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "ThreatActorAlias_threat_id_ThreatActor_threat_id_fk": { + "name": "ThreatActorAlias_threat_id_ThreatActor_threat_id_fk", + "tableFrom": "ThreatActorAlias", + "tableTo": "ThreatActor", + "columnsFrom": [ + "threat_id" + ], + "columnsTo": [ + "threat_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ThreatActorAlias_threat_id_alias_pk": { + "name": "ThreatActorAlias_threat_id_alias_pk", + "columns": [ + "threat_id", + "alias" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ThreatActorSector": { + "name": "ThreatActorSector", + "columns": { + "threat_id": { + "name": "threat_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sector": { + "name": "sector", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "ThreatActorSector_threat_id_ThreatActor_threat_id_fk": { + "name": "ThreatActorSector_threat_id_ThreatActor_threat_id_fk", + "tableFrom": "ThreatActorSector", + "tableTo": "ThreatActor", + "columnsFrom": [ + "threat_id" + ], + "columnsTo": [ + "threat_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ThreatActorSector_threat_id_sector_pk": { + "name": "ThreatActorSector_threat_id_sector_pk", + "columns": [ + "threat_id", + "sector" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ThreatActorSoftware": { + "name": "ThreatActorSoftware", + "columns": { + "threat_id": { + "name": "threat_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "software_id": { + "name": "software_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "ThreatActorSoftware_threat_id_ThreatActor_threat_id_fk": { + "name": "ThreatActorSoftware_threat_id_ThreatActor_threat_id_fk", + "tableFrom": "ThreatActorSoftware", + "tableTo": "ThreatActor", + "columnsFrom": [ + "threat_id" + ], + "columnsTo": [ + "threat_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ThreatActorSoftware_software_id_Software_software_id_fk": { + "name": "ThreatActorSoftware_software_id_Software_software_id_fk", + "tableFrom": "ThreatActorSoftware", + "tableTo": "Software", + "columnsFrom": [ + "software_id" + ], + "columnsTo": [ + "software_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ThreatActorSoftware_threat_id_software_id_pk": { + "name": "ThreatActorSoftware_threat_id_software_id_pk", + "columns": [ + "threat_id", + "software_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ThreatActorTechnique": { + "name": "ThreatActorTechnique", + "columns": { + "threat_id": { + "name": "threat_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "technique_id": { + "name": "technique_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "ThreatActorTechnique_threat_id_ThreatActor_threat_id_fk": { + "name": "ThreatActorTechnique_threat_id_ThreatActor_threat_id_fk", + "tableFrom": "ThreatActorTechnique", + "tableTo": "ThreatActor", + "columnsFrom": [ + "threat_id" + ], + "columnsTo": [ + "threat_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ThreatActorTechnique_technique_id_Technique_technique_id_fk": { + "name": "ThreatActorTechnique_technique_id_Technique_technique_id_fk", + "tableFrom": "ThreatActorTechnique", + "tableTo": "Technique", + "columnsFrom": [ + "technique_id" + ], + "columnsTo": [ + "technique_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ThreatActorTechnique_threat_id_technique_id_pk": { + "name": "ThreatActorTechnique_threat_id_technique_id_pk", + "columns": [ + "threat_id", + "technique_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "User": { + "name": "User", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "User_user_id": { + "name": "User_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": { + "User_username_unique": { + "name": "User_username_unique", + "columns": [ + "username" + ] + } + }, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/backend/drizzle/meta/_journal.json b/backend/drizzle/meta/_journal.json new file mode 100644 index 0000000..0c57b96 --- /dev/null +++ b/backend/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "mysql", + "entries": [ + { + "idx": 0, + "version": "5", + "when": 1791451762047, + "tag": "0000_init", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index b0671e6..45d44ce 100644 --- a/backend/package.json +++ b/backend/package.json @@ -21,11 +21,13 @@ "type": "module", "devDependencies": { "@types/node": "^26.6.2", + "drizzle-kit": "^0.31.11", "tsx": "^4.23.15", "typescript": "^7.0.2" }, "dependencies": { "@apollo/server": "^5.5.1", + "drizzle-orm": "^0.45.3", "graphql": "^17.0.2", "mysql2": "^3.24.4" } diff --git a/backend/pnpm-lock.yaml b/backend/pnpm-lock.yaml index 4d91c22..952e8fd 100644 --- a/backend/pnpm-lock.yaml +++ b/backend/pnpm-lock.yaml @@ -211,6 +211,9 @@ importers: '@apollo/server': specifier: ^5.5.1 version: 5.5.1(graphql@17.0.2) + drizzle-orm: + specifier: ^0.45.3 + version: 0.45.3(mysql2@3.24.4(@types/node@26.6.2)) graphql: specifier: ^17.0.2 version: 17.0.2 @@ -221,6 +224,9 @@ importers: '@types/node': specifier: ^26.6.2 version: 26.6.2 + drizzle-kit: + specifier: ^0.31.11 + version: 0.31.11 tsx: specifier: ^4.23.15 version: 4.23.15 @@ -313,156 +319,455 @@ packages: resolution: {integrity: sha512-aaxeavfJ+RHboh7c2ofO5HHtQobGX4AgUujXP4CXpREHp9fQ9jPi6K9T1jrAKe7HIipoP0OJ1gd6JamSkFIpvA==} engines: {node: '>=16'} + '@drizzle-team/brocli@0.10.2': + resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + + '@esbuild-kit/core-utils@3.3.2': + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.hirok.io' + + '@esbuild-kit/esm-loader@2.6.5': + resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.hirok.io' + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.28.2': resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.28.2': resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.28.2': resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.28.2': resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.28.2': resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.28.2': resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.28.2': resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.28.2': resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.28.2': resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.28.2': resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.28.2': resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.28.2': resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.28.2': resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.28.2': resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.28.2': resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.28.2': resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.28.2': resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-arm64@0.28.2': resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.28.2': resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-arm64@0.28.2': resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.28.2': resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/openharmony-arm64@0.28.2': resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.28.2': resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.28.2': resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.28.2': resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.28.2': resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} engines: {node: '>=18'} @@ -667,6 +972,9 @@ packages: resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} engines: {node: '>=18'} + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -716,6 +1024,105 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + drizzle-kit@0.31.11: + resolution: {integrity: sha512-YCYqxTLIB2OCqf6w9/Vef13baBVbwQIPcbT4Y56AfO6B9ajZhDhD8Jkveg/hJvT/iuMloKD/IYYkyMMNhr7Kqg==} + hasBin: true + + drizzle-orm@0.45.3: + resolution: {integrity: sha512-CAloER21cDdcgZ1OmjrZX82EeRNsdP+y0onX/eyoYDaYzki/adrAa91lzM5jWh7zZPK4qUbw/6LGv9J+A+uizA==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=4' + '@electric-sql/pglite': '>=0.2.0' + '@libsql/client': '>=0.10.0' + '@libsql/client-wasm': '>=0.10.0' + '@neondatabase/serverless': '>=0.10.0' + '@netlify/db': '>=0.4.0' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1.13' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/sql.js': '*' + '@upstash/redis': '>=1.34.7' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=14.0.0' + gel: '>=2' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@libsql/client-wasm': + optional: true + '@neondatabase/serverless': + optional: true + '@netlify/db': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/sql.js': + optional: true + '@upstash/redis': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + gel: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -739,6 +1146,16 @@ packages: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} engines: {node: '>= 0.4'} + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + esbuild@0.28.2: resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} engines: {node: '>=18'} @@ -774,6 +1191,9 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + get-tsconfig@4.14.3: + resolution: {integrity: sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -904,6 +1324,9 @@ packages: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + retry@0.13.1: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} engines: {node: '>= 4'} @@ -942,6 +1365,13 @@ packages: resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} engines: {node: '>= 0.4'} + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + sql-escaper@1.5.2: resolution: {integrity: sha512-6CKD38c31SENivxOADeMNLdukOnUxUcflKtzVWzace7Riv1v7cAEym5Cx9Q7gYZ3ezIJI7ZpqecQq8cqNeSSFg==} engines: {bun: '>=1.0.0', deno: '>=2.0.0', node: '>=12.0.0'} @@ -1106,81 +1536,237 @@ snapshots: '@apollo/utils.withrequired@3.0.0': {} + '@drizzle-team/brocli@0.10.2': {} + + '@esbuild-kit/core-utils@3.3.2': + dependencies: + esbuild: 0.18.20 + source-map-support: 0.5.21 + + '@esbuild-kit/esm-loader@2.6.5': + dependencies: + '@esbuild-kit/core-utils': 3.3.2 + get-tsconfig: 4.14.3 + + '@esbuild/aix-ppc64@0.25.12': + optional: true + '@esbuild/aix-ppc64@0.28.2': optional: true + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + '@esbuild/android-arm64@0.28.2': optional: true + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + '@esbuild/android-arm@0.28.2': optional: true + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + '@esbuild/android-x64@0.28.2': optional: true + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + '@esbuild/darwin-arm64@0.28.2': optional: true + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + '@esbuild/darwin-x64@0.28.2': optional: true + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + '@esbuild/freebsd-arm64@0.28.2': optional: true + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + '@esbuild/freebsd-x64@0.28.2': optional: true + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + '@esbuild/linux-arm64@0.28.2': optional: true + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + '@esbuild/linux-arm@0.28.2': optional: true + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + '@esbuild/linux-ia32@0.28.2': optional: true + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + '@esbuild/linux-loong64@0.28.2': optional: true + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + '@esbuild/linux-mips64el@0.28.2': optional: true + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + '@esbuild/linux-ppc64@0.28.2': optional: true + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + '@esbuild/linux-riscv64@0.28.2': optional: true + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + '@esbuild/linux-s390x@0.28.2': optional: true + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + '@esbuild/linux-x64@0.28.2': optional: true + '@esbuild/netbsd-arm64@0.25.12': + optional: true + '@esbuild/netbsd-arm64@0.28.2': optional: true + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + '@esbuild/netbsd-x64@0.28.2': optional: true + '@esbuild/openbsd-arm64@0.25.12': + optional: true + '@esbuild/openbsd-arm64@0.28.2': optional: true + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + '@esbuild/openbsd-x64@0.28.2': optional: true + '@esbuild/openharmony-arm64@0.25.12': + optional: true + '@esbuild/openharmony-arm64@0.28.2': optional: true + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + '@esbuild/sunos-x64@0.28.2': optional: true + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + '@esbuild/win32-arm64@0.28.2': optional: true + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + '@esbuild/win32-ia32@0.28.2': optional: true + '@esbuild/win32-x64@0.18.20': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + '@esbuild/win32-x64@0.28.2': optional: true @@ -1325,6 +1911,8 @@ snapshots: transitivePeerDependencies: - supports-color + buffer-from@1.1.2: {} + bytes@3.1.2: {} call-bind-apply-helpers@1.0.2: @@ -1369,6 +1957,17 @@ snapshots: depd@2.0.0: {} + drizzle-kit@0.31.11: + dependencies: + '@drizzle-team/brocli': 0.10.2 + '@esbuild-kit/esm-loader': 2.6.5 + esbuild: 0.25.12 + tsx: 4.23.15 + + drizzle-orm@0.45.3(mysql2@3.24.4(@types/node@26.6.2)): + optionalDependencies: + mysql2: 3.24.4(@types/node@26.6.2) + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -1387,6 +1986,60 @@ snapshots: dependencies: es-errors: 1.3.0 + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + esbuild@0.28.2: optionalDependencies: '@esbuild/aix-ppc64': 0.28.2 @@ -1460,6 +2113,10 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.2 + get-tsconfig@4.14.3: + dependencies: + resolve-pkg-maps: 1.0.0 + gopd@1.2.0: {} graphql@17.0.2: {} @@ -1569,6 +2226,8 @@ snapshots: iconv-lite: 0.7.3 unpipe: 1.0.0 + resolve-pkg-maps@1.0.0: {} + retry@0.13.1: {} safe-buffer@5.2.1: {} @@ -1620,6 +2279,13 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + sql-escaper@1.5.2: {} statuses@2.0.2: {} diff --git a/backend/src/db/connection.ts b/backend/src/db/connection.ts new file mode 100644 index 0000000..aa2a083 --- /dev/null +++ b/backend/src/db/connection.ts @@ -0,0 +1,26 @@ +/** + * connections.ts oppretter selve forbindelsen mellom + * backend og cyberdb + * + * pool - Mekanismen som snakker med SQL, brukes av drizzle + * + * db - Drizzle som kjører over mysql2 og gir oss bedre typescript muligheter + * altså backend <-> drizzle <-> cyberdb + * + */ +import { drizzle } from "drizzle-orm/mysql2"; +import mysql from "mysql2/promise"; + + +export const pool = mysql.createPool({ + host: process.env.DB_HOST ?? "localhost", + port: Number(process.env.DB_PORT ?? 3306), + user: process.env.DB_USER ?? "cyberuser", + password: process.env.DB_PASSWORD ?? "cyberpass", + database: process.env.DB_NAME ?? "cyberdb", + waitForConnections: true, + connectionLimit: 5, +}); + + +export const db = drizzle({ client: pool }); \ No newline at end of file diff --git a/backend/src/db/schema.ts b/backend/src/db/schema.ts new file mode 100644 index 0000000..5c9b992 --- /dev/null +++ b/backend/src/db/schema.ts @@ -0,0 +1,242 @@ +/** + * Siden databasen er primært for Selects fra databasen, + * så implementerer vi indexser for å gjøre Selects raskere, + * og dermed støtter skalerbarhet i brukerantall. Dette gjøres + * ved å effektivisere søk/filtrering osv. ulempen er vel at + * det krever ekstra ressurser, og delete/modify/insert blir treger + * + * Syntaksen på skjemadefinisjon er: + * + * Kolonner: navnet_i_typescript: datatype("navn_i_sql", {options for datatypen}).ekstra_options_for_attributtet() + */ +import { sql } from "drizzle-orm"; +import {mysqlTable,varchar,longtext,datetime,index,primaryKey,mysqlEnum,date,boolean,foreignKey} from "drizzle-orm/mysql-core"; + + +/** + * Tabeller for ThreatActors vvv + */ +export const threatActor = mysqlTable( + "ThreatActor", { + threat_id: varchar("threat_id", {length:100}).primaryKey(), + mitre_id: varchar("mitre_id", {length: 20}).unique().notNull(), + name: varchar("name", {length: 255}).notNull(), + description: longtext("description").notNull(), + threat_entry_created: datetime("threat_entry_created", { mode: "date", fsp: 3}).notNull(), + country: varchar("country", {length: 255}), + motivation: varchar("motivation", {length: 255}), + region: varchar("region", {length: 255}), + first_seen: date("first_seen", { mode: "date"}), + last_seen: date("last_seen", { mode: "date"}), + status: mysqlEnum("status", ["ACTIVE", "INACTIVE", "UNKNOWN"]).notNull(), + danger_level: mysqlEnum("danger_level", ["LOW", "MODERATE", "HIGH", "CRITICAL"]).notNull(), + x_mitre_deprecated: boolean("x_mitre_deprecated").notNull().default(false) + }, + + (table) => [ + index("idx_threat_actor_name").on(table.name), + index("idx_threat_actor_country").on(table.country), + index("idx_threat_actor_motivation").on(table.motivation), + index("idx_threat_actor_region").on(table.region), + index("idx_threat_actor_first_seen").on(table.first_seen), + index("idx_threat_actor_last_seen").on(table.last_seen), + index("idx_threat_actor_status").on(table.status), + index("idx_threat_actor_danger_level").on(table.danger_level) + ] +); + +export const threatActorAlias = mysqlTable( + "ThreatActorAlias",{ + threat_id: varchar("threat_id", {length: 100}).notNull().references(() => threatActor.threat_id, {onDelete: "cascade"}), + alias: varchar("alias", {length: 255}).notNull() + }, + + (table) => [ + primaryKey({columns: [table.threat_id, table.alias]}) + ] +); + +export const threatActorSector = mysqlTable( + "ThreatActorSector", { + threat_id: varchar("threat_id", {length: 100}).notNull().references(() => threatActor.threat_id, {onDelete: "cascade"}), + sector: varchar("sector", {length: 255}).notNull(), + }, + + (table) => [ + primaryKey({columns: [table.threat_id, table.sector]}), + ] +); + +export const threatActorTechnique = mysqlTable( + "ThreatActorTechnique", { + threat_id: varchar("threat_id", {length: 100}).notNull().references(() => threatActor.threat_id, {onDelete: "cascade"}), + technique_id: varchar("technique_id", {length: 100}).notNull().references(() => technique.technique_id, {onDelete: "cascade"}), + }, + + (table) => [ + primaryKey({columns: [table.threat_id, table.technique_id]}), + ] + +); + +export const threatActorSoftware = mysqlTable( + "ThreatActorSoftware", { + threat_id: varchar("threat_id", {length: 100}).notNull().references(() => threatActor.threat_id, {onDelete: "cascade"}), + software_id: varchar("software_id", {length: 100}).notNull().references(() => software.software_id, {onDelete: "cascade"}), + }, + + (table) => [ + primaryKey({columns: [table.threat_id, table.software_id]}), + ] + +); + +/** + * Tabeller for Techniques vvv + */ +export const technique = mysqlTable( + "Technique", { + technique_id: varchar("technique_id", {length: 100}).primaryKey(), + mitre_id: varchar("mitre_id", {length: 20}).unique().notNull(), + name: varchar("name", {length: 255}).notNull(), + description: longtext("description").notNull(), + technique_entry_created: datetime("technique_entry_created", {mode: "date", fsp: 3}).notNull(), + parent_technique_id: varchar("parent_technique_id", {length: 100}), + x_mitre_deprecated: boolean("x_mitre_deprecated").notNull().default(false), + }, + + (table) => [ + index("idx_technique_name").on(table.name), + index("idx_technique_parent").on(table.parent_technique_id), + foreignKey({name: "fk_technique_parent", columns: [table.parent_technique_id], foreignColumns: [table.technique_id],}).onDelete("set null"), + ] +); + +export const techniquePlatform = mysqlTable( + "TechniquePlatform", { + technique_id: varchar("technique_id", {length: 100}).notNull().references(() => technique.technique_id, {onDelete: "cascade"}), + platform: varchar("platform", {length: 100}).notNull(), + }, + + (table) => [ + primaryKey({columns: [table.technique_id, table.platform]}), + ] +); + +export const techniqueSoftware = mysqlTable( + "TechniqueSoftware", { + technique_id: varchar("technique_id", {length: 100}).notNull().references(() => technique.technique_id, {onDelete: "cascade"}), + software_id: varchar("software_id", {length: 100}).notNull().references(() => software.software_id, {onDelete: "cascade"}), + }, + + (table) => [ + primaryKey({columns: [table.technique_id, table.software_id]}), + ] +); + + +/** + * Tabeller for Software vvv + */ +export const software = mysqlTable( + "Software", { + software_id: varchar("software_id", {length: 100}).primaryKey(), + mitre_id: varchar("mitre_id", {length: 20}).unique().notNull(), + name: varchar("name", {length: 255}).notNull(), + description: longtext("description").notNull(), + software_type: mysqlEnum("software_type", ["MALWARE","TOOL"]).notNull(), + software_entry_created: datetime("software_entry_created", {mode: "date", fsp: 3}).notNull(), + x_mitre_deprecated: boolean("x_mitre_deprecated").notNull().default(false), + }, + + (table) => [ + index("idx_software_name").on(table.name), + index("idx_software_type").on(table.software_type), + ] +); + +export const softwarePlatform = mysqlTable( + "SoftwarePlatform", { + software_id: varchar("software_id", {length: 100}).notNull().references(() => software.software_id, {onDelete: "cascade"}), + platform: varchar("platform", {length: 100}).notNull(), + }, + + (table) => [ + primaryKey({columns: [table.software_id, table.platform]}), + ] + +); + +export const softwareAlias = mysqlTable( + "SoftwareAlias", { + software_id: varchar("software_id", {length: 100}).notNull().references(() => software.software_id, {onDelete: "cascade"}), + alias: varchar("alias", {length: 255}).notNull(), + }, + + (table) => [ + primaryKey({columns: [table.software_id, table.alias]}), + ] +); + + +/** + * Tabeller for Tactic vvv + */ +export const tactic = mysqlTable( + "Tactic", { + tactic_id: varchar("tactic_id", {length: 100}).primaryKey(), + mitre_id: varchar("mitre_id", {length: 20}).unique().notNull(), + name: varchar("name", {length: 255}).notNull(), + description: longtext("description").notNull(), + tactic_entry_created: datetime("tactic_entry_created", {mode: "date", fsp: 3}).notNull(), + x_mitre_deprecated: boolean("x_mitre_deprecated").notNull().default(false), + }, + + (table) => [ + index("idx_tactic_name").on(table.name), + ] +); + +export const tacticTechnique = mysqlTable( + "TacticTechnique", { + technique_id: varchar("technique_id", {length: 100}).notNull().references(() => technique.technique_id, {onDelete: "cascade"}), + tactic_id: varchar("tactic_id", {length: 100}).notNull().references(() => tactic.tactic_id, {onDelete: "cascade"}), + }, + + (table) => [ + primaryKey({columns: [table.technique_id, table.tactic_id]}), + ] +); + + +/** + * Ekstra tabeller vvv + */ + +export const user = mysqlTable( + "User", { + user_id: varchar("user_id", {length: 100}).primaryKey(), + username: varchar("username", {length: 255}).unique().notNull(), + } +); + +export const savedItem = mysqlTable( + "SavedItem", { + user_id: varchar("user_id", {length: 100}).notNull().references(() => user.user_id, {onDelete: "cascade"}), + item_id: varchar("item_id", {length: 100}).notNull(), + item_type: mysqlEnum("item_type", ["THREAT_ACTOR","TECHNIQUE","SOFTWARE"]).notNull(), + saved_at: datetime("saved_at", {mode: "date",fsp: 3}).notNull().default(sql`CURRENT_TIMESTAMP(3)`), + }, + + (table) => [ + primaryKey({ + columns: [ + table.user_id, + table.item_id, + table.item_type + ] + }), + + index("idx_saved_item_type").on(table.item_type), + ] +); diff --git a/backend/src/graphql/resolvers/index.ts b/backend/src/graphql/resolvers/index.ts new file mode 100644 index 0000000..33c0752 --- /dev/null +++ b/backend/src/graphql/resolvers/index.ts @@ -0,0 +1,23 @@ +/** + * Samler alle GraphQL-resolverne på ett sted. + * + * Hver resolver-fil har ansvar for sitt eget område, + * for eksempel Software, ThreatActors, Techniques eller Tactics. + * + * index.ts kombinerer disse resolverne til ett samlet objekt + * som sendes videre til Apollo Server. Dette er ryddigere enn å sende hver + * resolver inn til Apollo hver for seg. + * + * På denne måten slipper server-oppsettet å importere + * hver enkelt resolver-fil direkte. + */ + +import { threatActorResolvers } from "./threatActorResolver.js"; + + + +export const resolvers = { + Query: { + ...threatActorResolvers.Query, + }, +}; \ No newline at end of file diff --git a/backend/src/graphql/resolvers/softwareResolver.ts b/backend/src/graphql/resolvers/softwareResolver.ts new file mode 100644 index 0000000..c0d3458 --- /dev/null +++ b/backend/src/graphql/resolvers/softwareResolver.ts @@ -0,0 +1,12 @@ +/** + * Resolver-laget håndterer GraphQL-spørringer fra klienten. + * + * Resolverne kobler GraphQL-feltene til riktig funksjon i + * repository-laget og sender resultatet tilbake til klienten. + * + * Database-logikk skal ikke ligge direkte i resolverne. + * Den håndteres av repositories. + * + * + * GraphQL -> Resolver -> Repository -> Drizzle -> MySQL + */ \ No newline at end of file diff --git a/backend/src/graphql/resolvers/tacticResolver.ts b/backend/src/graphql/resolvers/tacticResolver.ts new file mode 100644 index 0000000..c0d3458 --- /dev/null +++ b/backend/src/graphql/resolvers/tacticResolver.ts @@ -0,0 +1,12 @@ +/** + * Resolver-laget håndterer GraphQL-spørringer fra klienten. + * + * Resolverne kobler GraphQL-feltene til riktig funksjon i + * repository-laget og sender resultatet tilbake til klienten. + * + * Database-logikk skal ikke ligge direkte i resolverne. + * Den håndteres av repositories. + * + * + * GraphQL -> Resolver -> Repository -> Drizzle -> MySQL + */ \ No newline at end of file diff --git a/backend/src/graphql/resolvers/techniqueResolver.ts b/backend/src/graphql/resolvers/techniqueResolver.ts new file mode 100644 index 0000000..c0d3458 --- /dev/null +++ b/backend/src/graphql/resolvers/techniqueResolver.ts @@ -0,0 +1,12 @@ +/** + * Resolver-laget håndterer GraphQL-spørringer fra klienten. + * + * Resolverne kobler GraphQL-feltene til riktig funksjon i + * repository-laget og sender resultatet tilbake til klienten. + * + * Database-logikk skal ikke ligge direkte i resolverne. + * Den håndteres av repositories. + * + * + * GraphQL -> Resolver -> Repository -> Drizzle -> MySQL + */ \ No newline at end of file diff --git a/backend/src/graphql/resolvers/threatActorResolver.ts b/backend/src/graphql/resolvers/threatActorResolver.ts new file mode 100644 index 0000000..ba49d94 --- /dev/null +++ b/backend/src/graphql/resolvers/threatActorResolver.ts @@ -0,0 +1,46 @@ +/** + * Resolver-laget håndterer GraphQL-spørringer fra klienten. + * + * Resolverne kobler GraphQL-feltene til riktig funksjon i + * repository-laget og sender resultatet tilbake til klienten. + * + * Database-logikk skal ikke ligge direkte i resolverne. + * Den håndteres av repositories. + * + * + * GraphQL Defs -> Resolver -> Repository -> Drizzle -> MySQL + */ + + + +/** + * Resolvere for GraphQL-operasjoner relatert til ThreatActors. + * + * Resolveren mottar GraphQL-spørringer og sender arbeidet + * videre til threatActorRepository. + * + * GraphQL -> Resolver -> Repository -> Drizzle -> MySQL + */ + +import {getAllThreatActors,getThreatActorByMitreId} from "../../repositories/threatActorRepository.js"; + +export const threatActorResolvers = { + Query: { + /** + * Henter alle ThreatActors. + */ + allThreatActors: async () => { + return getAllThreatActors(); + }, + + /** + * Henter én ThreatActor basert på MITRE-ID. + * + * Eksempel: + * G0016 -> APT3 + */ + threatActorByMitreId: async (_parent: unknown,args: { mitreId: string }) => { + return getThreatActorByMitreId(args.mitreId); + }, + }, +}; \ No newline at end of file diff --git a/backend/src/graphql/typeDefs.ts b/backend/src/graphql/typeDefs.ts new file mode 100644 index 0000000..6c618dc --- /dev/null +++ b/backend/src/graphql/typeDefs.ts @@ -0,0 +1,28 @@ +/** + * typeDefs beskriver hvilke operasjoner frontend + * har lov til å spørre GraphQL-API-et om. + * + * Det kan sammenlignes med et interface/kontrakt: + * vi definerer hvilke typer og operasjoner som finnes, + * mens selve implementasjonen ligger i resolvers/. + */ + +export const typeDefs = `#graphql + + # Beskriver hvordan et ThreatActor-objekt ser ut + type ThreatActor { + mitreId: String! + name: String! + description: String! + } + + # Operasjoner frontend kan spørre API-et om + type Query { + + # Henter alle threat actors + allThreatActors: [ThreatActor!]! + + # Henter én threat actor basert på MITRE-ID + threatActorByMitreId(mitreId: String!): ThreatActor + } +`; \ No newline at end of file diff --git a/backend/src/index.ts b/backend/src/index.ts index d95c9df..98b3c8b 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,180 +1,13 @@ +/** + * Henter ut graphql oppsettet, deretter oppretter Apollo Server. + * + * Og til slutt starter selve serveren (port 3001) + */ import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; -import mysql, { type RowDataPacket } from "mysql2/promise"; -type SoftwareRecord = RowDataPacket & { - id: string; - mitreId: string; - name: string; - description: string; - type: "MALWARE" | "TOOL"; - aliases: string[]; - platforms: string[]; -}; - -type RelatedRecord = RowDataPacket & { - id: string; - mitreId: string; - name: string; -}; - -const pool = mysql.createPool({ - host: process.env.DB_HOST ?? "localhost", - port: Number(process.env.DB_PORT ?? 3306), - user: process.env.DB_USER ?? "cyberuser", - password: process.env.DB_PASSWORD ?? "cyberpass", - database: process.env.DB_NAME ?? "cyberdb", - waitForConnections: true, - connectionLimit: 5, -}); - -async function getSoftware(filters: { - mitreId?: string; - search?: string; - type?: string; - platform?: string; -}): Promise { - const conditions: string[] = []; - const values: string[] = []; - - if (filters.mitreId) { - conditions.push("s.mitre_id = ?"); - values.push(filters.mitreId); - } - - if (filters.search?.trim()) { - const search = `%${filters.search.trim()}%`; - conditions.push( - `(s.name LIKE ? OR s.mitre_id LIKE ? OR s.description LIKE ? OR EXISTS ( - SELECT 1 FROM SoftwareAlias search_alias - WHERE search_alias.software_id = s.software_id AND search_alias.alias LIKE ? - ))`, - ); - values.push(search, search, search, search); - } - - if (filters.type && filters.type !== "All types") { - conditions.push("s.software_type = ?"); - values.push(filters.type.toUpperCase()); - } - - if (filters.platform && filters.platform !== "All platforms") { - conditions.push(`EXISTS ( - SELECT 1 FROM SoftwarePlatform filter_platform - WHERE filter_platform.software_id = s.software_id AND filter_platform.platform = ? - )`); - values.push(filters.platform); - } - - const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""; - const [softwareRows] = await pool.execute( - `SELECT s.software_id AS id, s.mitre_id AS mitreId, s.name, s.description, - s.software_type AS type - FROM Software s ${where} - ORDER BY s.name`, - values, - ); - - if (softwareRows.length === 0) return []; - - const ids = softwareRows.map((software) => software.id); - const [aliasRows] = await pool.query( - "SELECT software_id AS softwareId, alias FROM SoftwareAlias WHERE software_id IN (?) ORDER BY alias", - [ids], - ); - const [platformRows] = await pool.query( - "SELECT software_id AS softwareId, platform FROM SoftwarePlatform WHERE software_id IN (?) ORDER BY platform", - [ids], - ); - - const aliasesBySoftware = new Map(); - for (const row of aliasRows) { - const aliases = aliasesBySoftware.get(row.softwareId) ?? []; - aliases.push(row.alias); - aliasesBySoftware.set(row.softwareId, aliases); - } - - const platformsBySoftware = new Map(); - for (const row of platformRows) { - const platforms = platformsBySoftware.get(row.softwareId) ?? []; - platforms.push(row.platform); - platformsBySoftware.set(row.softwareId, platforms); - } - - return softwareRows.map((software) => ({ - ...software, - aliases: aliasesBySoftware.get(software.id) ?? [], - platforms: platformsBySoftware.get(software.id) ?? [], - })); -} - -const typeDefs = `#graphql - type Technique { - id: ID! - mitreId: String! - name: String! - } - - type ThreatActor { - id: ID! - mitreId: String! - name: String! - } - - type Software { - id: ID! - mitreId: String! - name: String! - description: String! - type: String! - aliases: [String!]! - platforms: [String!]! - associatedTechniques: [Technique!]! - usedByThreatActors: [ThreatActor!]! - } - - type Query { - hello: String - software(search: String, type: String, platform: String): [Software!]! - softwareByMitreId(mitreId: String!): Software - } -`; - -const resolvers = { - Query: { - hello: () => "CyberDB backend is alive", - software: (_: unknown, filters: { search?: string; type?: string; platform?: string }) => - getSoftware(filters), - softwareByMitreId: async (_: unknown, args: { mitreId: string }) => { - const [software] = await getSoftware({ mitreId: args.mitreId }); - return software ?? null; - }, - }, - Software: { - associatedTechniques: async (software: SoftwareRecord) => { - const [rows] = await pool.execute( - `SELECT t.technique_id AS id, t.mitre_id AS mitreId, t.name - FROM TechniqueSoftware ts - JOIN Technique t ON t.technique_id = ts.technique_id - WHERE ts.software_id = ? - ORDER BY t.name`, - [software.id], - ); - return rows; - }, - usedByThreatActors: async (software: SoftwareRecord) => { - const [rows] = await pool.execute( - `SELECT a.threat_id AS id, a.mitre_id AS mitreId, a.name - FROM ThreatActorSoftware tas - JOIN ThreatActor a ON a.threat_id = tas.threat_id - WHERE tas.software_id = ? - ORDER BY a.name`, - [software.id], - ); - return rows; - }, - }, -}; +import { typeDefs } from "./graphql/typeDefs.js"; +import { resolvers } from "./graphql/resolvers/index.js"; const server = new ApolloServer({ typeDefs, @@ -182,7 +15,9 @@ const server = new ApolloServer({ }); const { url } = await startStandaloneServer(server, { - listen: { port: Number(process.env.PORT ?? 3001) }, + listen: { + port: Number(process.env.PORT ?? 3001), + }, }); -console.log(`Server running at ${url}`); +console.log(`Server running at ${url}`); \ No newline at end of file diff --git a/backend/src/repositories/softwareRepository.ts b/backend/src/repositories/softwareRepository.ts new file mode 100644 index 0000000..b6f2fc5 --- /dev/null +++ b/backend/src/repositories/softwareRepository.ts @@ -0,0 +1,16 @@ +/** + * Repository-laget samler databaseoperasjoner relatert til Software. + * + * Hensikten er å skille database-logikken fra resten av backenden: + * resolvers skal ikke skrive Drizzle-spørringer direkte, men heller + * kalle funksjoner fra dette repositoryet. + * + * Dermed får vi en tydelig ansvarsfordeling: + * + * GraphQL resolver -> Repository -> Drizzle -> MySQL + * + * Dette gjør koden lettere å lese, teste og endre senere. + * + * + * !!!ALTSÅ HER SKRIVER VI KODEN SOM FAKTISK HENTER UT DATA FRA DB VIA DRIZZLE!!!! + */ \ No newline at end of file diff --git a/backend/src/repositories/tacticRepository.ts b/backend/src/repositories/tacticRepository.ts new file mode 100644 index 0000000..3031feb --- /dev/null +++ b/backend/src/repositories/tacticRepository.ts @@ -0,0 +1,16 @@ +/** + * Repository-laget samler databaseoperasjoner relatert til Tactic. + * + * Hensikten er å skille database-logikken fra resten av backenden: + * resolvers skal ikke skrive Drizzle-spørringer direkte, men heller + * kalle funksjoner fra dette repositoryet. + * + * Dermed får vi en tydelig ansvarsfordeling: + * + * GraphQL resolver -> Repository -> Drizzle -> MySQL + * + * Dette gjør koden lettere å lese, teste og endre senere. + * + * + * !!!ALTSÅ HER SKRIVER VI KODEN SOM FAKTISK HENTER UT DATA FRA DB VIA DRIZZLE!!!! + */ \ No newline at end of file diff --git a/backend/src/repositories/techniqueRepository.ts b/backend/src/repositories/techniqueRepository.ts new file mode 100644 index 0000000..b8faa23 --- /dev/null +++ b/backend/src/repositories/techniqueRepository.ts @@ -0,0 +1,17 @@ +/** + * Repository-laget samler databaseoperasjoner relatert til Technique. + * + * Hensikten er å skille database-logikken fra resten av backenden: + * resolvers skal ikke skrive Drizzle-spørringer direkte, men heller + * kalle funksjoner fra dette repositoryet. + * + * Dermed får vi en tydelig ansvarsfordeling: + * + * GraphQL resolver -> Repository -> Drizzle -> MySQL + * + * Dette gjør koden lettere å lese, teste og endre senere. + * + * + * !!!ALTSÅ HER SKRIVER VI KODEN SOM FAKTISK HENTER UT DATA FRA DB VIA DRIZZLE!!!! + */ + diff --git a/backend/src/repositories/threatActorRepository.ts b/backend/src/repositories/threatActorRepository.ts new file mode 100644 index 0000000..5c59727 --- /dev/null +++ b/backend/src/repositories/threatActorRepository.ts @@ -0,0 +1,86 @@ +/** + * Repository-laget samler databaseoperasjoner relatert til ThreatActor. + * + * Hensikten er å skille database-logikken fra resten av backenden: + * resolvers skal ikke skrive Drizzle-spørringer direkte, men heller + * kalle funksjoner fra dette repositoryet. + * + * Dermed får vi en tydelig ansvarsfordeling: + * + * GraphQL resolver -> Repository -> Drizzle -> MySQL + * + * Dette gjør koden lettere å lese, teste og endre senere. + * + * + * !!!ALTSÅ HER SKRIVER VI KODEN SOM FAKTISK HENTER UT DATA FRA DB VIA DRIZZLE!!!! + */ + +/** + * Repository-laget samler databaseoperasjoner relatert til Technique. + * + * Hensikten er å skille database-logikken fra resten av backenden: + * resolvers skal ikke skrive Drizzle-spørringer direkte, men heller + * kalle funksjoner fra dette repositoryet. + * + * Dermed får vi en tydelig ansvarsfordeling: + * + * GraphQL resolver -> Repository -> Drizzle -> MySQL + * + * Dette gjør koden lettere å lese, teste og endre senere. + * + * + * !!!ALTSÅ HER SKRIVER VI KODEN SOM FAKTISK HENTER UT DATA FRA DB VIA DRIZZLE!!!! + */ + +/** + * Repository-laget samler databaseoperasjoner relatert til ThreatActors. + * + * Hensikten er å skille database-logikken fra resten av backenden. + * Resolverne skal derfor ikke skrive Drizzle-spørringer direkte, + * men bruke funksjonene som er definert her. + * + * Repository -> Drizzle -> MySQL + */ + +import { eq } from "drizzle-orm"; + +import { db } from "../db/connection.js"; +import { threatActor } from "../db/schema.js"; + + +/** + * Henter alle ThreatActors fra databasen. + */ +export async function getAllThreatActors() { + return db + .select({ + mitreId: threatActor.mitre_id, + name: threatActor.name, + description: threatActor.description, + }) + .from(threatActor); +} + +/** + * Henter én ThreatActor basert på MITRE-ID. + * + * Eksempel: + * G0016 -> APT3 + */ +export async function getThreatActorByMitreId( + mitreId: string +) { + const [result] = await db + .select({ + mitreId: threatActor.mitre_id, + name: threatActor.name, + description: threatActor.description, + }) + .from(threatActor) + .where( + eq(threatActor.mitre_id, mitreId) + ) + .limit(1); + + return result ?? null; +} \ No newline at end of file diff --git a/backend/src/test-db.ts b/backend/src/test-db.ts deleted file mode 100644 index 1f93921..0000000 --- a/backend/src/test-db.ts +++ /dev/null @@ -1,25 +0,0 @@ -import mysql from "mysql2/promise"; - -//Oppretter en connection til vår LocalDataBase -const db = mysql.createPool({ - host: "localhost", - port: 3306, - user: "cyberuser", - password: "cyberpass", - database: "cyberdb", -}); - -async function testDB() { - //Oppretter en sql query og venter på resultatet - //db.query returenerer [ rows, metadata ... osv], vi er kun interessert i rows, altså første element - const [rows] = await db.query( - "SELECT name, mitre_id FROM ThreatActor LIMIT 1", - ); - - console.log(rows); - - //Som FD.close() i OS - await db.end(); -} - -testDB(); diff --git a/database/import-mitre.ts b/database/import-mitre.ts index bd55696..9f7abd0 100644 --- a/database/import-mitre.ts +++ b/database/import-mitre.ts @@ -1,950 +1,1464 @@ /** - * Import MITRE ATT&CK Enterprise v19.2 + ThreatActor enrichment into MySQL. + * Importerer MITRE ATT&CK Enterprise-data og + * ThreatActor enrichment inn i databasen. * - * Expected project paths (run from repository root): - * database/data/raw/enterprise-attack-19.2.json - * database/data/enrichment/threat-actor-enrichment-with-danger.json - * - * Tables created/imported from the UML: - * ThreatActor - * ThreatActorSector - * ThreatActorAlias - * ThreatActorTechnique - * ThreatActorSoftware - * Technique - * TechniquePlatform - * Tactic - * TacticTechnique - * Software - * SoftwarePlatform - * SoftwareAlias - * TechniqueSoftware - * User - * SavedItem - * - * Notes: - * - The enrichment file is authoritative for which ThreatActors are imported. - * Since G0028 was removed from enrichment, it will not be imported. - * - User and SavedItem are created but left empty because neither source file - * contains application-user data. - * - SavedItem.item_id is intentionally not a SQL FK because it is polymorphic - * (THREAT_ACTOR / TECHNIQUE / SOFTWARE). - * - * Required package: - * npm install mysql2 - * - * Example: - * DB_HOST=localhost DB_PORT=3306 DB_USER=root DB_PASSWORD=... DB_NAME=cyberdb \ - * npx tsx database/import-mitre.ts + * Database-strukturen håndteres av Drizzle. + * Denne filen har kun ansvar for import av data. */ - import fs from "node:fs/promises"; import path from "node:path"; -import mysql, { PoolConnection, RowDataPacket } from "mysql2/promise"; +import process from "node:process"; +/** + * For skriving av inserts til databasen + */ +import type { PoolConnection } from "mysql2/promise"; +import { pool } from "../backend/src/db/connection.js"; +/** + * En ekstern referanse på et MITRE/STIX objekt. + * Her finner vi blant annet MITRE-ID som G1011, T1059 osv. + */ type StixExternalReference = { - source_name?: string; - external_id?: string; + source_name?: string; + external_id?: string; }; +/** + * Beskriver hvilken tactic en technique tilhører. + */ type KillChainPhase = { - kill_chain_name?: string; - phase_name?: string; + kill_chain_name?: string; + phase_name?: string; }; +/** + * Felles struktur for objekter fra + * enterprise-attack JSON-filen. + */ type StixObject = { - type: string; - id: string; - name?: string; - description?: string; - created?: string; - aliases?: string[]; - x_mitre_aliases?: string[]; - x_mitre_platforms?: string[]; - x_mitre_shortname?: string; - kill_chain_phases?: KillChainPhase[]; - external_references?: StixExternalReference[]; - source_ref?: string; - target_ref?: string; - relationship_type?: string; + type: string; + id: string; + + name?: string; + description?: string; + created?: string; + + aliases?: string[]; + + x_mitre_aliases?: string[]; + x_mitre_platforms?: string[]; + x_mitre_shortname?: string; + x_mitre_deprecated?: boolean; + + kill_chain_phases?: KillChainPhase[]; + + external_references?: StixExternalReference[]; + + source_ref?: string; + target_ref?: string; + relationship_type?: string; }; +/** + * Hele enterprise-attack JSON-filen er en STIX bundle. + */ type StixBundle = { - type: "bundle"; - objects: StixObject[]; + type: "bundle"; + objects: StixObject[]; }; -type ThreatActorStatus = "ACTIVE" | "INACTIVE" | "UNKNOWN"; -type ThreatLevel = "LOW" | "MODERATE" | "HIGH" | "CRITICAL"; +/** + * Typene vi tillater fra enrichment-filen. + */ +type ThreatActorStatus = + | "ACTIVE" + | "INACTIVE" + | "UNKNOWN"; +type ThreatLevel = + | "LOW" + | "MODERATE" + | "HIGH" + | "CRITICAL"; + +/** + * Ekstra informasjon vi selv legger til på ThreatActors. + */ type ThreatActorEnrichment = { - mitre_id: string; - country: string | null; - region: string | null; - motivation: string | null; - first_seen: string | null; - last_seen: string | null; - status: ThreatActorStatus; - sectors: string[]; - danger_level: ThreatLevel; + mitre_id: string; + + country: string | null; + region: string | null; + motivation: string | null; + + first_seen: string | null; + last_seen: string | null; + + status: ThreatActorStatus; + sectors: string[]; + danger_level: ThreatLevel; }; const ROOT = process.cwd(); const ENTERPRISE_PATH = - process.env.ENTERPRISE_JSON ?? - path.join(ROOT, "database/data/raw/enterprise-attack-19.2.json"); + process.env.ENTERPRISE_JSON ?? + path.join( + ROOT, + "database/data/raw/enterprise-attack-19.2.json" + ); const ENRICHMENT_PATH = - process.env.THREAT_ACTOR_ENRICHMENT_JSON ?? - path.join( - ROOT, - "database/data/enrichment/threat-actor-enrichment-with-danger.json", - ); - -const DB_NAME = process.env.DB_NAME ?? "cyberdb"; - -const pool = mysql.createPool({ - host: process.env.DB_HOST ?? "localhost", - port: Number(process.env.DB_PORT ?? 3306), - user: process.env.DB_USER ?? "root", - password: process.env.DB_PASSWORD ?? "", - database: DB_NAME, - waitForConnections: true, - connectionLimit: 5, - multipleStatements: false, -}); + process.env.THREAT_ACTOR_ENRICHMENT_JSON ?? + path.join( + ROOT, + "database/data/enrichment/threat-actor-enrichment-with-danger.json" + ); +/** + * Henter den vanlige MITRE-ID-en fra external_references. + * + * Eksempler: + * intrusion-set--... -> G1011 + * attack-pattern--... -> T1059 + * malware--... -> S0575 + */ function getMitreId(object: StixObject): string | null { - const reference = object.external_references?.find( - (ref) => - ref.source_name === "mitre-attack" && typeof ref.external_id === "string", - ); - return reference?.external_id ?? null; + const reference = object.external_references?.find( + (ref) => + ref.source_name === "mitre-attack" && + typeof ref.external_id === "string" + ); + + return reference?.external_id ?? null; } -function uniqueStrings(values: (string | undefined | null)[]): string[] { - const unique = new Map(); - for (const value of values) { - if (!value) continue; +/** + * Fjerner tomme verdier og duplikater fra arrays. + * + * Brukes blant annet på aliases, platforms og sectors, + * slik at vi ikke prøver å legge samme verdi inn flere ganger. + */ +function uniqueStrings( + values: (string | undefined | null)[] +): string[] { - const normalized = value.trim().toLocaleLowerCase(); + const unique = new Map(); - if (!unique.has(normalized)) { - unique.set(normalized, value.trim()); + for (const value of values) { + + if (!value) { + continue; + } + + const normalized = value + .trim() + .toLocaleLowerCase(); + + if (!unique.has(normalized)) { + unique.set( + normalized, + value.trim() + ); + } } - } - return [...unique.values()]; + return [...unique.values()]; } + +/** + * Sjekker at datoer fra enrichment-filen enten er: + * + * YYYY-MM-DD + * + * eller null. + */ function assertDateOrNull( - value: string | null, - field: string, - mitreId: string, + value: string | null, + field: string, + mitreId: string ): void { - if (value === null) return; - if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) { - throw new Error( - `${mitreId}: ${field} must be YYYY-MM-DD or null, got "${value}"`, - ); - } -} - -function validateEnrichment(entries: ThreatActorEnrichment[]): void { - const validStatuses = new Set([ - "ACTIVE", - "INACTIVE", - "UNKNOWN", - ]); - - const validDangerLevels = new Set([ - "LOW", - "MODERATE", - "HIGH", - "CRITICAL", - ]); - - const seen = new Set(); - - for (const entry of entries) { - if (!/^G\d{4}$/.test(entry.mitre_id)) { - throw new Error(`Invalid ThreatActor MITRE ID: ${entry.mitre_id}`); + if (value === null) { + return; } - if (seen.has(entry.mitre_id)) { - throw new Error(`Duplicate enrichment entry: ${entry.mitre_id}`); + if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) { + throw new Error( + `${mitreId}: ${field} må være YYYY-MM-DD eller null, fikk "${value}"` + ); } - seen.add(entry.mitre_id); +} - if (!validStatuses.has(entry.status)) { - throw new Error( - `${entry.mitre_id}: invalid status "${String(entry.status)}"`, - ); - } - if (!validDangerLevels.has(entry.danger_level)) { - throw new Error( - `${entry.mitre_id}: invalid danger_level "${String( - entry.danger_level, - )}"`, - ); +/** + * Validerer dataene vi selv har lagt til gjennom + * ThreatActor enrichment-filen. + * + * Dette gjør at ugyldige verdier blir oppdaget før + * vi begynner å skrive noe til databasen. + */ +function validateEnrichment( + entries: ThreatActorEnrichment[] +): void { + + const validStatuses = new Set([ + "ACTIVE", + "INACTIVE", + "UNKNOWN" + ]); + + const validDangerLevels = new Set([ + "LOW", + "MODERATE", + "HIGH", + "CRITICAL" + ]); + + const seen = new Set(); + + for (const entry of entries) { + + /** + * ThreatActors skal ha MITRE-ID på formen: + * G0001, G1011 osv. + */ + if (!/^G\d{4}$/.test(entry.mitre_id)) { + throw new Error( + `Ugyldig ThreatActor MITRE-ID: ${entry.mitre_id}` + ); + } + + /** + * Samme ThreatActor skal ikke finnes flere + * ganger i enrichment-filen. + */ + if (seen.has(entry.mitre_id)) { + throw new Error( + `Duplikat i enrichment: ${entry.mitre_id}` + ); + } + + seen.add(entry.mitre_id); + + + if (!validStatuses.has(entry.status)) { + throw new Error( + `${entry.mitre_id}: ugyldig status "${entry.status}"` + ); + } + + + if (!validDangerLevels.has(entry.danger_level)) { + throw new Error( + `${entry.mitre_id}: ugyldig danger_level "${entry.danger_level}"` + ); + } + + + if (!Array.isArray(entry.sectors)) { + throw new Error( + `${entry.mitre_id}: sectors må være en array` + ); + } + + + assertDateOrNull( + entry.first_seen, + "first_seen", + entry.mitre_id + ); + + assertDateOrNull( + entry.last_seen, + "last_seen", + entry.mitre_id + ); } +} - if (!Array.isArray(entry.sectors)) { - throw new Error(`${entry.mitre_id}: sectors must be an array`); +/** + * Setter inn mange rader i databasen samtidig. + * + * Radene deles opp i mindre blokker (chunks), + * slik at vi slipper å kjøre én INSERT per rad. + */ +async function bulkInsert( + connection: PoolConnection, + table: string, + columns: string[], + rows: unknown[][], + chunkSize = 500 +): Promise { + + if (rows.length === 0) { + return; } - assertDateOrNull(entry.first_seen, "first_seen", entry.mitre_id); - assertDateOrNull(entry.last_seen, "last_seen", entry.mitre_id); - } -} + const escapedTable = `\`${table}\``; + + const escapedColumns = columns + .map((column) => `\`${column}\``) + .join(", "); -async function createSchema(connection: PoolConnection): Promise { - const statements = [ - ` - CREATE TABLE IF NOT EXISTS \`ThreatActor\` ( - threat_id VARCHAR(100) PRIMARY KEY, - mitre_id VARCHAR(20) NOT NULL UNIQUE, - name VARCHAR(255) NOT NULL, - description LONGTEXT NOT NULL, - threat_entry_created DATETIME(3) NOT NULL, - country VARCHAR(255) NULL, - motivation VARCHAR(255) NULL, - region VARCHAR(255) NULL, - first_seen DATE NULL, - last_seen DATE NULL, - status ENUM('ACTIVE', 'INACTIVE', 'UNKNOWN') NOT NULL, - danger_level ENUM('LOW', 'MODERATE', 'HIGH', 'CRITICAL') NOT NULL, - INDEX idx_threat_actor_name (name), - INDEX idx_threat_actor_country (country), - INDEX idx_threat_actor_region (region), - INDEX idx_threat_actor_status (status), - INDEX idx_threat_actor_danger_level (danger_level) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - `, - ` - CREATE TABLE IF NOT EXISTS \`ThreatActorSector\` ( - threat_id VARCHAR(100) NOT NULL, - sector VARCHAR(255) NOT NULL, - PRIMARY KEY (threat_id, sector), - CONSTRAINT fk_threat_actor_sector_actor - FOREIGN KEY (threat_id) - REFERENCES \`ThreatActor\` (threat_id) - ON DELETE CASCADE - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - `, - ` - CREATE TABLE IF NOT EXISTS \`ThreatActorAlias\` ( - threat_id VARCHAR(100) NOT NULL, - alias VARCHAR(255) NOT NULL, - PRIMARY KEY (threat_id, alias), - CONSTRAINT fk_threat_actor_alias_actor - FOREIGN KEY (threat_id) - REFERENCES \`ThreatActor\` (threat_id) - ON DELETE CASCADE - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - `, - ` - CREATE TABLE IF NOT EXISTS \`Technique\` ( - technique_id VARCHAR(100) PRIMARY KEY, - mitre_id VARCHAR(20) NOT NULL UNIQUE, - name VARCHAR(255) NOT NULL, - description LONGTEXT NOT NULL, - technique_entry_created DATETIME(3) NOT NULL, - parent_technique_id VARCHAR(100) NULL, - INDEX idx_technique_name (name), - INDEX idx_technique_parent (parent_technique_id), - CONSTRAINT fk_technique_parent - FOREIGN KEY (parent_technique_id) - REFERENCES \`Technique\` (technique_id) - ON DELETE SET NULL - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - `, - ` - CREATE TABLE IF NOT EXISTS \`TechniquePlatform\` ( - technique_id VARCHAR(100) NOT NULL, - platform VARCHAR(100) NOT NULL, - PRIMARY KEY (technique_id, platform), - CONSTRAINT fk_technique_platform_technique - FOREIGN KEY (technique_id) - REFERENCES \`Technique\` (technique_id) - ON DELETE CASCADE - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - `, - ` - CREATE TABLE IF NOT EXISTS \`Tactic\` ( - tactic_id VARCHAR(100) PRIMARY KEY, - mitre_id VARCHAR(20) NOT NULL UNIQUE, - name VARCHAR(255) NOT NULL, - description LONGTEXT NOT NULL, - tactic_entry_created DATETIME(3) NOT NULL, - INDEX idx_tactic_name (name) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - `, - ` - CREATE TABLE IF NOT EXISTS \`Software\` ( - software_id VARCHAR(100) PRIMARY KEY, - mitre_id VARCHAR(20) NOT NULL UNIQUE, - name VARCHAR(255) NOT NULL, - description LONGTEXT NOT NULL, - software_type VARCHAR(20) NOT NULL, - software_entry_created DATETIME(3) NOT NULL, - INDEX idx_software_name (name), - INDEX idx_software_type (software_type) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - `, - ` - CREATE TABLE IF NOT EXISTS \`SoftwarePlatform\` ( - software_id VARCHAR(100) NOT NULL, - platform VARCHAR(100) NOT NULL, - PRIMARY KEY (software_id, platform), - CONSTRAINT fk_software_platform_software - FOREIGN KEY (software_id) - REFERENCES \`Software\` (software_id) - ON DELETE CASCADE - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - `, - ` - CREATE TABLE IF NOT EXISTS \`SoftwareAlias\` ( - software_id VARCHAR(100) NOT NULL, - alias VARCHAR(255) NOT NULL, - PRIMARY KEY (software_id, alias), - CONSTRAINT fk_software_alias_software - FOREIGN KEY (software_id) - REFERENCES \`Software\` (software_id) - ON DELETE CASCADE - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - `, - ` - CREATE TABLE IF NOT EXISTS \`ThreatActorTechnique\` ( - threat_id VARCHAR(100) NOT NULL, - technique_id VARCHAR(100) NOT NULL, - PRIMARY KEY (threat_id, technique_id), - CONSTRAINT fk_threat_actor_technique_actor - FOREIGN KEY (threat_id) - REFERENCES \`ThreatActor\` (threat_id) - ON DELETE CASCADE, - CONSTRAINT fk_threat_actor_technique_technique - FOREIGN KEY (technique_id) - REFERENCES \`Technique\` (technique_id) - ON DELETE CASCADE - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - `, - ` - CREATE TABLE IF NOT EXISTS \`ThreatActorSoftware\` ( - threat_id VARCHAR(100) NOT NULL, - software_id VARCHAR(100) NOT NULL, - PRIMARY KEY (threat_id, software_id), - CONSTRAINT fk_threat_actor_software_actor - FOREIGN KEY (threat_id) - REFERENCES \`ThreatActor\` (threat_id) - ON DELETE CASCADE, - CONSTRAINT fk_threat_actor_software_software - FOREIGN KEY (software_id) - REFERENCES \`Software\` (software_id) - ON DELETE CASCADE - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - `, - ` - CREATE TABLE IF NOT EXISTS \`TechniqueSoftware\` ( - technique_id VARCHAR(100) NOT NULL, - software_id VARCHAR(100) NOT NULL, - PRIMARY KEY (technique_id, software_id), - CONSTRAINT fk_technique_software_technique - FOREIGN KEY (technique_id) - REFERENCES \`Technique\` (technique_id) - ON DELETE CASCADE, - CONSTRAINT fk_technique_software_software - FOREIGN KEY (software_id) - REFERENCES \`Software\` (software_id) - ON DELETE CASCADE - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - `, - ` - CREATE TABLE IF NOT EXISTS \`TacticTechnique\` ( - technique_id VARCHAR(100) NOT NULL, - tactic_id VARCHAR(100) NOT NULL, - PRIMARY KEY (technique_id, tactic_id), - CONSTRAINT fk_tactic_technique_technique - FOREIGN KEY (technique_id) - REFERENCES \`Technique\` (technique_id) - ON DELETE CASCADE, - CONSTRAINT fk_tactic_technique_tactic - FOREIGN KEY (tactic_id) - REFERENCES \`Tactic\` (tactic_id) - ON DELETE CASCADE - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - `, - ` - CREATE TABLE IF NOT EXISTS \`User\` ( - user_id VARCHAR(100) PRIMARY KEY, - username VARCHAR(255) NOT NULL UNIQUE - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - `, - ` - CREATE TABLE IF NOT EXISTS \`SavedItem\` ( - user_id VARCHAR(100) NOT NULL, - item_id VARCHAR(100) NOT NULL, - item_type ENUM('THREAT_ACTOR', 'TECHNIQUE', 'SOFTWARE') NOT NULL, - saved_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - PRIMARY KEY (user_id, item_id, item_type), - INDEX idx_saved_item_type (item_type), - CONSTRAINT fk_saved_item_user - FOREIGN KEY (user_id) - REFERENCES \`User\` (user_id) - ON DELETE CASCADE - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - `, - ]; - - for (const statement of statements) { - await connection.query(statement); - } + + for ( + let start = 0; + start < rows.length; + start += chunkSize + ) { + + const chunk = + rows.slice(start, start + chunkSize); + + /** + * Lager placeholders som: + * + * (?, ?, ?), (?, ?, ?), (?, ?, ?) + */ + const placeholders = chunk + .map( + () => + `(${columns.map(() => "?").join(", ")})` + ) + .join(", "); + + /** + * Gjør: + * + * [ + * [1, 2], + * [3, 4] + * ] + * + * om til: + * + * [1, 2, 3, 4] + */ + const values = chunk.flat(); + + + await connection.query( + `INSERT INTO ${escapedTable} (${escapedColumns}) + VALUES ${placeholders}`, + values + ); + } } -async function bulkInsert( - connection: PoolConnection, - table: string, - columns: string[], - rows: unknown[][], - chunkSize = 500, + +/** + * Fjerner gammel MITRE-data før vi importerer på nytt. + * + * User og SavedItem skal ikke tømmes, + * fordi dette er data som brukerne lager selv. + */ +async function clearImportedData( + connection: PoolConnection ): Promise { - if (rows.length === 0) return; - const escapedTable = `\`${table}\``; - const escapedColumns = columns.map((column) => `\`${column}\``).join(", "); + /** + * Child- og relasjonstabeller slettes først, + * siden de har foreign keys til hovedtabellene. + */ + const childTables = [ + "ThreatActorTechnique", + "ThreatActorSoftware", + "TechniqueSoftware", + "TacticTechnique", + "ThreatActorSector", + "ThreatActorAlias", + "TechniquePlatform", + "SoftwarePlatform", + "SoftwareAlias", + ]; - for (let start = 0; start < rows.length; start += chunkSize) { - const chunk = rows.slice(start, start + chunkSize); - const placeholders = chunk - .map(() => `(${columns.map(() => "?").join(", ")})`) - .join(", "); - const values = chunk.flat(); + for (const table of childTables) { + await connection.query( + `DELETE FROM \`${table}\`` + ); + } + + + /** + * Technique har en self-reference gjennom + * parent_technique_id. + * + * Den må derfor fjernes før Technique tømmes. + */ await connection.query( - `INSERT INTO ${escapedTable} (${escapedColumns}) VALUES ${placeholders}`, - values, + `UPDATE \`Technique\` + SET parent_technique_id = NULL + WHERE parent_technique_id IS NOT NULL` ); - } -} -async function clearImportedData(connection: PoolConnection): Promise { - // Keep User and SavedItem intact: these are application-generated data. - // SavedItem has no domain FK because item_id is polymorphic. - const childTables = [ - "ThreatActorTechnique", - "ThreatActorSoftware", - "TechniqueSoftware", - "TacticTechnique", - "ThreatActorSector", - "ThreatActorAlias", - "TechniquePlatform", - "SoftwarePlatform", - "SoftwareAlias", - ]; - - for (const table of childTables) { - await connection.query(`DELETE FROM \`${table}\``); - } - - // Break self-references before deleting all techniques. - await connection.query( - "UPDATE `Technique` SET parent_technique_id = NULL WHERE parent_technique_id IS NOT NULL", - ); - - for (const table of ["ThreatActor", "Technique", "Tactic", "Software"]) { - await connection.query(`DELETE FROM \`${table}\``); - } + + /** + * Nå kan hovedtabellene tømmes. + */ + const parentTables = [ + "ThreatActor", + "Technique", + "Tactic", + "Software", + ]; + + + for (const table of parentTables) { + + await connection.query( + `DELETE FROM \`${table}\`` + ); + } } async function main(): Promise { - const [enterpriseRaw, enrichmentRaw] = await Promise.all([ - fs.readFile(ENTERPRISE_PATH, "utf8"), - fs.readFile(ENRICHMENT_PATH, "utf8"), - ]); - - const enterprise = JSON.parse(enterpriseRaw) as StixBundle; - const enrichment = JSON.parse(enrichmentRaw) as ThreatActorEnrichment[]; - - if (enterprise.type !== "bundle" || !Array.isArray(enterprise.objects)) { - throw new Error("Enterprise ATT&CK file is not a valid STIX bundle"); - } - - validateEnrichment(enrichment); - - const objects = enterprise.objects; - const enrichmentByMitreId = new Map( - enrichment.map((entry) => [entry.mitre_id, entry]), - ); - - const allActors = objects.filter((object) => object.type === "intrusion-set"); - const techniques = objects.filter( - (object) => object.type === "attack-pattern", - ); - const tactics = objects.filter((object) => object.type === "x-mitre-tactic"); - const software = objects.filter( - (object) => object.type === "malware" || object.type === "tool", - ); - const relationships = objects.filter( - (object) => object.type === "relationship", - ); - - const actorByMitreId = new Map(); - - for (const actor of allActors) { - const mitreId = getMitreId(actor); - if (mitreId) actorByMitreId.set(mitreId, actor); - } - - const missingActors = enrichment - .map((entry) => entry.mitre_id) - .filter((mitreId) => !actorByMitreId.has(mitreId)); - - if (missingActors.length > 0) { - throw new Error( - `Enrichment contains MITRE IDs missing from Enterprise ATT&CK: ${missingActors.join( - ", ", - )}`, - ); - } - - // IMPORTANT: - // Only actors present in enrichment are imported. - // Therefore G0028 remains excluded exactly as requested. - const actors = enrichment.map((entry) => { - const actor = actorByMitreId.get(entry.mitre_id); - if (!actor) { - throw new Error(`Could not resolve actor ${entry.mitre_id}`); - } - return actor; - }); - const actorIds = new Set(actors.map((actor) => actor.id)); - const techniqueIds = new Set(techniques.map((technique) => technique.id)); - const softwareIds = new Set(software.map((item) => item.id)); + /** + * Leser begge JSON-filene parallelt. + */ + const [enterpriseRaw, enrichmentRaw] = await Promise.all([ + fs.readFile(ENTERPRISE_PATH, "utf8"), + fs.readFile(ENRICHMENT_PATH, "utf8"), + ]); - const tacticByShortname = new Map(); - for (const tactic of tactics) { - if (tactic.x_mitre_shortname) { - tacticByShortname.set(tactic.x_mitre_shortname, tactic); - } - } - const parentByTechniqueId = new Map(); + /** + * Gjør JSON-tekst om til JavaScript-objekter. + */ + const enterprise = JSON.parse(enterpriseRaw) as StixBundle; + + const enrichment = + JSON.parse(enrichmentRaw) as ThreatActorEnrichment[]; - for (const relationship of relationships) { + + /** + * Enkel validering av selve filformatene. + */ if ( - relationship.relationship_type === "subtechnique-of" && - relationship.source_ref && - relationship.target_ref && - techniqueIds.has(relationship.source_ref) && - techniqueIds.has(relationship.target_ref) + enterprise.type !== "bundle" || + !Array.isArray(enterprise.objects) ) { - parentByTechniqueId.set(relationship.source_ref, relationship.target_ref); + throw new Error( + "Enterprise ATT&CK filen er ikke en gyldig STIX bundle" + ); } - } - const threatActorRows = actors.map((actor) => { - const mitreId = getMitreId(actor); - if (!mitreId) { - throw new Error(`ThreatActor ${actor.id} has no MITRE ID`); + if (!Array.isArray(enrichment)) { + throw new Error( + "ThreatActor enrichment filen er ikke en array" + ); } - const extra = enrichmentByMitreId.get(mitreId); - if (!extra) { - throw new Error(`ThreatActor ${mitreId} has no enrichment`); - } - if (!actor.created) { - throw new Error(`${mitreId}: missing created timestamp`); - } + /** + * Mer detaljert validering av enrichment-dataene. + */ + validateEnrichment(enrichment); - if (!actor.name) { - throw new Error(`${mitreId}: missing name`); - } - return [ - actor.id, - mitreId, - actor.name, - actor.description ?? "", - new Date(actor.created), - extra.country, - extra.motivation, - extra.region, - extra.first_seen, - extra.last_seen, - extra.status, - extra.danger_level, - ]; - }); + /** + * Alle MITRE-objektene ligger i samme objects-array. + * + * Vi deler dem derfor opp etter STIX type, + * slik at resten av importscriptet blir lettere å jobbe med. + */ + const objects = enterprise.objects; - const threatActorSectorRows: unknown[][] = []; - for (const actor of actors) { - const mitreId = getMitreId(actor)!; - const extra = enrichmentByMitreId.get(mitreId)!; + const allActors = objects.filter( + (object) => object.type === "intrusion-set" + ); - for (const sector of uniqueStrings(extra.sectors)) { - threatActorSectorRows.push([actor.id, sector]); - } - } + const techniques = objects.filter( + (object) => object.type === "attack-pattern" + ); - const threatActorAliasRows: unknown[][] = []; - for (const actor of actors) { - for (const alias of uniqueStrings(actor.aliases ?? [])) { - threatActorAliasRows.push([actor.id, alias]); - } - } + const tactics = objects.filter( + (object) => object.type === "x-mitre-tactic" + ); - const techniqueRows = techniques.map((technique) => { - const mitreId = getMitreId(technique); + const software = objects.filter( + (object) => + object.type === "malware" || + object.type === "tool" + ); - if (!mitreId || !technique.name || !technique.created) { - throw new Error( - `Technique ${technique.id} is missing mitre_id, name, or created`, - ); - } + const relationships = objects.filter( + (object) => object.type === "relationship" + ); - return [ - technique.id, - mitreId, - technique.name, - technique.description ?? "", - new Date(technique.created), - null, // parent added after all techniques exist - ]; - }); - const techniquePlatformRows: unknown[][] = []; - for (const technique of techniques) { - for (const platform of uniqueStrings(technique.x_mitre_platforms ?? [])) { - techniquePlatformRows.push([technique.id, platform]); - } - } + /** + * Lager et Map slik at vi raskt kan finne enrichment + * ved hjelp av MITRE-ID. + * + * G1011 -> enrichment-data for G1011 + */ + const enrichmentByMitreId = new Map( + enrichment.map( + (entry) => [entry.mitre_id, entry] + ) + ); - const tacticRows = tactics.map((tactic) => { - const mitreId = getMitreId(tactic); - if (!mitreId || !tactic.name || !tactic.created) { - throw new Error( - `Tactic ${tactic.id} is missing mitre_id, name, or created`, - ); - } + /** + * Lager også et Map over alle ThreatActors fra MITRE. + * + * G1011 -> selve MITRE/STIX objektet + */ + const actorByMitreId = + new Map(); - return [ - tactic.id, - mitreId, - tactic.name, - tactic.description ?? "", - new Date(tactic.created), - ]; - }); + for (const actor of allActors) { - const softwareRows = software.map((item) => { - const mitreId = getMitreId(item); + const mitreId = getMitreId(actor); - if (!mitreId || !item.name || !item.created) { - throw new Error( - `Software ${item.id} is missing mitre_id, name, or created`, - ); + if (mitreId) { + actorByMitreId.set( + mitreId, + actor + ); + } } - return [ - item.id, - mitreId, - item.name, - item.description ?? "", - item.type === "malware" ? "MALWARE" : "TOOL", - new Date(item.created), - ]; - }); - const softwarePlatformRows: unknown[][] = []; - const softwareAliasRows: unknown[][] = []; - - for (const item of software) { - for (const platform of uniqueStrings(item.x_mitre_platforms ?? [])) { - softwarePlatformRows.push([item.id, platform]); + /** + * Sjekker at alle ThreatActors vi har enrichment for + * faktisk eksisterer i MITRE-datasettet. + */ + const missingActors = enrichment + .map((entry) => entry.mitre_id) + .filter( + (mitreId) => + !actorByMitreId.has(mitreId) + ); + + if (missingActors.length > 0) { + throw new Error( + `Enrichment inneholder ThreatActors som ikke finnes i MITRE: ${missingActors.join(", ")}` + ); } - for (const alias of uniqueStrings(item.x_mitre_aliases ?? [])) { - softwareAliasRows.push([item.id, alias]); - } - } - const threatActorTechniqueSet = new Set(); - const threatActorTechniqueRows: unknown[][] = []; + /** + * Vi importerer kun ThreatActors som finnes + * i enrichment-filen. + * + * Enrichment-filen bestemmer altså hvilke + * ThreatActors som skal være med i databasen. + */ + const actors = enrichment.map((entry) => { + + const actor = + actorByMitreId.get(entry.mitre_id); + + if (!actor) { + throw new Error( + `Kunne ikke finne ThreatActor ${entry.mitre_id}` + ); + } + + return actor; + }); + + /** + * Lager raske oppslag på interne STIX-ID-er. + * + * Disse brukes senere når vi skal bygge relasjoner + * mellom ThreatActors, Techniques og Software. + */ + const actorIds = new Set( + actors.map((actor) => actor.id) + ); - const threatActorSoftwareSet = new Set(); - const threatActorSoftwareRows: unknown[][] = []; + const techniqueIds = new Set( + techniques.map((technique) => technique.id) + ); - const techniqueSoftwareSet = new Set(); - const techniqueSoftwareRows: unknown[][] = []; + const softwareIds = new Set( + software.map((item) => item.id) + ); - for (const relationship of relationships) { - if ( - relationship.relationship_type !== "uses" || - !relationship.source_ref || - !relationship.target_ref - ) { - continue; + + /** + * Tactics blir referert til gjennom shortname i + * kill_chain_phases på Techniques. + * + * Derfor lager vi: + * + * "initial-access" -> Tactic objekt + */ + const tacticByShortname = + new Map(); + + for (const tactic of tactics) { + + if (tactic.x_mitre_shortname) { + tacticByShortname.set( + tactic.x_mitre_shortname, + tactic + ); + } } - const { source_ref: source, target_ref: target } = relationship; - if (actorIds.has(source) && techniqueIds.has(target)) { - const key = `${source}|${target}`; - if (!threatActorTechniqueSet.has(key)) { - threatActorTechniqueSet.add(key); - threatActorTechniqueRows.push([source, target]); - } + /** + * Finner parent-child forhold mellom Techniques. + * + * Eksempel: + * T1059.001 -> T1059 + * + * Disse ligger som "subtechnique-of" relationships + * i MITRE-dataene. + */ + const parentByTechniqueId = + new Map(); + + for (const relationship of relationships) { + + if ( + relationship.relationship_type === "subtechnique-of" && + relationship.source_ref && + relationship.target_ref && + techniqueIds.has(relationship.source_ref) && + techniqueIds.has(relationship.target_ref) + ) { + parentByTechniqueId.set( + relationship.source_ref, + relationship.target_ref + ); + } } - if (actorIds.has(source) && softwareIds.has(target)) { - const key = `${source}|${target}`; - if (!threatActorSoftwareSet.has(key)) { - threatActorSoftwareSet.add(key); - threatActorSoftwareRows.push([source, target]); - } - } - if (softwareIds.has(source) && techniqueIds.has(target)) { - const key = `${target}|${source}`; - if (!techniqueSoftwareSet.has(key)) { - techniqueSoftwareSet.add(key); - techniqueSoftwareRows.push([target, source]); - } + /** + * ========================================================== + * ThreatActor rows + * ========================================================== + * + * Her transformerer vi MITRE + enrichment til formatet + * som ThreatActor-tabellen forventer. + */ + const threatActorRows = actors.map((actor) => { + + const mitreId = getMitreId(actor); + + if (!mitreId) { + throw new Error( + `ThreatActor ${actor.id} mangler MITRE-ID` + ); + } + + const extra = + enrichmentByMitreId.get(mitreId); + + if (!extra) { + throw new Error( + `ThreatActor ${mitreId} mangler enrichment` + ); + } + + if (!actor.created) { + throw new Error( + `${mitreId}: mangler created timestamp` + ); + } + + if (!actor.name) { + throw new Error( + `${mitreId}: mangler name` + ); + } + + return [ + actor.id, + mitreId, + actor.name, + actor.description ?? "", + new Date(actor.created), + + extra.country, + extra.motivation, + extra.region, + + extra.first_seen, + extra.last_seen, + + extra.status, + extra.danger_level, + + actor.x_mitre_deprecated ?? false, + ]; + }); + + + /** + * ThreatActorSector + */ + const threatActorSectorRows: unknown[][] = []; + + for (const actor of actors) { + + const mitreId = getMitreId(actor); + + if (!mitreId) { + continue; + } + + const extra = + enrichmentByMitreId.get(mitreId); + + if (!extra) { + continue; + } + + for ( + const sector of uniqueStrings(extra.sectors) + ) { + threatActorSectorRows.push([ + actor.id, + sector + ]); + } } - } - const tacticTechniqueSet = new Set(); - const tacticTechniqueRows: unknown[][] = []; - for (const technique of techniques) { - for (const phase of technique.kill_chain_phases ?? []) { - if (phase.kill_chain_name !== "mitre-attack" || !phase.phase_name) { - continue; - } + /** + * ThreatActorAlias + */ + const threatActorAliasRows: unknown[][] = []; - const tactic = tacticByShortname.get(phase.phase_name); - if (!tactic) continue; + for (const actor of actors) { - const key = `${technique.id}|${tactic.id}`; - if (!tacticTechniqueSet.has(key)) { - tacticTechniqueSet.add(key); - tacticTechniqueRows.push([technique.id, tactic.id]); - } + for ( + const alias of uniqueStrings( + actor.aliases ?? [] + ) + ) { + threatActorAliasRows.push([ + actor.id, + alias + ]); + } } - } - - console.log("Validated source data:"); - console.log(` ThreatActors selected: ${actors.length}`); - console.log(` Techniques: ${techniques.length}`); - console.log(` Tactics: ${tactics.length}`); - console.log(` Software: ${software.length}`); - console.log(` ThreatActor sectors: ${threatActorSectorRows.length}`); - console.log(` ThreatActor aliases: ${threatActorAliasRows.length}`); - console.log( - ` ThreatActor -> Technique: ${threatActorTechniqueRows.length}`, - ); - console.log( - ` ThreatActor -> Software: ${threatActorSoftwareRows.length}`, - ); - console.log(` Technique -> Software: ${techniqueSoftwareRows.length}`); - console.log(` Tactic -> Technique: ${tacticTechniqueRows.length}`); - - const connection = await pool.getConnection(); - - try { - await createSchema(connection); - - await connection.beginTransaction(); - - await clearImportedData(connection); - - // Parent tables first - await bulkInsert( - connection, - "ThreatActor", - [ - "threat_id", - "mitre_id", - "name", - "description", - "threat_entry_created", - "country", - "motivation", - "region", - "first_seen", - "last_seen", - "status", - "danger_level", - ], - threatActorRows, - ); - await bulkInsert( - connection, - "Technique", - [ - "technique_id", - "mitre_id", - "name", - "description", - "technique_entry_created", - "parent_technique_id", - ], - techniqueRows, - ); - await bulkInsert( - connection, - "Tactic", - ["tactic_id", "mitre_id", "name", "description", "tactic_entry_created"], - tacticRows, + /** + * ========================================================== + * Technique rows + * ========================================================== + */ + const techniqueRows = techniques.map( + (technique) => { + + const mitreId = + getMitreId(technique); + + if ( + !mitreId || + !technique.name || + !technique.created + ) { + throw new Error( + `Technique ${technique.id} mangler mitre_id, name eller created` + ); + } + + return [ + technique.id, + mitreId, + technique.name, + technique.description ?? "", + new Date(technique.created), + + /** + * Parent settes til null først. + * + * Vi oppdaterer denne etter at alle + * Technique-radene er satt inn. + */ + null, + + technique.x_mitre_deprecated ?? false, + ]; + } ); - await bulkInsert( - connection, - "Software", - [ - "software_id", - "mitre_id", - "name", - "description", - "software_type", - "software_entry_created", - ], - softwareRows, - ); - // Technique self-reference after all Technique rows exist - for (const [techniqueId, parentTechniqueId] of parentByTechniqueId) { - await connection.query( - ` - UPDATE \`Technique\` - SET parent_technique_id = ? - WHERE technique_id = ? - `, - [parentTechniqueId, techniqueId], - ); + /** + * TechniquePlatform + */ + const techniquePlatformRows: unknown[][] = []; + + for (const technique of techniques) { + + for ( + const platform of uniqueStrings( + technique.x_mitre_platforms ?? [] + ) + ) { + techniquePlatformRows.push([ + technique.id, + platform + ]); + } } - // Child / join tables - await bulkInsert( - connection, - "ThreatActorSector", - ["threat_id", "sector"], - threatActorSectorRows, - ); - await bulkInsert( - connection, - "ThreatActorAlias", - ["threat_id", "alias"], - threatActorAliasRows, + /** + * ========================================================== + * Tactic rows + * ========================================================== + */ + const tacticRows = tactics.map((tactic) => { + + const mitreId = getMitreId(tactic); + + if ( + !mitreId || + !tactic.name || + !tactic.created + ) { + throw new Error( + `Tactic ${tactic.id} mangler mitre_id, name eller created` + ); + } + + return [ + tactic.id, + mitreId, + tactic.name, + tactic.description ?? "", + new Date(tactic.created), + + tactic.x_mitre_deprecated ?? false, + ]; + }); + + + /** + * ========================================================== + * Software rows + * ========================================================== + */ + const softwareRows = software.map((item) => { + + const mitreId = getMitreId(item); + + if ( + !mitreId || + !item.name || + !item.created + ) { + throw new Error( + `Software ${item.id} mangler mitre_id, name eller created` + ); + } + + /** + * Vi har tidligere filtrert Software til kun: + * + * malware + * tool + * + * derfor kan vi trygt mappe disse til enum-verdiene + * i databasen. + */ + const softwareType = + item.type === "malware" + ? "MALWARE" + : "TOOL"; + + return [ + item.id, + mitreId, + item.name, + item.description ?? "", + softwareType, + new Date(item.created), + + item.x_mitre_deprecated ?? false, + ]; + }); + + + /** + * SoftwarePlatform + SoftwareAlias + */ + const softwarePlatformRows: unknown[][] = []; + const softwareAliasRows: unknown[][] = []; + + for (const item of software) { + + for ( + const platform of uniqueStrings( + item.x_mitre_platforms ?? [] + ) + ) { + softwarePlatformRows.push([ + item.id, + platform + ]); + } + + + for ( + const alias of uniqueStrings( + item.x_mitre_aliases ?? [] + ) + ) { + softwareAliasRows.push([ + item.id, + alias + ]); + } + } + /** + * ========================================================== + * Relasjoner mellom ThreatActors, Techniques og Software + * ========================================================== + * + * MITRE lagrer disse forholdene som egne relationship-objekter. + * + * Eksempel: + * + * ThreatActor -> uses -> Technique + * ThreatActor -> uses -> Software + * Software -> uses -> Technique + * + * Set brukes for å unngå at samme relasjon blir lagt inn + * flere ganger. + */ + + + /** + * ThreatActor -> Technique + */ + const threatActorTechniqueSet = new Set(); + const threatActorTechniqueRows: unknown[][] = []; + + + /** + * ThreatActor -> Software + */ + const threatActorSoftwareSet = new Set(); + const threatActorSoftwareRows: unknown[][] = []; + + + /** + * Technique -> Software + */ + const techniqueSoftwareSet = new Set(); + const techniqueSoftwareRows: unknown[][] = []; + + + for (const relationship of relationships) { + + /** + * Her er vi kun interessert i MITRE-relasjoner + * av typen "uses". + */ + if ( + relationship.relationship_type !== "uses" || + !relationship.source_ref || + !relationship.target_ref + ) { + continue; + } + + const source = relationship.source_ref; + const target = relationship.target_ref; + + + /** + * ThreatActor -> Technique + * + * Eksempel: + * APT28 bruker Technique T1059 + */ + if ( + actorIds.has(source) && + techniqueIds.has(target) + ) { + const key = `${source}|${target}`; + + if (!threatActorTechniqueSet.has(key)) { + + threatActorTechniqueSet.add(key); + + threatActorTechniqueRows.push([ + source, + target + ]); + } + } + + + /** + * ThreatActor -> Software + * + * Eksempel: + * APT28 bruker Mimikatz + */ + if ( + actorIds.has(source) && + softwareIds.has(target) + ) { + const key = `${source}|${target}`; + + if (!threatActorSoftwareSet.has(key)) { + + threatActorSoftwareSet.add(key); + + threatActorSoftwareRows.push([ + source, + target + ]); + } + } + + + /** + * Software -> Technique + * + * I databasen lagrer vi dette som: + * + * TechniqueSoftware( + * technique_id, + * software_id + * ) + * + * Derfor snur vi rekkefølgen når raden opprettes. + */ + if ( + softwareIds.has(source) && + techniqueIds.has(target) + ) { + const key = `${target}|${source}`; + + if (!techniqueSoftwareSet.has(key)) { + + techniqueSoftwareSet.add(key); + + techniqueSoftwareRows.push([ + target, + source + ]); + } + } + } + + + /** + * ========================================================== + * Tactic -> Technique + * ========================================================== + * + * Techniques inneholder kill_chain_phases som sier + * hvilke tactics de tilhører. + * + * Derfor bygger vi denne relasjonen fra Technique-dataene + * i stedet for relationship-objektene. + */ + const tacticTechniqueSet = new Set(); + const tacticTechniqueRows: unknown[][] = []; + + + for (const technique of techniques) { + + for ( + const phase of technique.kill_chain_phases ?? [] + ) { + + /** + * Vi er kun interessert i Enterprise ATT&CK + * kill chain. + */ + if ( + phase.kill_chain_name !== "mitre-attack" || + !phase.phase_name + ) { + continue; + } + + + /** + * phase_name kan f.eks. være: + * + * "initial-access" + * + * tacticByShortname gir oss da det faktiske + * Tactic-objektet. + */ + const tactic = + tacticByShortname.get(phase.phase_name); + + if (!tactic) { + continue; + } + + + const key = + `${technique.id}|${tactic.id}`; + + + if (!tacticTechniqueSet.has(key)) { + + tacticTechniqueSet.add(key); + + tacticTechniqueRows.push([ + technique.id, + tactic.id + ]); + } + } + } + /** + * Midlertidig logging mens vi bygger importscriptet. + */ + console.log("Kildedata lest og validert:"); + + console.log( + `ThreatActors: ${actors.length}` ); - await bulkInsert( - connection, - "TechniquePlatform", - ["technique_id", "platform"], - techniquePlatformRows, + console.log( + `Techniques: ${techniques.length}` ); - await bulkInsert( - connection, - "SoftwarePlatform", - ["software_id", "platform"], - softwarePlatformRows, + console.log( + `Tactics: ${tactics.length}` ); - await bulkInsert( - connection, - "SoftwareAlias", - ["software_id", "alias"], - softwareAliasRows, + console.log( + `Software: ${software.length}` ); - await bulkInsert( - connection, - "ThreatActorTechnique", - ["threat_id", "technique_id"], - threatActorTechniqueRows, + console.log( + `Relationships: ${relationships.length}` + ); + console.log( + `ThreatActor sectors: ${threatActorSectorRows.length}` ); - await bulkInsert( - connection, - "ThreatActorSoftware", - ["threat_id", "software_id"], - threatActorSoftwareRows, + console.log( + `ThreatActor aliases: ${threatActorAliasRows.length}` ); - await bulkInsert( - connection, - "TechniqueSoftware", - ["technique_id", "software_id"], - techniqueSoftwareRows, + console.log( + `ThreatActor -> Technique: ${threatActorTechniqueRows.length}` ); - await bulkInsert( - connection, - "TacticTechnique", - ["technique_id", "tactic_id"], - tacticTechniqueRows, + console.log( + `ThreatActor -> Software: ${threatActorSoftwareRows.length}` ); - await connection.commit(); - - // Final DB verification - const tablesToVerify = [ - "ThreatActor", - "ThreatActorSector", - "ThreatActorAlias", - "Technique", - "TechniquePlatform", - "Tactic", - "TacticTechnique", - "Software", - "SoftwarePlatform", - "SoftwareAlias", - "ThreatActorTechnique", - "ThreatActorSoftware", - "TechniqueSoftware", - "User", - "SavedItem", - ]; + console.log( + `Technique -> Software: ${techniqueSoftwareRows.length}` + ); - console.log("\nImport completed successfully."); - console.log("Database row counts:"); + console.log( + `Tactic -> Technique: ${tacticTechniqueRows.length}` + ); - for (const table of tablesToVerify) { - const [rows] = await connection.query( - `SELECT COUNT(*) AS count FROM \`${table}\``, - ); - console.log(` ${table.padEnd(24)} ${String(rows[0].count).padStart(6)}`); + /** + * ========================================================== + * Skriver MITRE-data til databasen + * ========================================================== + * + * Vi henter én connection fra poolen og kjører hele + * importen som en transaction. + * + * Det betyr at enten blir hele importen gjennomført, + * eller så blir alt rullet tilbake hvis noe feiler. + */ + const connection = await pool.getConnection(); + + try { + + await connection.beginTransaction(); + + + /** + * Før vi importerer nye MITRE-data, + * fjerner vi de gamle. + * + * User og SavedItem blir ikke påvirket. + */ + await clearImportedData(connection); + + + /** + * ====================================================== + * Hovedtabeller + * ====================================================== + * + * Disse må settes inn først fordi andre tabeller + * har foreign keys som peker på dem. + */ + + await bulkInsert( + connection, + "ThreatActor", + [ + "threat_id", + "mitre_id", + "name", + "description", + "threat_entry_created", + "country", + "motivation", + "region", + "first_seen", + "last_seen", + "status", + "danger_level", + "x_mitre_deprecated", + ], + threatActorRows + ); + + + await bulkInsert( + connection, + "Technique", + [ + "technique_id", + "mitre_id", + "name", + "description", + "technique_entry_created", + "parent_technique_id", + "x_mitre_deprecated", + ], + techniqueRows + ); + + + await bulkInsert( + connection, + "Tactic", + [ + "tactic_id", + "mitre_id", + "name", + "description", + "tactic_entry_created", + "x_mitre_deprecated", + ], + tacticRows + ); + + + await bulkInsert( + connection, + "Software", + [ + "software_id", + "mitre_id", + "name", + "description", + "software_type", + "software_entry_created", + "x_mitre_deprecated", + ], + softwareRows + ); + + + /** + * ====================================================== + * Parent relationship på Techniques + * ====================================================== + * + * Technique-radene ble først satt inn med: + * + * parent_technique_id = null + * + * Nå som alle Techniques eksisterer kan vi koble + * sub-techniques til parent-techniquen sin. + */ + for ( + const [techniqueId, parentTechniqueId] + of parentByTechniqueId + ) { + + await connection.query( + ` + UPDATE \`Technique\` + SET parent_technique_id = ? + WHERE technique_id = ? + `, + [ + parentTechniqueId, + techniqueId + ] + ); + } + + + /** + * ====================================================== + * Child-tabeller + * ====================================================== + */ + + await bulkInsert( + connection, + "ThreatActorSector", + [ + "threat_id", + "sector" + ], + threatActorSectorRows + ); + + + await bulkInsert( + connection, + "ThreatActorAlias", + [ + "threat_id", + "alias" + ], + threatActorAliasRows + ); + + + await bulkInsert( + connection, + "TechniquePlatform", + [ + "technique_id", + "platform" + ], + techniquePlatformRows + ); + + + await bulkInsert( + connection, + "SoftwarePlatform", + [ + "software_id", + "platform" + ], + softwarePlatformRows + ); + + + await bulkInsert( + connection, + "SoftwareAlias", + [ + "software_id", + "alias" + ], + softwareAliasRows + ); + + + /** + * ====================================================== + * Relasjonstabeller + * ====================================================== + */ + + await bulkInsert( + connection, + "ThreatActorTechnique", + [ + "threat_id", + "technique_id" + ], + threatActorTechniqueRows + ); + + + await bulkInsert( + connection, + "ThreatActorSoftware", + [ + "threat_id", + "software_id" + ], + threatActorSoftwareRows + ); + + + await bulkInsert( + connection, + "TechniqueSoftware", + [ + "technique_id", + "software_id" + ], + techniqueSoftwareRows + ); + + + await bulkInsert( + connection, + "TacticTechnique", + [ + "technique_id", + "tactic_id" + ], + tacticTechniqueRows + ); + + + /** + * Hvis vi kommer helt hit uten feil, + * gjør vi alle endringene permanente. + */ + await connection.commit(); + + console.log( + "\nMITRE-data importert til databasen." + ); + + } catch (error) { + + /** + * Dersom noe feiler underveis, + * går databasen tilbake til slik den var + * før beginTransaction(). + */ + await connection.rollback(); + + throw error; + + } finally { + + /** + * Connection leveres alltid tilbake til poolen. + */ + connection.release(); } - - console.log( - "\nUser and SavedItem are intentionally not populated by MITRE import.", - ); - } catch (error) { - await connection.rollback(); - throw error; - } finally { - connection.release(); - await pool.end(); - } } -main().catch((error) => { - console.error("\nImport failed:"); - console.error(error); - process.exitCode = 1; -}); +main() + .catch((error) => { + + console.error("Import feilet:"); + console.error(error); + + process.exitCode = 1; + + }) + .finally(async () => { + + /** + * Lukker pool når hele import-scriptet + * er ferdig. + */ + await pool.end(); + + }); \ No newline at end of file diff --git a/docs/local-database-setup.md b/docs/local-database-setup.md index 570ca90..72c00ea 100644 --- a/docs/local-database-setup.md +++ b/docs/local-database-setup.md @@ -7,7 +7,7 @@ Følg denne guiden, eller hør med chat hvis noen av stegene ikke funker, gjerne DATABASEN I SEG SELV ER IKKE EN DEL AV REPOEN!!!!! -## Installasjon av MySQL +## 1.Installasjon av MySQL ### macOS - i terminalen @@ -41,7 +41,7 @@ Start-Service [navnet fra "Get-Service *mysql*"] Get-Service [navnet fra "Get-Service *mysql*"] ``` -## Koble til my SQL +## 2.Koble til my SQL ### Powershel/macOS - i den samme terminalen som fra steg 1 @@ -53,7 +53,7 @@ mysql -u root Nå er du inne i selve database CLI -## Opprett databasen og en bruker i sql CLI +## 3.Opprett databasen og en bruker i sql CLI Lim inn dette her inn i CLI @@ -72,7 +72,7 @@ EXIT; din bruker er nå `cyberuser` og passorder er nå `cyberpass` -## Opprett en forbindelse: VSCode <-> CyberDB +## 4.Opprett en forbindelse: VSCode <-> CyberDB Gå til VScode -> Åpne fanen på venstre panelet som er Database og trykk på Add Connection og fyll ut dette her: @@ -82,7 +82,10 @@ Deretter trykk Connect! Med dette så kan du nå leke med Databasen akkuratt som i IDAT2002, opprett en ny query og skriv in ønskede selects -## Trenger mysql2 og typescript inne i på /database/ for å kjøre import-scriptet + + + +## 4. Installer prosjekt-dependencies De er allerede lagt inn som dependencies i root package.json. Derfor bare kjør: @@ -95,7 +98,41 @@ npm install -g pnpm #hvis pnpm er ikke installert på windowsmaskinen pnpm install ``` -## Importerer DB struktur og data +## 5. Opprett databasestrukturen med Drizzle + +### (Hvis du har den gamle databasen fra før Drizzle-refaktoreringen, slett/reopprett cyberdb først.) + +Databasestrukturen er definert i: `backend/src/db/schema.ts` + +Ferdige migrations ligger i: `backend/drizzle/`, en migration er en SQL kode generert av Drizzle som følger vår `schema.ts`når den oppretter databasestrukturen. + +Når prosjektet settes opp på en ny PC skal man **ikke generere en ny migration**. Bruk den som allerede finnes i repoen. + +`Notat: for å faktisk generere en ny migration bruke pnpm drizzle-kit generate --name=navn-på-endringen` + + +### Fra backend/ i vscode + +Kjør + +### Mac +```bash +DB_HOST=localhost DB_PORT=3306 DB_USER=cyberuser DB_PASSWORD=cyberpass DB_NAME=cyberdb pnpm exec drizzle-kit migrate +``` +### Windows + +```powershell +$env:DB_HOST="localhost" +$env:DB_PORT="3306" +$env:DB_USER="cyberuser" +$env:DB_PASSWORD="cyberpass" +$env:DB_NAME="cyberdb" +pnpm exec drizzle-kit migrate +``` + +Etter dette skal databasen inneholde prosjektets tabeller. + +## 6. Importerer Data inn i tabellene Import script ligger her: @@ -129,8 +166,8 @@ $env:DB_NAME="cyberdb" pnpm exec tsx database/import-mitre.ts ``` -## Verifikasjon - +## 7.Verifikasjon + Hvis import funker så ser man: `Import completed successfully.`. Så gå til VScode og se alle tabellene. Hvis du ikke ser det, spør chat, gi chat errormessage + denne MD guiden. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fba7e06..cf688a1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,16 +27,22 @@ importers: '@apollo/server': specifier: ^5.5.1 version: 5.5.1(graphql@17.0.2) + drizzle-orm: + specifier: ^0.45.3 + version: 0.45.3(mysql2@3.24.4(@types/node@26.6.2)) graphql: specifier: ^17.0.2 version: 17.0.2 mysql2: specifier: ^3.24.4 - version: 3.24.4 + version: 3.24.4(@types/node@26.6.2) devDependencies: '@types/node': specifier: ^26.6.2 version: 26.6.2 + drizzle-kit: + specifier: ^0.31.11 + version: 0.31.11 tsx: specifier: ^4.23.15 version: 4.23.15 @@ -269,156 +275,455 @@ packages: '@cacheable/utils@2.5.0': resolution: {integrity: sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==} + '@drizzle-team/brocli@0.10.2': + resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + + '@esbuild-kit/core-utils@3.3.2': + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.hirok.io' + + '@esbuild-kit/esm-loader@2.6.5': + resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.hirok.io' + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.28.2': resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.28.2': resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.28.2': resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.28.2': resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.28.2': resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.28.2': resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.28.2': resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.28.2': resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.28.2': resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.28.2': resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.28.2': resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.28.2': resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.28.2': resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.28.2': resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.28.2': resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.28.2': resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.28.2': resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-arm64@0.28.2': resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.28.2': resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-arm64@0.28.2': resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.28.2': resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/openharmony-arm64@0.28.2': resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.28.2': resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.28.2': resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.28.2': resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.28.2': resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} engines: {node: '>=18'} @@ -991,6 +1296,9 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -1071,6 +1379,105 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + drizzle-kit@0.31.11: + resolution: {integrity: sha512-YCYqxTLIB2OCqf6w9/Vef13baBVbwQIPcbT4Y56AfO6B9ajZhDhD8Jkveg/hJvT/iuMloKD/IYYkyMMNhr7Kqg==} + hasBin: true + + drizzle-orm@0.45.3: + resolution: {integrity: sha512-CAloER21cDdcgZ1OmjrZX82EeRNsdP+y0onX/eyoYDaYzki/adrAa91lzM5jWh7zZPK4qUbw/6LGv9J+A+uizA==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=4' + '@electric-sql/pglite': '>=0.2.0' + '@libsql/client': '>=0.10.0' + '@libsql/client-wasm': '>=0.10.0' + '@neondatabase/serverless': '>=0.10.0' + '@netlify/db': '>=0.4.0' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1.13' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/sql.js': '*' + '@upstash/redis': '>=1.34.7' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=14.0.0' + gel: '>=2' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@libsql/client-wasm': + optional: true + '@neondatabase/serverless': + optional: true + '@netlify/db': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/sql.js': + optional: true + '@upstash/redis': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + gel: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -1100,6 +1507,16 @@ packages: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} engines: {node: '>= 0.4'} + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + esbuild@0.28.2: resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} engines: {node: '>=18'} @@ -1238,6 +1655,9 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + get-tsconfig@4.14.3: + resolution: {integrity: sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==} + glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} @@ -1630,6 +2050,9 @@ packages: resolution: {integrity: sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==} engines: {node: '>=0.10.0'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + retry@0.13.1: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} engines: {node: '>= 4'} @@ -1703,6 +2126,13 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + sql-escaper@1.5.2: resolution: {integrity: sha512-6CKD38c31SENivxOADeMNLdukOnUxUcflKtzVWzace7Riv1v7cAEym5Cx9Q7gYZ3ezIJI7ZpqecQq8cqNeSSFg==} engines: {bun: '>=1.0.0', deno: '>=2.0.0', node: '>=12.0.0'} @@ -2145,81 +2575,237 @@ snapshots: hashery: 1.5.1 keyv: 5.6.0 + '@drizzle-team/brocli@0.10.2': {} + + '@esbuild-kit/core-utils@3.3.2': + dependencies: + esbuild: 0.18.20 + source-map-support: 0.5.21 + + '@esbuild-kit/esm-loader@2.6.5': + dependencies: + '@esbuild-kit/core-utils': 3.3.2 + get-tsconfig: 4.14.3 + + '@esbuild/aix-ppc64@0.25.12': + optional: true + '@esbuild/aix-ppc64@0.28.2': optional: true + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + '@esbuild/android-arm64@0.28.2': optional: true + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + '@esbuild/android-arm@0.28.2': optional: true + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + '@esbuild/android-x64@0.28.2': optional: true + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + '@esbuild/darwin-arm64@0.28.2': optional: true + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + '@esbuild/darwin-x64@0.28.2': optional: true + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + '@esbuild/freebsd-arm64@0.28.2': optional: true + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + '@esbuild/freebsd-x64@0.28.2': optional: true + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + '@esbuild/linux-arm64@0.28.2': optional: true + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + '@esbuild/linux-arm@0.28.2': optional: true + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + '@esbuild/linux-ia32@0.28.2': optional: true + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + '@esbuild/linux-loong64@0.28.2': optional: true + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + '@esbuild/linux-mips64el@0.28.2': optional: true + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + '@esbuild/linux-ppc64@0.28.2': optional: true + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + '@esbuild/linux-riscv64@0.28.2': optional: true + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + '@esbuild/linux-s390x@0.28.2': optional: true + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + '@esbuild/linux-x64@0.28.2': optional: true + '@esbuild/netbsd-arm64@0.25.12': + optional: true + '@esbuild/netbsd-arm64@0.28.2': optional: true + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + '@esbuild/netbsd-x64@0.28.2': optional: true + '@esbuild/openbsd-arm64@0.25.12': + optional: true + '@esbuild/openbsd-arm64@0.28.2': optional: true + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + '@esbuild/openbsd-x64@0.28.2': optional: true + '@esbuild/openharmony-arm64@0.25.12': + optional: true + '@esbuild/openharmony-arm64@0.28.2': optional: true + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + '@esbuild/sunos-x64@0.28.2': optional: true + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + '@esbuild/win32-arm64@0.28.2': optional: true + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + '@esbuild/win32-ia32@0.28.2': optional: true + '@esbuild/win32-x64@0.18.20': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + '@esbuild/win32-x64@0.28.2': optional: true @@ -2689,6 +3275,8 @@ snapshots: node-releases: 2.0.57 update-browserslist-db: 1.3.3(browserslist@4.29.1) + buffer-from@1.1.2: {} + bytes@3.1.2: {} cacheable@2.5.0: @@ -2761,6 +3349,17 @@ snapshots: detect-libc@2.1.2: {} + drizzle-kit@0.31.11: + dependencies: + '@drizzle-team/brocli': 0.10.2 + '@esbuild-kit/esm-loader': 2.6.5 + esbuild: 0.25.12 + tsx: 4.23.15 + + drizzle-orm@0.45.3(mysql2@3.24.4(@types/node@26.6.2)): + optionalDependencies: + mysql2: 3.24.4(@types/node@26.6.2) + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -2783,6 +3382,60 @@ snapshots: dependencies: es-errors: 1.3.0 + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + esbuild@0.28.2: optionalDependencies: '@esbuild/aix-ppc64': 0.28.2 @@ -2974,6 +3627,10 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.2 + get-tsconfig@4.14.3: + dependencies: + resolve-pkg-maps: 1.0.0 + glob-parent@6.0.2: dependencies: is-glob: 4.0.3 @@ -3279,6 +3936,8 @@ snapshots: react@19.3.0: {} + resolve-pkg-maps@1.0.0: {} + retry@0.13.1: {} rolldown@1.2.10: @@ -3369,6 +4028,13 @@ snapshots: source-map-js@1.2.1: {} + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + sql-escaper@1.5.2: {} stackback@0.0.2: {}