From 763eea226fd27f9f0fdf35ed130a16fd2d5740cf Mon Sep 17 00:00:00 2001 From: alea Date: Fri, 16 Oct 2026 13:40:59 +0200 Subject: [PATCH 1/6] La til tactics og platforms til technique queries --- .../TechniqueCard/TechniqueCard.tsx | 13 ++++++------- .../TechniqueGrid/TechniqueGrid.tsx | 8 ++++---- .../src/graphql/queries/techniqueQueries.ts | 2 ++ .../src/pages/Techniques/TechniqueDetailed.tsx | 18 ++++++++++++++---- frontend/src/pages/Techniques/Techniques.tsx | 4 ++-- 5 files changed, 28 insertions(+), 17 deletions(-) diff --git a/frontend/src/components/techniques/allTechniques/TechniqueCard/TechniqueCard.tsx b/frontend/src/components/techniques/allTechniques/TechniqueCard/TechniqueCard.tsx index f501ed2..5b0e1b0 100644 --- a/frontend/src/components/techniques/allTechniques/TechniqueCard/TechniqueCard.tsx +++ b/frontend/src/components/techniques/allTechniques/TechniqueCard/TechniqueCard.tsx @@ -8,16 +8,15 @@ type TechniqueCardProps = { mitreId: string; name: string; description: string; - tactic?: string; - platform?: string; + tactics: string[]; + platforms: string[]; }; - function TechniqueCard({ mitreId, name, description, - tactic, - platform, + tactics, + platforms, }: TechniqueCardProps) { const { icon: TechniqueIcon, @@ -44,11 +43,11 @@ function TechniqueCard({
- {tactic ?? "Unknown"} + {tactics[0] ?? "Unknown"} - {platform ?? "Unknown"} + {platforms[0] ?? "Unknown"}
diff --git a/frontend/src/components/techniques/allTechniques/TechniqueGrid/TechniqueGrid.tsx b/frontend/src/components/techniques/allTechniques/TechniqueGrid/TechniqueGrid.tsx index c578097..2018624 100644 --- a/frontend/src/components/techniques/allTechniques/TechniqueGrid/TechniqueGrid.tsx +++ b/frontend/src/components/techniques/allTechniques/TechniqueGrid/TechniqueGrid.tsx @@ -5,8 +5,8 @@ type Technique = { mitreId: string; name: string; description: string; - tactic?: string; - platform?: string; + tactics: string[]; + platforms: string[]; }; type TechniqueGridProps = { @@ -24,8 +24,8 @@ function TechniqueGrid({ mitreId={technique.mitreId} name={technique.name} description={technique.description} - tactic={technique.tactic ?? "Unknown"} - platform={technique.platform ?? "Unknown"} + tactics={technique.tactics ?? ["Unknown"]} + platforms={technique.platforms ?? ["Unknown"]} /> ))} diff --git a/frontend/src/graphql/queries/techniqueQueries.ts b/frontend/src/graphql/queries/techniqueQueries.ts index d47a990..cd43e9d 100644 --- a/frontend/src/graphql/queries/techniqueQueries.ts +++ b/frontend/src/graphql/queries/techniqueQueries.ts @@ -6,6 +6,8 @@ export const GET_TECHNIQUES = gql` mitreId name description + tactics + platforms } } `; \ No newline at end of file diff --git a/frontend/src/pages/Techniques/TechniqueDetailed.tsx b/frontend/src/pages/Techniques/TechniqueDetailed.tsx index ac0e235..10f030d 100644 --- a/frontend/src/pages/Techniques/TechniqueDetailed.tsx +++ b/frontend/src/pages/Techniques/TechniqueDetailed.tsx @@ -17,6 +17,8 @@ export const GET_TECHNIQUE = gql` mitreId name description + platforms + tactics } } `; @@ -25,6 +27,8 @@ interface Technique { mitreId: string; name: string; description: string; + platforms: string[]; + tactics: string[]; } interface GetTechniqueData { @@ -126,8 +130,11 @@ function TechniqueDetailed() { @@ -135,8 +142,11 @@ function TechniqueDetailed() { diff --git a/frontend/src/pages/Techniques/Techniques.tsx b/frontend/src/pages/Techniques/Techniques.tsx index 5eda3af..73e07e7 100644 --- a/frontend/src/pages/Techniques/Techniques.tsx +++ b/frontend/src/pages/Techniques/Techniques.tsx @@ -9,8 +9,8 @@ interface Technique { mitreId: string; name: string; description: string; - tactic?: string; - platform?: string; + tactic: string[]; + platform: string[]; } interface GetTechniquesData { From c89887cc051625c63e872bfe6915553844ae99a4 Mon Sep 17 00:00:00 2001 From: alea Date: Fri, 16 Oct 2026 13:43:21 +0200 Subject: [PATCH 2/6] La til tactics og platforms til technique queries backend endring --- backend/src/graphql/typeDefs.ts | 3 + .../src/repositories/techniqueRepository.ts | 170 +++++++++++++----- 2 files changed, 129 insertions(+), 44 deletions(-) diff --git a/backend/src/graphql/typeDefs.ts b/backend/src/graphql/typeDefs.ts index 15416a8..116fd55 100644 --- a/backend/src/graphql/typeDefs.ts +++ b/backend/src/graphql/typeDefs.ts @@ -57,6 +57,9 @@ export const typeDefs = `#graphql mitreId: String! name: String! description: String! + + tactics: [String!]! + platforms: [String!]! } type Query { diff --git a/backend/src/repositories/techniqueRepository.ts b/backend/src/repositories/techniqueRepository.ts index 52d62f6..267182e 100644 --- a/backend/src/repositories/techniqueRepository.ts +++ b/backend/src/repositories/techniqueRepository.ts @@ -1,63 +1,145 @@ -/** - * 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!!!! - */ - import { eq } from "drizzle-orm"; import { db } from "../db/connection.js"; -import { technique } from "../db/schema.js"; +import { + technique, + techniquePlatform, + tacticTechnique, + tactic, +} from "../db/schema.js"; /** * Henter alle Techniques fra databasen. */ export async function getAllTechniques() { - return db + const techniques = await db + .select({ + techniqueId: technique.technique_id, + mitreId: technique.mitre_id, + name: technique.name, + description: technique.description, + }) + .from(technique); + + return Promise.all( + techniques.map(async (currentTechnique) => { + const platforms = await db + .select({ + platform: techniquePlatform.platform, + }) + .from(techniquePlatform) + .where( + eq( + techniquePlatform.technique_id, + currentTechnique.techniqueId, + ), + ); + + const tactics = await db .select({ - mitreId: technique.mitre_id, - name: technique.name, - description: technique.description, - /*tactic: technique.tactic, - platform: technique.platform,*/ + name: tactic.name, }) - .from(technique); + .from(tacticTechnique) + .leftJoin( + tactic, + eq( + tacticTechnique.tactic_id, + tactic.tactic_id, + ), + ) + .where( + eq( + tacticTechnique.technique_id, + currentTechnique.techniqueId, + ), + ); + + return { + mitreId: currentTechnique.mitreId, + name: currentTechnique.name, + description: currentTechnique.description, + + tactics: tactics.map( + (t) => t.name, + ), + + platforms: platforms.map( + (p) => p.platform, + ), + }; + }), + ); } + /** * Henter én Technique basert på MITRE-ID. - * - * Eksempel: - * T1566 -> Phishing */ export async function getTechniqueByMitreId( - mitreId: string, + mitreId: string, ) { - const [result] = await db - .select({ - mitreId: technique.mitre_id, - name: technique.name, - description: technique.description, - }) - .from(technique) - .where( - eq( - technique.mitre_id, - mitreId, - ), - ) - .limit(1); + const [result] = await db + .select({ + techniqueId: technique.technique_id, + mitreId: technique.mitre_id, + name: technique.name, + description: technique.description, + }) + .from(technique) + .where( + eq( + technique.mitre_id, + mitreId, + ), + ) + .limit(1); + + if (!result) { + return null; + } + + const platforms = await db + .select({ + platform: techniquePlatform.platform, + }) + .from(techniquePlatform) + .where( + eq( + techniquePlatform.technique_id, + result.techniqueId, + ), + ); + + const tactics = await db + .select({ + name: tactic.name, + }) + .from(tacticTechnique) + .leftJoin( + tactic, + eq( + tacticTechnique.tactic_id, + tactic.tactic_id, + ), + ) + .where( + eq( + tacticTechnique.technique_id, + result.techniqueId, + ), + ); + + return { + mitreId: result.mitreId, + name: result.name, + description: result.description, + + platforms: platforms.map( + (platform) => platform.platform, + ), - return result ?? null; + tactics: tactics.map( + (tactic) => tactic.name, + ), + }; } \ No newline at end of file From 38a0826e9da59e3b7466ddb08dc4175ddf6822d8 Mon Sep 17 00:00:00 2001 From: alea Date: Fri, 16 Oct 2026 14:39:35 +0200 Subject: [PATCH 3/6] =?UTF-8?q?Endret=20technique=20repository=20og=20la?= =?UTF-8?q?=20til=20forbedret=20tactics/platforms=20st=C3=B8tte?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../graphql/resolvers/techniqueResolver.ts | 4 +- .../src/repositories/techniqueRepository.ts | 172 ++++++++++++------ 2 files changed, 119 insertions(+), 57 deletions(-) diff --git a/backend/src/graphql/resolvers/techniqueResolver.ts b/backend/src/graphql/resolvers/techniqueResolver.ts index d4d1fe2..c04340e 100644 --- a/backend/src/graphql/resolvers/techniqueResolver.ts +++ b/backend/src/graphql/resolvers/techniqueResolver.ts @@ -11,7 +11,7 @@ * GraphQL -> Resolver -> Repository -> Drizzle -> MySQL */ import { - getAllTechniques, + getAllTechniquesGrid, getTechniqueByMitreId, } from "../../repositories/techniqueRepository.js"; @@ -21,7 +21,7 @@ export const techniqueResolvers = { * Henter alle Techniques. */ allTechniques: async () => { - return getAllTechniques(); + return getAllTechniquesGrid(); }, /** diff --git a/backend/src/repositories/techniqueRepository.ts b/backend/src/repositories/techniqueRepository.ts index 267182e..035a173 100644 --- a/backend/src/repositories/techniqueRepository.ts +++ b/backend/src/repositories/techniqueRepository.ts @@ -1,4 +1,4 @@ -import { eq } from "drizzle-orm"; +import { eq, count, sql } from "drizzle-orm"; import { db } from "../db/connection.js"; import { @@ -6,69 +6,64 @@ import { techniquePlatform, tacticTechnique, tactic, + software, + techniqueSoftware, + threatActor, + threatActorTechnique } from "../db/schema.js"; -/** - * Henter alle Techniques fra databasen. - */ -export async function getAllTechniques() { - const techniques = await db +export async function getAllTechniquesGrid( + limit: number = 12, + offset: number = 0, +) { + const techniqueSelect = db .select({ - techniqueId: technique.technique_id, mitreId: technique.mitre_id, name: technique.name, - description: technique.description, + + tactic: sql` + COALESCE(( + SELECT ${tactic.name} + FROM ${tacticTechnique} + INNER JOIN ${tactic} + ON ${tacticTechnique.tactic_id} = ${tactic.tactic_id} + WHERE ${tacticTechnique.technique_id} = + ${sql.identifier("Technique")}.${sql.identifier("technique_id")} + LIMIT 1 + ), 'Unknown') + `, + + platform: sql` + COALESCE(( + SELECT ${techniquePlatform.platform} + FROM ${techniquePlatform} + WHERE ${techniquePlatform.technique_id} = + ${sql.identifier("Technique")}.${sql.identifier("technique_id")} + LIMIT 1 + ), 'Unknown') + `, }) - .from(technique); + .from(technique) + .orderBy(technique.mitre_id) + .limit(limit) + .offset(offset); - return Promise.all( - techniques.map(async (currentTechnique) => { - const platforms = await db - .select({ - platform: techniquePlatform.platform, - }) - .from(techniquePlatform) - .where( - eq( - techniquePlatform.technique_id, - currentTechnique.techniqueId, - ), - ); - - const tactics = await db + const [techniques, countResult] = + await Promise.all([ + techniqueSelect, + + db .select({ - name: tactic.name, + totalCount: count(), }) - .from(tacticTechnique) - .leftJoin( - tactic, - eq( - tacticTechnique.tactic_id, - tactic.tactic_id, - ), - ) - .where( - eq( - tacticTechnique.technique_id, - currentTechnique.techniqueId, - ), - ); - - return { - mitreId: currentTechnique.mitreId, - name: currentTechnique.name, - description: currentTechnique.description, - - tactics: tactics.map( - (t) => t.name, - ), + .from(technique), + ]); - platforms: platforms.map( - (p) => p.platform, - ), - }; - }), - ); + return { + items: techniques, + totalCount: + countResult[0]?.totalCount ?? 0, + }; } @@ -142,4 +137,71 @@ export async function getTechniqueByMitreId( (tactic) => tactic.name, ), }; -} \ No newline at end of file +} + +export async function getTechniqueSubTechniques( + mitreId: string, +) { + const [parent] = await db + .select({ + techniqueId: technique.technique_id, + }) + .from(technique) + .where(eq(technique.mitre_id, mitreId)) + .limit(1); + + if (!parent) { + return []; + } + + return db + .select({ + mitreId: technique.mitre_id, + name: technique.name, + }) + .from(technique) + .where( + eq( + technique.parent_technique_id, + parent.techniqueId, + ), + ); +} + +export async function getTechniqueSoftware( + mitreId: string, +) { + return db + .select({ + mitreId: software.mitre_id, + name: software.name, + }) + .from(techniqueSoftware) + .innerJoin( + software, + eq( + techniqueSoftware.software_id, + software.software_id, + ), + ) + .where(eq(techniqueSoftware.technique_id, mitreId)); +} + +export async function getTechniqueThreatActors( + mitreId: string, +) { + return db + .select({ + threatActor: threatActor.name, + }) + .from(threatActor) + .innerJoin( + threatActorTechnique, + eq( + threatActorTechnique.threat_id, + threatActor.threat_id, + ), + ) + + .where(eq(threatActorTechnique.technique_id, mitreId)); +} From 44d4a2d62b6abe65d5ea0da24acd72b69bcf9905 Mon Sep 17 00:00:00 2001 From: alea Date: Fri, 16 Oct 2026 17:10:12 +0200 Subject: [PATCH 4/6] =?UTF-8?q?Endret=20p=C3=A5=20queriers,=20la=20til=20p?= =?UTF-8?q?aginering,=20knyttet=20frontend=20med=20nye=20kommandoer=20i=20?= =?UTF-8?q?backend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../graphql/resolvers/techniqueResolver.ts | 36 +++++++++- backend/src/graphql/typeDefs.ts | 30 ++++++++- .../src/repositories/techniqueRepository.ts | 23 ++++--- .../TechniqueCard/TechniqueCard.tsx | 25 +++---- .../TechniqueCard/getTechniqueCardVisual.tsx | 67 +++++++++++++------ .../TechniqueGrid/TechniqueGrid.css | 52 +++++++++++++- .../TechniqueGrid/TechniqueGrid.tsx | 59 ++++++++++++++-- .../src/graphql/queries/techniqueQueries.ts | 22 ++++-- frontend/src/pages/Techniques/Techniques.tsx | 39 ++++++++--- 9 files changed, 280 insertions(+), 73 deletions(-) diff --git a/backend/src/graphql/resolvers/techniqueResolver.ts b/backend/src/graphql/resolvers/techniqueResolver.ts index c04340e..41a10a1 100644 --- a/backend/src/graphql/resolvers/techniqueResolver.ts +++ b/backend/src/graphql/resolvers/techniqueResolver.ts @@ -13,6 +13,9 @@ import { getAllTechniquesGrid, getTechniqueByMitreId, + getTechniqueSubTechniques, + getTechniqueSoftware, + getTechniqueThreatActors } from "../../repositories/techniqueRepository.js"; export const techniqueResolvers = { @@ -20,8 +23,17 @@ export const techniqueResolvers = { /** * Henter alle Techniques. */ - allTechniques: async () => { - return getAllTechniquesGrid(); + allTechniques: async ( + _parent: unknown, + args: { + limit: number; + offset: number; + }, + ) => { + return getAllTechniquesGrid( + args.limit, + args.offset, + ); }, /** @@ -32,5 +44,25 @@ export const techniqueResolvers = { args.mitreId, ); }, + /** + * Henter alle subteknikker for en spesifikk teknikk + */ + getTechniqueSubTechniques: async (_parent: unknown, args: { mitreId: string }) => { + return getTechniqueSubTechniques(args.mitreId); + }, + + /** + * Henter all programvare for en spesifikk teknikk + */ + getTechniqueSoftware: async (_parent: unknown, args: { mitreId: string }) => { + return getTechniqueSoftware(args.mitreId); + }, + + /** + * Henter alle trusselaktører for en spesifikk teknikk + */ + getTechniqueThreatActors: async (_parent: unknown, args: { mitreId: string }) => { + return getTechniqueThreatActors(args.mitreId); + }, }, }; \ No newline at end of file diff --git a/backend/src/graphql/typeDefs.ts b/backend/src/graphql/typeDefs.ts index 116fd55..ff2db85 100644 --- a/backend/src/graphql/typeDefs.ts +++ b/backend/src/graphql/typeDefs.ts @@ -61,6 +61,23 @@ export const typeDefs = `#graphql tactics: [String!]! platforms: [String!]! } + + type TechniqueGrid { + mitreId: String! + name: String! + tactic: String! + platform: String! + } + + type TechniqueGridResult { + items: [TechniqueGrid!]! + totalCount: Int! + } + + + type ThreatActorItem { + name: String! + } type Query { ######################################## ThreatActors vvvv @@ -78,11 +95,22 @@ export const typeDefs = `#graphql ######################################## Techinques vvvv # Henter alle Techniques - allTechniques: [Technique!]! + allTechniques( + limit: Int! + offset: Int! + ): TechniqueGridResult! # Henter én Technique basert på MITRE-ID techniqueByMitreId(mitreId: String!): Technique + # Henter alle trusselaktører for en spesifikk teknikk + getTechniqueThreatActors(mitreId: String!): [ThreatActorItem!]! + + # Henter alle programvare for en spesifikk teknikk + getTechniqueSoftware(mitreId: String!): [AssociatedItem!]! + + # Henter alle subteknikker for en spesifikk trusselaktør + getTechniqueSubTechniques(mitreId: String!): [Technique!]! ######################################## Software vvvv diff --git a/backend/src/repositories/techniqueRepository.ts b/backend/src/repositories/techniqueRepository.ts index 035a173..049e8fb 100644 --- a/backend/src/repositories/techniqueRepository.ts +++ b/backend/src/repositories/techniqueRepository.ts @@ -23,16 +23,19 @@ export async function getAllTechniquesGrid( tactic: sql` COALESCE(( - SELECT ${tactic.name} - FROM ${tacticTechnique} - INNER JOIN ${tactic} - ON ${tacticTechnique.tactic_id} = ${tactic.tactic_id} - WHERE ${tacticTechnique.technique_id} = - ${sql.identifier("Technique")}.${sql.identifier("technique_id")} - LIMIT 1 + SELECT ${tactic.name} + FROM ${tactic} + WHERE ${tactic.tactic_id} = ( + SELECT ${tacticTechnique.tactic_id} + FROM ${tacticTechnique} + WHERE ${tacticTechnique.technique_id} = + ${sql.identifier("Technique")}.${sql.identifier("technique_id")} + LIMIT 1 + ) + LIMIT 1 ), 'Unknown') - `, - + `, + platform: sql` COALESCE(( SELECT ${techniquePlatform.platform} @@ -192,7 +195,7 @@ export async function getTechniqueThreatActors( ) { return db .select({ - threatActor: threatActor.name, + name: threatActor.name, }) .from(threatActor) .innerJoin( diff --git a/frontend/src/components/techniques/allTechniques/TechniqueCard/TechniqueCard.tsx b/frontend/src/components/techniques/allTechniques/TechniqueCard/TechniqueCard.tsx index 5b0e1b0..2aa9514 100644 --- a/frontend/src/components/techniques/allTechniques/TechniqueCard/TechniqueCard.tsx +++ b/frontend/src/components/techniques/allTechniques/TechniqueCard/TechniqueCard.tsx @@ -7,21 +7,20 @@ import getTechniqueCardVisual from "./getTechniqueCardVisual"; type TechniqueCardProps = { mitreId: string; name: string; - description: string; - tactics: string[]; - platforms: string[]; + tactic: string; + platform: string; }; function TechniqueCard({ mitreId, name, - description, - tactics, - platforms, + tactic, + platform, }: TechniqueCardProps) { + console.log("TACTIC:", tactic); const { icon: TechniqueIcon, colorClass, - } = getTechniqueCardVisual(mitreId); + } = getTechniqueCardVisual(tactic); return (
@@ -33,21 +32,19 @@ function TechniqueCard({ {mitreId} +

+ {name} +

-
-

{name}

-

{description}

-
-
- {tactics[0] ?? "Unknown"} + {tactic ?? "Unknown"} - {platforms[0] ?? "Unknown"} + {platform ?? "Unknown"}
diff --git a/frontend/src/components/techniques/allTechniques/TechniqueCard/getTechniqueCardVisual.tsx b/frontend/src/components/techniques/allTechniques/TechniqueCard/getTechniqueCardVisual.tsx index b2c1eeb..763b1b3 100644 --- a/frontend/src/components/techniques/allTechniques/TechniqueCard/getTechniqueCardVisual.tsx +++ b/frontend/src/components/techniques/allTechniques/TechniqueCard/getTechniqueCardVisual.tsx @@ -8,6 +8,9 @@ import { Network, Eye, FileText, + KeyRound, + Upload, + AlertTriangle } from "lucide-react"; import type { LucideIcon } from "lucide-react"; @@ -17,57 +20,77 @@ type TechniqueVisual = { colorClass: string; }; -const techniqueVisualMap: Record = { - T1566: { +const tacticVisualMap: Record = { + "Initial Access": { icon: Mail, - colorClass: "technique-phishing", + colorClass: "technique-initial-access", }, - T1059: { + "Execution": { icon: TerminalSquare, colorClass: "technique-execution", }, - T1027: { + "Persistence": { + icon: Layers3, + colorClass: "technique-persistence", + }, + + "Privilege Escalation": { + icon: KeyRound, + colorClass: "technique-privilege-escalation", + }, + + "Defense Evasion": { icon: Shield, colorClass: "technique-defense-evasion", }, - T1003: { + "Credential Access": { icon: Database, colorClass: "technique-credential-access", }, - T1041: { - icon: Radio, - colorClass: "technique-exfiltration", - }, - - T1204: { - icon: Layers3, - colorClass: "technique-user-execution", + "Discovery": { + icon: Eye, + colorClass: "technique-discovery", }, - T1021: { + "Lateral Movement": { icon: Network, colorClass: "technique-lateral-movement", }, - T1078: { - icon: Eye, - colorClass: "technique-valid-accounts", + "Collection": { + icon: FileText, + colorClass: "technique-collection", + }, + + "Command and Control": { + icon: Radio, + colorClass: "technique-command-and-control", + }, + + "Exfiltration": { + icon: Upload, + colorClass: "technique-exfiltration", + }, + + "Impact": { + icon: AlertTriangle, + colorClass: "technique-impact", }, }; -function getTechniqueVisual( - mitreId: string, +function getTechniqueCardVisual( + tactic: string, ): TechniqueVisual { return ( - techniqueVisualMap[mitreId] ?? { + tacticVisualMap[tactic] ?? { icon: FileText, colorClass: "technique-default", } ); } -export default getTechniqueVisual; \ No newline at end of file +export default getTechniqueCardVisual; \ No newline at end of file diff --git a/frontend/src/components/techniques/allTechniques/TechniqueGrid/TechniqueGrid.css b/frontend/src/components/techniques/allTechniques/TechniqueGrid/TechniqueGrid.css index 731164b..58ea026 100644 --- a/frontend/src/components/techniques/allTechniques/TechniqueGrid/TechniqueGrid.css +++ b/frontend/src/components/techniques/allTechniques/TechniqueGrid/TechniqueGrid.css @@ -27,4 +27,54 @@ .technique-grid { grid-template-columns: 1fr; } -} \ No newline at end of file +} + +.technique-pagination { + display: flex; + justify-content: center; + align-items: center; + gap: 24px; + + margin-top: 32px; +} + +.technique-pagination button { + min-width: 120px; + min-height: 48px; + + padding: 14px 20px; + + border: 1px solid var(--border); + border-radius: 8px; + + background: var(--surface); + color: var(--accent); + + font-size: 16px; + font-weight: 600; + + cursor: pointer; + + transition: + border-color 0.2s ease, + background-color 0.2s ease; +} + +.technique-pagination button:hover:not(:disabled) { + border-color: var(--accent); + background: var(--surface-hover); +} + +.technique-pagination button:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.technique-pagination span { + min-width: 48px; + text-align: center; + + color: var(--text); + font-size: 16px; + font-weight: 500; +} diff --git a/frontend/src/components/techniques/allTechniques/TechniqueGrid/TechniqueGrid.tsx b/frontend/src/components/techniques/allTechniques/TechniqueGrid/TechniqueGrid.tsx index 2018624..685dc03 100644 --- a/frontend/src/components/techniques/allTechniques/TechniqueGrid/TechniqueGrid.tsx +++ b/frontend/src/components/techniques/allTechniques/TechniqueGrid/TechniqueGrid.tsx @@ -4,32 +4,77 @@ import "./TechniqueGrid.css"; type Technique = { mitreId: string; name: string; - description: string; - tactics: string[]; - platforms: string[]; + tactic: string; + platform: string; }; type TechniqueGridProps = { techniques: Technique[]; + + page: number; + pageSize: number; + techniquesCount: number; + + onPageChange: ( + page: number, + ) => void; }; function TechniqueGrid({ techniques, + page, + pageSize, + techniquesCount, + onPageChange, }: TechniqueGridProps) { + const totalPossiblePages = + Math.ceil( + techniquesCount / pageSize, + ); return ( + <>
{techniques.map((technique) => ( ))}
- ); + +
+ + + + {techniquesCount === 0 + ? 0 + : (page - 1) * pageSize + 1} + – + {Math.min( + page * pageSize, + techniquesCount, + )} + + + +
+ +); } export default TechniqueGrid; \ No newline at end of file diff --git a/frontend/src/graphql/queries/techniqueQueries.ts b/frontend/src/graphql/queries/techniqueQueries.ts index cd43e9d..90eb0fe 100644 --- a/frontend/src/graphql/queries/techniqueQueries.ts +++ b/frontend/src/graphql/queries/techniqueQueries.ts @@ -1,13 +1,21 @@ import { gql } from "@apollo/client"; export const GET_TECHNIQUES = gql` - query GetTechniques { - allTechniques { - mitreId - name - description - tactics - platforms + query GetTechniques( + $limit: Int! + $offset: Int! + ) { + allTechniques( + limit: $limit + offset: $offset + ) { + totalCount + items { + mitreId + name + tactic + platform + } } } `; \ No newline at end of file diff --git a/frontend/src/pages/Techniques/Techniques.tsx b/frontend/src/pages/Techniques/Techniques.tsx index 73e07e7..e8ed173 100644 --- a/frontend/src/pages/Techniques/Techniques.tsx +++ b/frontend/src/pages/Techniques/Techniques.tsx @@ -2,45 +2,66 @@ import TechniqueGrid from "../../components/techniques/allTechniques/TechniqueGr import TechniqueToolbar from "../../components/techniques/allTechniques/TechniqueToolbar/TechniqueToolbar"; import "./Techniques.css"; +import { useState} from "react"; import { useQuery } from "@apollo/client/react"; import { GET_TECHNIQUES } from "../../graphql/queries/techniqueQueries"; interface Technique { mitreId: string; name: string; - description: string; - tactic: string[]; - platform: string[]; + tactic: string; + platform: string; } interface GetTechniquesData { - allTechniques: Technique[]; + allTechniques: { + items: Technique[]; + totalCount: number; + }; } function Techniques() { + const pageSize = 12; + const [page, setPage] = useState(1); + const { data, loading, error, } = useQuery( GET_TECHNIQUES, + { + variables: { + limit: pageSize, + offset: (page - 1) * pageSize, + }, + }, ); - console.log(data); - console.log(error); - console.log(loading); if (loading) return

Loading...

; if (error) return

Error: {error.message}

; - const techniques = data?.allTechniques || []; + const techniques = + data?.allTechniques.items ?? []; + + const techniquesCount = + data?.allTechniques.totalCount ?? 0; return (
- + +
); } + export default Techniques; \ No newline at end of file From 16ff17d4e2bf47f22bce2bbbdfbf058b9cb47a2d Mon Sep 17 00:00:00 2001 From: alea Date: Fri, 16 Oct 2026 18:04:35 +0200 Subject: [PATCH 5/6] Fikset typeDefs etter merge --- backend/src/graphql/typeDefs.ts | 51 ++++++++------------------------- 1 file changed, 12 insertions(+), 39 deletions(-) diff --git a/backend/src/graphql/typeDefs.ts b/backend/src/graphql/typeDefs.ts index d39cef5..0cc47e5 100644 --- a/backend/src/graphql/typeDefs.ts +++ b/backend/src/graphql/typeDefs.ts @@ -109,45 +109,6 @@ export const typeDefs = `#graphql usedByThreatActors: [RelatedSoftwareEntity!]! } - type TacticGrid { - mitreId: String! - name: String! - description: String! - techniqueCount: Int! - } - - type TacticDetailed { - tacticId: String! - mitreId: String! - name: String! - description: String! - tacticEntryCreated: String! - xMitreDeprecated: Boolean! - } - - type TechniquePage { - items: [Technique!]! - totalCount: Int! - } - - type RelatedSoftwareEntity { - id: String! - mitreId: String! - name: String! - } - - type Software { - id: String! - mitreId: String! - name: String! - description: String! - type: String! - aliases: [String!]! - platforms: [String!]! - associatedTechniques: [RelatedSoftwareEntity!]! - usedByThreatActors: [RelatedSoftwareEntity!]! - } - type Query { allThreatActorsGrid(limit: Int!, offset: Int!): ThreatActorGridInfo! getThreatActorByMitreId(mitreId: String!): ThreatActorDetailed @@ -165,6 +126,18 @@ export const typeDefs = `#graphql # Henter én Technique basert på MITRE-ID techniqueByMitreId(mitreId: String!): Technique + getTechniqueThreatActors( + mitreId: String! + ): [ThreatActorItem!]! + + getTechniqueSoftware( + mitreId: String! + ): [AssociatedItem!]! + + getTechniqueSubTechniques( + mitreId: String! + ): [Technique!]! + software: [Software!]! softwareByMitreId(mitreId: String!): Software From 59ab03c5b6d818c4e852ccd4f22459cca1f67bf7 Mon Sep 17 00:00:00 2001 From: alea Date: Fri, 16 Oct 2026 18:46:57 +0200 Subject: [PATCH 6/6] Endret technique detailed mock data med database data --- .../src/repositories/techniqueRepository.ts | 51 ++++- .../RelatedThreatActors.tsx | 10 +- .../TechniqueOverview/TechniqueOverview.tsx | 16 +- .../src/graphql/queries/techniqueQueries.ts | 52 +++++ .../pages/Techniques/TechniqueDetailed.tsx | 193 +++++++++--------- 5 files changed, 207 insertions(+), 115 deletions(-) diff --git a/backend/src/repositories/techniqueRepository.ts b/backend/src/repositories/techniqueRepository.ts index 049e8fb..a56bcdb 100644 --- a/backend/src/repositories/techniqueRepository.ts +++ b/backend/src/repositories/techniqueRepository.ts @@ -174,6 +174,18 @@ export async function getTechniqueSubTechniques( export async function getTechniqueSoftware( mitreId: string, ) { + const [techniqueResult] = await db + .select({ + techniqueId: technique.technique_id, + }) + .from(technique) + .where(eq(technique.mitre_id, mitreId)) + .limit(1); + + if (!techniqueResult) { + return []; + } + return db .select({ mitreId: software.mitre_id, @@ -187,24 +199,45 @@ export async function getTechniqueSoftware( software.software_id, ), ) - .where(eq(techniqueSoftware.technique_id, mitreId)); + .where( + eq( + techniqueSoftware.technique_id, + techniqueResult.techniqueId, + ), + ); } export async function getTechniqueThreatActors( mitreId: string, ) { + const [techniqueResult] = await db + .select({ + techniqueId: technique.technique_id, + }) + .from(technique) + .where(eq(technique.mitre_id, mitreId)) + .limit(1); + + if (!techniqueResult) { + return []; + } + return db .select({ name: threatActor.name, }) .from(threatActor) - .innerJoin( - threatActorTechnique, - eq( - threatActorTechnique.threat_id, - threatActor.threat_id, - ), + .innerJoin( + threatActorTechnique, + eq( + threatActorTechnique.threat_id, + threatActor.threat_id, + ), ) - - .where(eq(threatActorTechnique.technique_id, mitreId)); + .where( + eq( + threatActorTechnique.technique_id, + techniqueResult.techniqueId, + ), + ); } diff --git a/frontend/src/components/techniques/specificTechnique/RelatedThreatActors/RelatedThreatActors.tsx b/frontend/src/components/techniques/specificTechnique/RelatedThreatActors/RelatedThreatActors.tsx index cea2442..8417d2c 100644 --- a/frontend/src/components/techniques/specificTechnique/RelatedThreatActors/RelatedThreatActors.tsx +++ b/frontend/src/components/techniques/specificTechnique/RelatedThreatActors/RelatedThreatActors.tsx @@ -2,9 +2,7 @@ import "./RelatedThreatActors.css"; import { Users } from "lucide-react"; type ThreatActor = { - mitreId: string; name: string; - alias: string; }; type RelatedThreatActorsProps = { @@ -23,18 +21,12 @@ function RelatedThreatActors({ {actors.map((actor) => (

{actor.name}

- - {actor.mitreId} -
-

- Also known as {actor.alias} -

))} diff --git a/frontend/src/components/techniques/specificTechnique/TechniqueOverview/TechniqueOverview.tsx b/frontend/src/components/techniques/specificTechnique/TechniqueOverview/TechniqueOverview.tsx index 498951d..fc290f2 100644 --- a/frontend/src/components/techniques/specificTechnique/TechniqueOverview/TechniqueOverview.tsx +++ b/frontend/src/components/techniques/specificTechnique/TechniqueOverview/TechniqueOverview.tsx @@ -4,14 +4,16 @@ type TechniqueOverviewProps = { mitreId: string; name: string; tactic: string; - platforms: string[]; + platform: string[]; + subTechniqueCount: number; }; function TechniqueOverview({ mitreId, name, tactic, - platforms, + platform, + subTechniqueCount, }: TechniqueOverviewProps) { return ( ); } diff --git a/frontend/src/graphql/queries/techniqueQueries.ts b/frontend/src/graphql/queries/techniqueQueries.ts index 90eb0fe..8396301 100644 --- a/frontend/src/graphql/queries/techniqueQueries.ts +++ b/frontend/src/graphql/queries/techniqueQueries.ts @@ -18,4 +18,56 @@ export const GET_TECHNIQUES = gql` } } } +`; + +export const GET_TECHNIQUE_BY_MITRE_ID = gql` + query GetTechniqueByMitreId( + $mitreId: String! + ) { + techniqueByMitreId(mitreId: $mitreId) { + mitreId + name + description + platforms + tactics + } + } +`; + +export const GET_TECHNIQUE_SUBTECHNIQUES = gql` + query GetTechniqueSubTechniques( + $mitreId: String! + ) { + getTechniqueSubTechniques( + mitreId: $mitreId + ) { + mitreId + name + } + } +`; + +export const GET_TECHNIQUE_THREAT_ACTORS = gql` + query GetTechniqueThreatActors( + $mitreId: String! + ) { + getTechniqueThreatActors( + mitreId: $mitreId + ) { + name + } + } +`; + +export const GET_TECHNIQUE_SOFTWARE = gql` + query GetTechniqueSoftware( + $mitreId: String! + ) { + getTechniqueSoftware( + mitreId: $mitreId + ) { + mitreId + name + } + } `; \ No newline at end of file diff --git a/frontend/src/pages/Techniques/TechniqueDetailed.tsx b/frontend/src/pages/Techniques/TechniqueDetailed.tsx index 10f030d..2348718 100644 --- a/frontend/src/pages/Techniques/TechniqueDetailed.tsx +++ b/frontend/src/pages/Techniques/TechniqueDetailed.tsx @@ -1,4 +1,3 @@ -import { gql } from "@apollo/client"; import { useQuery } from "@apollo/client/react"; import { useParams } from "react-router-dom"; @@ -11,17 +10,12 @@ import AssociatedSoftware from "../../components/techniques/specificTechnique/As import "./TechniqueDetailed.css"; -export const GET_TECHNIQUE = gql` - query GetTechnique($mitreId: String!) { - techniqueByMitreId(mitreId: $mitreId) { - mitreId - name - description - platforms - tactics - } - } -`; +import { + GET_TECHNIQUE_BY_MITRE_ID, + GET_TECHNIQUE_SUBTECHNIQUES, + GET_TECHNIQUE_SOFTWARE, + GET_TECHNIQUE_THREAT_ACTORS, +} from "../../graphql/queries/techniqueQueries"; interface Technique { mitreId: string; @@ -31,63 +25,34 @@ interface Technique { tactics: string[]; } +interface AssociatedItem { + mitreId: string; + name: string; +} + +interface ThreatActorItem { + name: string; +} + +interface GetTechniqueSubTechniquesData { + getTechniqueSubTechniques: + AssociatedItem[]; +} + +interface GetTechniqueSoftwareData { + getTechniqueSoftware: + AssociatedItem[]; +} + +interface GetTechniqueThreatActorsData { + getTechniqueThreatActors: + ThreatActorItem[]; +} + interface GetTechniqueData { techniqueByMitreId: Technique | null; } -const subTechniques = [ - { - mitreId: "T1566.001", - name: "Spearphishing Attachment", - }, - { - mitreId: "T1566.002", - name: "Spearphishing Link", - }, - { - mitreId: "T1566.003", - name: "Spearphishing via Service", - }, -]; - -const actors = [ - { - mitreId: "G0016", - name: "APT29", - alias: "Cozy Bear", - }, - { - mitreId: "G0032", - name: "Lazarus Group", - alias: "Hidden Cobra", - }, - { - mitreId: "G0034", - name: "Sandworm Team", - alias: "BlackEnergy", - }, - { - mitreId: "G1015", - name: "Scattered Spider", - alias: "UNC3944", - }, -]; - -const software = [ - { - name: "Cobalt Strike", - }, - { - name: "PlugX", - }, - { - name: "PowerShell Empire", - }, - { - name: "WellMess", - }, -]; - function TechniqueDetailed() { const { mitreId } = useParams(); @@ -96,17 +61,39 @@ function TechniqueDetailed() { loading, error, } = useQuery( - GET_TECHNIQUE, + GET_TECHNIQUE_BY_MITRE_ID, { - variables: { - mitreId, - }, - } + variables: { mitreId }, + }, ); - console.log(data); - console.log(error); - console.log(loading); + const { + data: subTechniquesData, + } = useQuery( + GET_TECHNIQUE_SUBTECHNIQUES, + { + variables: { mitreId }, + }, + ); + + const { + data: softwareData, + } = useQuery( + GET_TECHNIQUE_SOFTWARE, + { + variables: { mitreId }, + }, + ); + + const { + data: threatActorData, + } = useQuery( + GET_TECHNIQUE_THREAT_ACTORS, + { + variables: { mitreId }, + }, + ); + console.log(threatActorData); if (loading) { return

Loading...

; @@ -116,15 +103,29 @@ function TechniqueDetailed() { return

Error: {error.message}

; } - const technique = data?.techniqueByMitreId; + const technique = + data?.techniqueByMitreId; if (!technique) { return

Technique not found.

; } + const subTechniques = + subTechniquesData?.getTechniqueSubTechniques ?? + []; + + const software = + softwareData?.getTechniqueSoftware ?? + []; + + const actors = + threatActorData?.getTechniqueThreatActors ?? + []; + return (
+
-
-
- - - -
- -
- -
-
+
+ + + + + +
+ +
+ + + +
+
);