diff --git a/backend/src/graphql/resolvers/techniqueResolver.ts b/backend/src/graphql/resolvers/techniqueResolver.ts index d4d1fe2..41a10a1 100644 --- a/backend/src/graphql/resolvers/techniqueResolver.ts +++ b/backend/src/graphql/resolvers/techniqueResolver.ts @@ -11,8 +11,11 @@ * GraphQL -> Resolver -> Repository -> Drizzle -> MySQL */ import { - getAllTechniques, + 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 getAllTechniques(); + 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 027d9d1..0cc47e5 100644 --- a/backend/src/graphql/typeDefs.ts +++ b/backend/src/graphql/typeDefs.ts @@ -48,6 +48,26 @@ export const typeDefs = `#graphql mitreId: String! name: String! description: String! + + 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 TacticGrid { @@ -95,9 +115,29 @@ export const typeDefs = `#graphql getThreatActorTechniques(mitreId: String!, limit: Int!, offset: Int!): AssociatedItemsPage! getThreatActorSoftwares(mitreId: String!, limit: Int!, offset: Int!): AssociatedItemsPage! - allTechniques: [Technique!]! + ######################################## Techinques vvvv + + # Henter alle Techniques + allTechniques( + limit: Int! + offset: Int! + ): TechniqueGridResult! + + # 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 diff --git a/backend/src/repositories/techniqueRepository.ts b/backend/src/repositories/techniqueRepository.ts index 52d62f6..a56bcdb 100644 --- a/backend/src/repositories/techniqueRepository.ts +++ b/backend/src/repositories/techniqueRepository.ts @@ -1,63 +1,243 @@ -/** - * 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 { eq, count, sql } from "drizzle-orm"; import { db } from "../db/connection.js"; -import { technique } from "../db/schema.js"; +import { + technique, + techniquePlatform, + tacticTechnique, + tactic, + software, + techniqueSoftware, + threatActor, + threatActorTechnique +} from "../db/schema.js"; -/** - * Henter alle Techniques fra databasen. - */ -export async function getAllTechniques() { - return db +export async function getAllTechniquesGrid( + limit: number = 12, + offset: number = 0, +) { + const techniqueSelect = db + .select({ + mitreId: technique.mitre_id, + name: technique.name, + + tactic: sql` + COALESCE(( + 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} + FROM ${techniquePlatform} + WHERE ${techniquePlatform.technique_id} = + ${sql.identifier("Technique")}.${sql.identifier("technique_id")} + LIMIT 1 + ), 'Unknown') + `, + }) + .from(technique) + .orderBy(technique.mitre_id) + .limit(limit) + .offset(offset); + + const [techniques, countResult] = + await Promise.all([ + techniqueSelect, + + db .select({ - mitreId: technique.mitre_id, - name: technique.name, - description: technique.description, - /*tactic: technique.tactic, - platform: technique.platform,*/ + totalCount: count(), }) - .from(technique); + .from(technique), + ]); + + return { + items: techniques, + totalCount: + countResult[0]?.totalCount ?? 0, + }; } + /** * 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); - - return result ?? null; -} \ No newline at end of file + 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, + ), + + tactics: tactics.map( + (tactic) => tactic.name, + ), + }; +} + +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, +) { + 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, + name: software.name, + }) + .from(techniqueSoftware) + .innerJoin( + software, + eq( + techniqueSoftware.software_id, + software.software_id, + ), + ) + .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, + ), + ) + .where( + eq( + threatActorTechnique.technique_id, + techniqueResult.techniqueId, + ), + ); +} diff --git a/frontend/src/components/techniques/allTechniques/TechniqueCard/TechniqueCard.tsx b/frontend/src/components/techniques/allTechniques/TechniqueCard/TechniqueCard.tsx index f501ed2..2aa9514 100644 --- a/frontend/src/components/techniques/allTechniques/TechniqueCard/TechniqueCard.tsx +++ b/frontend/src/components/techniques/allTechniques/TechniqueCard/TechniqueCard.tsx @@ -7,22 +7,20 @@ import getTechniqueCardVisual from "./getTechniqueCardVisual"; type TechniqueCardProps = { mitreId: string; name: string; - description: string; - tactic?: string; - platform?: string; + tactic: string; + platform: string; }; - function TechniqueCard({ mitreId, name, - description, tactic, platform, }: TechniqueCardProps) { + console.log("TACTIC:", tactic); const { icon: TechniqueIcon, colorClass, - } = getTechniqueCardVisual(mitreId); + } = getTechniqueCardVisual(tactic); return (
@@ -34,14 +32,12 @@ function TechniqueCard({ {mitreId} +

+ {name} +

-
-

{name}

-

{description}

-
-
{tactic ?? "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 c578097..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; - tactic?: string; - platform?: 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/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 d47a990..8396301 100644 --- a/frontend/src/graphql/queries/techniqueQueries.ts +++ b/frontend/src/graphql/queries/techniqueQueries.ts @@ -1,11 +1,73 @@ import { gql } from "@apollo/client"; export const GET_TECHNIQUES = gql` - query GetTechniques { - allTechniques { + query GetTechniques( + $limit: Int! + $offset: Int! + ) { + allTechniques( + limit: $limit + offset: $offset + ) { + totalCount + items { + mitreId + name + tactic + platform + } + } + } +`; + +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 ac0e235..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,79 +10,49 @@ 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 - } - } -`; +import { + GET_TECHNIQUE_BY_MITRE_ID, + GET_TECHNIQUE_SUBTECHNIQUES, + GET_TECHNIQUE_SOFTWARE, + GET_TECHNIQUE_THREAT_ACTORS, +} from "../../graphql/queries/techniqueQueries"; interface Technique { mitreId: string; name: string; description: string; + platforms: string[]; + 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(); @@ -92,17 +61,39 @@ function TechniqueDetailed() { loading, error, } = useQuery( - GET_TECHNIQUE, + GET_TECHNIQUE_BY_MITRE_ID, + { + variables: { mitreId }, + }, + ); + + const { + data: subTechniquesData, + } = useQuery( + GET_TECHNIQUE_SUBTECHNIQUES, + { + variables: { mitreId }, + }, + ); + + const { + data: softwareData, + } = useQuery( + GET_TECHNIQUE_SOFTWARE, { - variables: { - mitreId, - }, - } + variables: { mitreId }, + }, ); - console.log(data); - console.log(error); - console.log(loading); + const { + data: threatActorData, + } = useQuery( + GET_TECHNIQUE_THREAT_ACTORS, + { + variables: { mitreId }, + }, + ); + console.log(threatActorData); if (loading) { return

Loading...

; @@ -112,22 +103,39 @@ 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 (
+
@@ -135,37 +143,46 @@ function TechniqueDetailed() {
-
-
- - - -
- -
- -
-
+
+ + + + + +
+ +
+ + + +
+
); diff --git a/frontend/src/pages/Techniques/Techniques.tsx b/frontend/src/pages/Techniques/Techniques.tsx index 5eda3af..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