diff --git a/README.md b/README.md index f758c7b..27eba3c 100644 --- a/README.md +++ b/README.md @@ -65,10 +65,9 @@ Frontend utvikles med React og TypeScript og settes opp med Vite. Frontend er an - brukerinteraksjon I den nåværende prototypen brukes hovedsakelig mock-data og statiske eksempeldata. ### Backend -Backend skal utvikles med Node.js og TypeScript og tilby et GraphQL API som frontend kan bruke for å hente og endre data. -Backend er foreløpig ikke implementert. +Backend er utviklet med Node.js og TypeScript og tilbyr et GraphQL API. Software-katalogen og detaljsidene henter MITRE ATT&CK-data fra MySQL-databasen. ### Database -Det er planlagt å bruke MySQL som database. MITRE ATT&CK-datasettet skal importeres til databasen ved hjelp av et import-script som transformerer dataene fra det opprinnelige JSON-formatet til databaseskjemaet vårt. Database og backend skal senere kjøres på VMen. +Prosjektet bruker MySQL. MITRE ATT&CK-data importeres fra JSON-datasettet med import-scriptet i `database/import-mitre.ts`. Den lokale databasen må være satt opp og dataene importert før databasefunksjonene kan brukes. Se [lokal databaseoppsett](./docs/local-database-setup.md). ## Nåværende status og prototype Det som er implementert frem til nå: - Oppsett av utviklingsmiljø for frontend, backend og database @@ -92,41 +91,16 @@ KI har blitt brukt til veiledning i store deler av prosjektet. Ofte har ideer om ## Kjøre prosjektet lokalt -## 1.Dependencies -Installer dependencies, fra Rooten: - -```bash -pnpm install -``` - -## 2.Database - -**Følg guiden : `docs/local-database-setup.md`** - -`Resultat`: Kjørende CyberDB database lokalt på maskinen -## 3.Backend - -Opprett en ny `Terminal 1`: - -```bash -cd backend -pnpm run dev +```sh +pnpm --filter backend dev ``` -`Resultat`: Kjørende Backend med Apollo GraphQL Server kjører på http://localhost:3001/. - -## 4.Frontend - -Opprett en ny `Terminal 2`: - -```bash -cd frontend -pnpm run dev -``` - -`Resultat`: Kjørende Frontend på http://localhost:5173/project2 eller noe sånt. - - +Sørg for at MySQL kjører, og at databaseskjemaet og MITRE-dataene er satt opp +som beskrevet i [lokal databaseoppsett](./docs/local-database-setup.md). +Åpne URL-en Vite skriver ut i frontend-terminalen (vanligvis +`http://localhost:5173`). Frontenden bruker GraphQL API-et på +`http://localhost:3001`. Den lokale frontend-ruten er `/`, mens produksjonsruten +er satt opp for publisering under `/project2`. diff --git a/backend/src/graphql/resolvers/index.ts b/backend/src/graphql/resolvers/index.ts index 5443653..5bdee49 100644 --- a/backend/src/graphql/resolvers/index.ts +++ b/backend/src/graphql/resolvers/index.ts @@ -14,12 +14,13 @@ import { threatActorResolvers } from "./threatActorResolver.js"; import { techniqueResolvers } from "./techniqueResolver.js"; - +import { softwareResolvers } from "./softwareResolver.js"; export const resolvers = { Query: { ...threatActorResolvers.Query, ...techniqueResolvers.Query, + ...softwareResolvers.Query, }, }; \ No newline at end of file diff --git a/backend/src/graphql/resolvers/softwareResolver.ts b/backend/src/graphql/resolvers/softwareResolver.ts index c0d3458..cc3ebcb 100644 --- a/backend/src/graphql/resolvers/softwareResolver.ts +++ b/backend/src/graphql/resolvers/softwareResolver.ts @@ -1,12 +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 +import { + getAllSoftware, + getSoftwareByMitreId, +} from "../../repositories/softwareRepository.js"; + +export const softwareResolvers = { + Query: { + software: async () => getAllSoftware(), + softwareByMitreId: async (_parent: unknown, args: { mitreId: string }) => + getSoftwareByMitreId(args.mitreId), + }, +}; \ No newline at end of file diff --git a/backend/src/graphql/typeDefs.ts b/backend/src/graphql/typeDefs.ts index 15416a8..c2643ef 100644 --- a/backend/src/graphql/typeDefs.ts +++ b/backend/src/graphql/typeDefs.ts @@ -19,75 +19,26 @@ export const typeDefs = `#graphql motivation: String! } - type ThreatActorGridInfo { - items: [ThreatActorGrid!]! - totalCount: Int! - } - - type AssociatedItem { - mitreId: String! - name: String! - } - type AssociatedItemsPage { - items: [AssociatedItem!]! - totalCount: Int! } - type ThreatActorDetailed { - threatId: String! + type Software { + id: String! mitreId: String! name: String! description: String! - threatEntryCreated: String! - country: String! - motivation: String! - region: String! - firstSeen: String! - lastSeen: String! - status: String! - dangerLevel: String! - xMitreDeprecated: Boolean! - + type: String! aliases: [String!]! - sectors: [String!]! - } - - type Technique { - mitreId: String! - name: String! - description: String! + platforms: [String!]! + associatedTechniques: [RelatedSoftwareEntity!]! + usedByThreatActors: [RelatedSoftwareEntity!]! } + # Operasjoner frontend kan spørre API-et om type Query { - ######################################## ThreatActors vvvv - - # Henter alle threat actors som trengs i grid - allThreatActorsGrid(limit: Int!, offset: Int!): ThreatActorGridInfo! - # Henter alt nødvending info for ThreatActorDetailed i frontend - getThreatActorByMitreId(mitreId: String!): ThreatActorDetailed - - getThreatActorTechniques(mitreId: String!, limit: Int!, offset: Int!): AssociatedItemsPage! - - getThreatActorSoftwares(mitreId: String!, limit: Int!, offset: Int!): AssociatedItemsPage! - - ######################################## Techinques vvvv - - # Henter alle Techniques allTechniques: [Technique!]! - - # Henter én Technique basert på MITRE-ID techniqueByMitreId(mitreId: String!): Technique - - ######################################## Software vvvv - - - - ######################################## Tactics vvvv - - } - `; \ No newline at end of file diff --git a/backend/src/repositories/softwareRepository.ts b/backend/src/repositories/softwareRepository.ts index b6f2fc5..7fa4347 100644 --- a/backend/src/repositories/softwareRepository.ts +++ b/backend/src/repositories/softwareRepository.ts @@ -1,16 +1,134 @@ -/** - * 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 +import { eq, inArray } from "drizzle-orm"; + +import { db } from "../db/connection.js"; +import { + software, + softwareAlias, + softwarePlatform, + technique, + techniqueSoftware, + threatActor, + threatActorSoftware, +} from "../db/schema.js"; + +async function getSoftwareMetadata(softwareIds: string[]) { + if (softwareIds.length === 0) { + return { + aliases: [], + platforms: [], + associatedTechniques: [], + usedByThreatActors: [], + }; + } + + const [aliases, platforms, associatedTechniques, usedByThreatActors] = + await Promise.all([ + db + .select({ + softwareId: softwareAlias.software_id, + alias: softwareAlias.alias, + }) + .from(softwareAlias) + .where(inArray(softwareAlias.software_id, softwareIds)), + db + .select({ + softwareId: softwarePlatform.software_id, + platform: softwarePlatform.platform, + }) + .from(softwarePlatform) + .where(inArray(softwarePlatform.software_id, softwareIds)), + db + .select({ + softwareId: techniqueSoftware.software_id, + id: technique.technique_id, + mitreId: technique.mitre_id, + name: technique.name, + }) + .from(techniqueSoftware) + .innerJoin( + technique, + eq(techniqueSoftware.technique_id, technique.technique_id), + ) + .where(inArray(techniqueSoftware.software_id, softwareIds)), + db + .select({ + softwareId: threatActorSoftware.software_id, + id: threatActor.threat_id, + mitreId: threatActor.mitre_id, + name: threatActor.name, + }) + .from(threatActorSoftware) + .innerJoin( + threatActor, + eq(threatActorSoftware.threat_id, threatActor.threat_id), + ) + .where(inArray(threatActorSoftware.software_id, softwareIds)), + ]); + + return { aliases, platforms, associatedTechniques, usedByThreatActors }; +} + +export async function getAllSoftware() { + const entries = await db + .select({ + id: software.software_id, + mitreId: software.mitre_id, + name: software.name, + description: software.description, + type: software.software_type, + }) + .from(software) + .orderBy(software.name); + + const metadata = await getSoftwareMetadata(entries.map((entry) => entry.id)); + + return entries.map((entry) => ({ + ...entry, + aliases: metadata.aliases + .filter((alias) => alias.softwareId === entry.id) + .map((alias) => alias.alias), + platforms: metadata.platforms + .filter((platform) => platform.softwareId === entry.id) + .map((platform) => platform.platform), + })); +} + +export async function getSoftwareByMitreId(mitreId: string) { + const [entry] = await db + .select({ + id: software.software_id, + mitreId: software.mitre_id, + name: software.name, + description: software.description, + type: software.software_type, + }) + .from(software) + .where(eq(software.mitre_id, mitreId)) + .limit(1); + + if (!entry) { + return null; + } + + const metadata = await getSoftwareMetadata([entry.id]); + + return { + ...entry, + aliases: metadata.aliases.map((alias) => alias.alias), + platforms: metadata.platforms.map((platform) => platform.platform), + associatedTechniques: metadata.associatedTechniques.map( + ({ id, mitreId: relatedMitreId, name }) => ({ + id, + mitreId: relatedMitreId, + name, + }), + ), + usedByThreatActors: metadata.usedByThreatActors.map( + ({ id, mitreId: relatedMitreId, name }) => ({ + id, + mitreId: relatedMitreId, + name, + }), + ), + }; +} \ No newline at end of file diff --git a/frontend/src/data/softwareData.ts b/frontend/src/data/softwareData.ts index def4431..a84d715 100644 --- a/frontend/src/data/softwareData.ts +++ b/frontend/src/data/softwareData.ts @@ -1,15 +1,30 @@ import { graphqlRequest } from "../api/graphql"; -import { mockSoftware, type SoftwareEntry } from "./softwareMockData"; -export type { SoftwareEntry } from "./softwareMockData"; - -export const USE_SOFTWARE_MOCK_DATA = false; // ENDRE DENNE FOR Å IKKE BRUKE MOCK!!! - -export type SoftwareCatalogEntry = Omit; +export type RelatedSoftwareEntry = { + id: string; + mitreId: string; + name: string; +}; + +export type SoftwareEntry = { + id: string; + mitreId: string; + name: string; + type: "TOOL" | "MALWARE"; + summary?: string; + description: string; + aliases: string[]; + platforms: string[]; + associatedTechniques: RelatedSoftwareEntry[]; + usedByThreatActors: RelatedSoftwareEntry[]; +}; + +export type SoftwareCatalogEntry = Omit< + SoftwareEntry, + "associatedTechniques" | "usedByThreatActors" +>; export async function getSoftwareCatalog(): Promise { - if (USE_SOFTWARE_MOCK_DATA) return mockSoftware; - const data = await graphqlRequest<{ software: SoftwareCatalogEntry[] }>(` query SoftwareCatalog { software { @@ -28,10 +43,6 @@ export async function getSoftwareCatalog(): Promise { } export async function getSoftwareByMitreId(mitreId: string): Promise { - if (USE_SOFTWARE_MOCK_DATA) { - return mockSoftware.find((entry) => entry.mitreId === mitreId) ?? null; - } - const data = await graphqlRequest<{ softwareByMitreId: SoftwareEntry | null }>(` query SoftwareDetail($mitreId: String!) { softwareByMitreId(mitreId: $mitreId) {