From 6b8fde45da8daa2a1c1c46c2fba11e92571cece7 Mon Sep 17 00:00:00 2001 From: alea Date: Sat, 10 Oct 2026 22:59:34 +0200 Subject: [PATCH 1/3] Koble techniques frontend til GraphQL backend --- .../src/repositories/techniqueRepository.ts | 2 + docs/referat/meeting-29-09-26.txt | 8 ++ frontend/package.json | 2 + frontend/src/apollo/client.ts | 13 +++ .../TechniqueCard/TechniqueCard.tsx | 10 +- .../TechniqueGrid/TechniqueGrid.tsx | 8 +- .../src/graphql/queries/techniqueQueries.ts | 11 ++ frontend/src/main.tsx | 7 +- frontend/src/pages/Techniques/Techniques.tsx | 64 +++++----- pnpm-lock.yaml | 110 ++++++++++++++++++ 10 files changed, 191 insertions(+), 44 deletions(-) create mode 100644 docs/referat/meeting-29-09-26.txt create mode 100644 frontend/src/apollo/client.ts create mode 100644 frontend/src/graphql/queries/techniqueQueries.ts diff --git a/backend/src/repositories/techniqueRepository.ts b/backend/src/repositories/techniqueRepository.ts index 2cb9733..52d62f6 100644 --- a/backend/src/repositories/techniqueRepository.ts +++ b/backend/src/repositories/techniqueRepository.ts @@ -29,6 +29,8 @@ export async function getAllTechniques() { mitreId: technique.mitre_id, name: technique.name, description: technique.description, + /*tactic: technique.tactic, + platform: technique.platform,*/ }) .from(technique); } diff --git a/docs/referat/meeting-29-09-26.txt b/docs/referat/meeting-29-09-26.txt new file mode 100644 index 0000000..e615a7d --- /dev/null +++ b/docs/referat/meeting-29-09-26.txt @@ -0,0 +1,8 @@ +Oppsummering + +- Vi har alle begynt på hver våre sider (som vi fordelte forrige møte) +- Vi skal begynne å skrive KI kommentarer i PR +- Jobbe med readME til neste møte (fredag) +- Se om vi kan implementere globale: card, grid og toolbar +- Legge layout til «left» istedenfor «center» +- Tenke på ideer for å oppfylle «utvidet bruker-funksjonalitet» \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 39a2208..9994474 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,8 @@ "preview": "vite preview" }, "dependencies": { + "@apollo/client": "^4.3.3", + "graphql": "16.11.0", "lucide-react": "^1.48.0", "react": "^19.2.8", "react-country-flag": "^3.1.0", diff --git a/frontend/src/apollo/client.ts b/frontend/src/apollo/client.ts new file mode 100644 index 0000000..853885a --- /dev/null +++ b/frontend/src/apollo/client.ts @@ -0,0 +1,13 @@ +import { + ApolloClient, + InMemoryCache, + HttpLink, +} from "@apollo/client"; + +export const apolloClient = + new ApolloClient({ + link: new HttpLink({ + uri: "http://localhost:3001", + }), + cache: new InMemoryCache(), + }); \ No newline at end of file diff --git a/frontend/src/components/techniques/allTechniques/TechniqueCard/TechniqueCard.tsx b/frontend/src/components/techniques/allTechniques/TechniqueCard/TechniqueCard.tsx index 3fe287f..f501ed2 100644 --- a/frontend/src/components/techniques/allTechniques/TechniqueCard/TechniqueCard.tsx +++ b/frontend/src/components/techniques/allTechniques/TechniqueCard/TechniqueCard.tsx @@ -8,8 +8,8 @@ type TechniqueCardProps = { mitreId: string; name: string; description: string; - tactic: string; - platform: string; + tactic?: string; + platform?: string; }; function TechniqueCard({ @@ -44,16 +44,16 @@ function TechniqueCard({
- {tactic} + {tactic ?? "Unknown"} - {platform} + {platform ?? "Unknown"}
Open profile diff --git a/frontend/src/components/techniques/allTechniques/TechniqueGrid/TechniqueGrid.tsx b/frontend/src/components/techniques/allTechniques/TechniqueGrid/TechniqueGrid.tsx index f7473a7..c578097 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; + tactic?: string; + platform?: string; }; type TechniqueGridProps = { @@ -24,8 +24,8 @@ function TechniqueGrid({ mitreId={technique.mitreId} name={technique.name} description={technique.description} - tactic={technique.tactic} - platform={technique.platform} + tactic={technique.tactic ?? "Unknown"} + platform={technique.platform ?? "Unknown"} /> ))} diff --git a/frontend/src/graphql/queries/techniqueQueries.ts b/frontend/src/graphql/queries/techniqueQueries.ts new file mode 100644 index 0000000..d47a990 --- /dev/null +++ b/frontend/src/graphql/queries/techniqueQueries.ts @@ -0,0 +1,11 @@ +import { gql } from "@apollo/client"; + +export const GET_TECHNIQUES = gql` + query GetTechniques { + allTechniques { + mitreId + name + description + } + } +`; \ No newline at end of file diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index eff7ccc..63432b1 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,10 +1,15 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; +import { ApolloProvider } from "@apollo/client/react"; + import "./index.css"; import App from "./App.tsx"; +import { apolloClient } from "./apollo/client"; createRoot(document.getElementById("root")!).render( - + + + , ); diff --git a/frontend/src/pages/Techniques/Techniques.tsx b/frontend/src/pages/Techniques/Techniques.tsx index f8499ca..5eda3af 100644 --- a/frontend/src/pages/Techniques/Techniques.tsx +++ b/frontend/src/pages/Techniques/Techniques.tsx @@ -2,42 +2,38 @@ import TechniqueGrid from "../../components/techniques/allTechniques/TechniqueGr import TechniqueToolbar from "../../components/techniques/allTechniques/TechniqueToolbar/TechniqueToolbar"; import "./Techniques.css"; +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; +} + +interface GetTechniquesData { + allTechniques: Technique[]; +} + function Techniques() { - const techniques = [ - { - mitreId: "T1566", - name: "Phishing", - description: - "Send deceptive messages to induce victims to reveal information.", - tactic: "Initial Access", - platform: "Windows", - }, - { - mitreId: "T1059", - name: "Command and Scripting Interpreter", - description: - "Execute commands and scripts through supported interpreters.", - tactic: "Execution", - platform: "Linux", - }, - { - mitreId: "T1003", - name: "OS Credential Dumping", - description: - "Obtain account login and credential material from operating systems.", - tactic: "Credential Access", - platform: "Windows", - }, - { - mitreId: "T1027", - name: "Obfuscated Files or Information", - description: - "Hide artifacts to make detection and analysis more difficult.", - tactic: "Defense Evasion", - platform: "Cross Platform", - }, - ]; + const { + data, + loading, + error, + } = useQuery( + GET_TECHNIQUES, + ); + console.log(data); + console.log(error); + console.log(loading); + + if (loading) return

Loading...

; + if (error) return

Error: {error.message}

; + + const techniques = data?.allTechniques || []; return (
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf688a1..eb20a68 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,6 +52,12 @@ importers: frontend: dependencies: + '@apollo/client': + specifier: ^4.3.3 + version: 4.3.3(graphql@16.11.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(rxjs@7.8.2) + graphql: + specifier: 16.11.0 + version: 16.11.0 lucide-react: specifier: ^1.48.0 version: 1.48.0(react@19.3.0) @@ -124,6 +130,25 @@ packages: peerDependencies: graphql: 14.x || 15.x || 16.x + '@apollo/client@4.3.3': + resolution: {integrity: sha512-wMfOUaFKLgNsu0OMCg50gfhEHTV9/GyO7DxQvut7iSBUoePy0kvll3oWeY8KVPRfnVhYEzcwPJ/Jwz4uSe3fQA==} + peerDependencies: + graphql: ^16.0.0 || ^17.0.0 + graphql-ws: ^5.5.5 || ^6.0.3 + react: ^17.0.0 || ^18.0.0 || >=19.0.0-rc + react-dom: ^17.0.0 || ^18.0.0 || >=19.0.0-rc + rxjs: ^7.3.0 + subscriptions-transport-ws: ^0.9.0 || ^0.11.0 + peerDependenciesMeta: + graphql-ws: + optional: true + react: + optional: true + react-dom: + optional: true + subscriptions-transport-ws: + optional: true + '@apollo/protobufjs@1.2.8': resolution: {integrity: sha512-r7xNeUqZX+eBBEmyvaPw0/cSz6zgf5jdH8mjUz8ynKpNs/GU7vi2T7sNcZINk2ZID7wwjG91FCgdpCrQuJ8rzA==} hasBin: true @@ -1243,6 +1268,22 @@ packages: resolution: {integrity: sha512-Nst5JdK47VIl9UcGwtv2Rcgyn5lWtZ0/mhRQ4G8NN2isxpq2TO30iqHzmwoJycjWuyUfg3GFXqP/gFHXeV57IA==} engines: {node: '>=16.0.0'} + '@wry/caches@1.0.1': + resolution: {integrity: sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA==} + engines: {node: '>=8'} + + '@wry/context@0.7.4': + resolution: {integrity: sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ==} + engines: {node: '>=8'} + + '@wry/equality@0.5.7': + resolution: {integrity: sha512-BRFORjsTuQv5gxcXsuDXx6oGRhuVsEGwZy6LOzRRfgu+eSfxbhUQ9L9YtSEIuIjY/o7g3iWFjrc5eSY1GXP2Dw==} + engines: {node: '>=8'} + + '@wry/trie@0.5.0': + resolution: {integrity: sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA==} + engines: {node: '>=8'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1670,6 +1711,16 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + graphql-tag@2.12.7: + resolution: {integrity: sha512-xnE/NFzy+0eIesvAsREJZ284zTl/wYuBAvpsFSDhRGRdRHdnE90M21Q3xAWyYInb0J756c6x0pIQ62+vtvOs1Q==} + engines: {node: '>=10'} + peerDependencies: + graphql: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + graphql@16.11.0: + resolution: {integrity: sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + graphql@17.0.2: resolution: {integrity: sha512-FRWbddMxfkjiB7z+aQDWIR+E34xo9I8c9mtK2RPv8PmMzKRvrdsreHL/Ui/TmwHJfhHChEtsFPyMHKI+xuarQQ==} engines: {node: ^22.0.0 || ^24.0.0 || ^25.0.0 || >=26.0.0} @@ -1954,6 +2005,9 @@ packages: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} + optimism@0.18.1: + resolution: {integrity: sha512-mLXNwWPa9dgFyDqkNi54sjDyNJ9/fTI6WGBLgnXku1vdKY/jovHfZT5r+aiVeFFLOz+foPNOm5YJ4mqgld2GBQ==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -2062,6 +2116,9 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -2361,6 +2418,21 @@ snapshots: dependencies: graphql: 17.0.2 + '@apollo/client@4.3.3(graphql@16.11.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(rxjs@7.8.2)': + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.11.0) + '@wry/caches': 1.0.1 + '@wry/equality': 0.5.7 + '@wry/trie': 0.5.0 + graphql: 16.11.0 + graphql-tag: 2.12.7(graphql@16.11.0) + optimism: 0.18.1 + rxjs: 7.8.2 + tslib: 2.8.1 + optionalDependencies: + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + '@apollo/protobufjs@1.2.8': dependencies: '@protobufjs/aspromise': 1.1.2 @@ -2864,6 +2936,10 @@ snapshots: graphql: 17.0.2 tslib: 2.8.1 + '@graphql-typed-document-node/core@3.2.0(graphql@16.11.0)': + dependencies: + graphql: 16.11.0 + '@graphql-typed-document-node/core@3.2.0(graphql@17.0.2)': dependencies: graphql: 17.0.2 @@ -3216,6 +3292,22 @@ snapshots: dependencies: tslib: 2.8.1 + '@wry/caches@1.0.1': + dependencies: + tslib: 2.8.1 + + '@wry/context@0.7.4': + dependencies: + tslib: 2.8.1 + + '@wry/equality@0.5.7': + dependencies: + tslib: 2.8.1 + + '@wry/trie@0.5.0': + dependencies: + tslib: 2.8.1 + acorn-jsx@5.3.2(acorn@8.18.0): dependencies: acorn: 8.18.0 @@ -3639,6 +3731,13 @@ snapshots: gopd@1.2.0: {} + graphql-tag@2.12.7(graphql@16.11.0): + dependencies: + graphql: 16.11.0 + tslib: 2.8.1 + + graphql@16.11.0: {} + graphql@17.0.2: {} has-property-descriptors@1.0.2: @@ -3854,6 +3953,13 @@ snapshots: dependencies: ee-first: 1.1.1 + optimism@0.18.1: + dependencies: + '@wry/caches': 1.0.1 + '@wry/context': 0.7.4 + '@wry/trie': 0.5.0 + tslib: 2.8.1 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -3961,6 +4067,10 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.2.10 '@rolldown/binding-win32-x64-msvc': 1.2.10 + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + safe-buffer@5.2.1: {} safer-buffer@2.1.2: {} From f9ea2f58bcf694e5b9682d29a8033b0fc7b4bb10 Mon Sep 17 00:00:00 2001 From: nikitita Date: Mon, 12 Oct 2026 14:40:21 +0200 Subject: [PATCH 2/3] feat: integrate Threat Actor Grid with CyberDB (#54) --- README.md | 39 +++++- StrukturMinneLap.png | Bin 0 -> 63584 bytes .../graphql/resolvers/threatActorResolver.ts | 18 +-- backend/src/graphql/typeDefs.ts | 37 ++++-- .../src/repositories/threatActorRepository.ts | 115 ++++++++-------- frontend/src/api/graphql.ts | 3 + frontend/src/apollo/client.ts | 3 + .../ThreatActorCard/ThreatActorCard.css | 55 ++++++-- .../ThreatActorCard/ThreatActorCard.tsx | 54 +++++--- .../ThreatActorGrid/ThreatActorGrid.tsx | 123 +++++------------- .../src/graphql/queries/threatActorQueries.ts | 20 +++ .../src/pages/ThreatActors/ThreatActors.tsx | 27 +++- frontend/src/types/ThreatActorTypes.ts | 23 ++++ frontend/src/utils/getCountryCode.ts | 27 ++++ 14 files changed, 333 insertions(+), 211 deletions(-) create mode 100644 StrukturMinneLap.png create mode 100644 frontend/src/graphql/queries/threatActorQueries.ts create mode 100644 frontend/src/types/ThreatActorTypes.ts create mode 100644 frontend/src/utils/getCountryCode.ts diff --git a/README.md b/README.md index e371941..f758c7b 100644 --- a/README.md +++ b/README.md @@ -88,14 +88,45 @@ De ulike sidene har litt ulik layout. Dette er fordi vi var litt usikre på nøy ## KI deklarasjon KI har blitt brukt til veiledning i store deler av prosjektet. Ofte har ideer om implementasjon, definisjon av navn og oppførsel samt forslag til forbedringer blitt gjort av oss, mens KI har lettet arbeidslasten med å foreslå kode som samsvarer med det vi la frem. Vi har lagt fokus på å se gjennom koden og nettsiden for å kvalitetsikre, og har manuelt rettet opp der vi ikke er fornøyde. +--- + ## Kjøre prosjektet lokalt -For å kjøre koden, kjør: +## 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 -pnpm install i rotmappen, deretter: +Opprett en ny `Terminal 1`: +```bash +cd backend +pnpm run 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. + + -fordi vi har kun satt opp prosjektet til å kjøre frontend hittil. Deretter: -pnpm run for å kjøre nettsiden. diff --git a/StrukturMinneLap.png b/StrukturMinneLap.png new file mode 100644 index 0000000000000000000000000000000000000000..fe901cdb6e4d42c6dd46f15b382775a4a47a7ad4 GIT binary patch literal 63584 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 Q75vgvKd)AxY7_E*04%?R4FCWD literal 0 HcmV?d00001 diff --git a/backend/src/graphql/resolvers/threatActorResolver.ts b/backend/src/graphql/resolvers/threatActorResolver.ts index ba49d94..662909f 100644 --- a/backend/src/graphql/resolvers/threatActorResolver.ts +++ b/backend/src/graphql/resolvers/threatActorResolver.ts @@ -22,25 +22,15 @@ * GraphQL -> Resolver -> Repository -> Drizzle -> MySQL */ -import {getAllThreatActors,getThreatActorByMitreId} from "../../repositories/threatActorRepository.js"; +import {getAllThreatActorsGrid} from "../../repositories/threatActorRepository.js"; export const threatActorResolvers = { Query: { /** - * Henter alle ThreatActors. + * Henter alle ThreatActors for Grid i front end. */ - 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); + allThreatActorsGrid: async (_parent: unknown, args: {limit: number; offset: number}) => { + return getAllThreatActorsGrid(args.limit, args.offset); }, }, }; \ No newline at end of file diff --git a/backend/src/graphql/typeDefs.ts b/backend/src/graphql/typeDefs.ts index d83fb7d..4d2e40f 100644 --- a/backend/src/graphql/typeDefs.ts +++ b/backend/src/graphql/typeDefs.ts @@ -10,20 +10,18 @@ export const typeDefs = `#graphql # Beskriver hvordan et ThreatActor-objekt ser ut - type ThreatActor { - mitreId: String! + type ThreatActorGrid { name: String! - description: String! + mitreId: String! + alias: String! + status: String! + country: String! + motivation: 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 + type ThreatActorGridInfo { + items: [ThreatActorGrid!]! + totalCount: Int! } type Technique { @@ -33,11 +31,28 @@ export const typeDefs = `#graphql } type Query { + ######################################## ThreatActors vvvv + + # Henter alle threat actors som trengs i grid + allThreatActorsGrid(limit: Int!, offset: Int!): ThreatActorGridInfo! + + + ######################################## 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/threatActorRepository.ts b/backend/src/repositories/threatActorRepository.ts index 5c59727..8e489b9 100644 --- a/backend/src/repositories/threatActorRepository.ts +++ b/backend/src/repositories/threatActorRepository.ts @@ -15,72 +15,71 @@ * !!!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 { asc,count, eq, sql } from "drizzle-orm"; import { db } from "../db/connection.js"; -import { threatActor } from "../db/schema.js"; - +import { threatActor, threatActorAlias } from "../db/schema.js"; /** - * Henter alle ThreatActors fra databasen. + * Henter alle ThreatActors som trengs i frontend gridet. + * + * Bruker en SQL subquery for å hente det alfabetisk først + * aliaset til hver Actor underveis i Drizzle Query. Sikrer at vi + * får minst 1 alias med (LIMIT 1 + COALESCE()) + * + * [COALESCE() erstater en manglende verdi med en forhåndsdefinert verdi + * --> COALESCE(mulig NULL, "Null verdi")] + * + * Fordeller for skalerbarhet: + * - Henter ut alle data med én SQL spørring og unngår N+1 problemmet. + * Dvs slipper å hente ut først alle ThreatActors (1 query) og deretter hente + * ut én alias per ThreatActor (190 queries). Totalt 191 queries = uoptimalt. + * Nå har vi én query som gjør alt. Bruker sql.identifier for å bruke thread_id fra + * ytre querien + * + * For Pagination bruker vi: + * - LIMIT - Hvor mange rader vi maksimal skal hente + * - OFFSET - Hvor mange rader vi hopper over telt fra starten. + * + * Definerer derfor standardverider som gjelder når ingen limit/offset er spesifisert i funksjonskallet */ -export async function getAllThreatActors() { - return db +export async function getAllThreatActorsGrid(limit: number = 9, offset: number = 0) { + const actorsSelect = 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, + + alias: sql` + COALESCE(( + SELECT ${threatActorAlias.alias} + FROM ${threatActorAlias} + WHERE ${threatActorAlias.threat_id} = ${sql.identifier("ThreatActor")}.${sql.identifier("threat_id")} + ORDER BY ${threatActorAlias.alias} ASC + LIMIT 1 + ), 'No Aliases')`, + + status: threatActor.status, + country: sql `COALESCE(${threatActor.country}, 'Unknown')`, + motivation: sql `COALESCE(${threatActor.motivation}, 'Unknown')`, }) .from(threatActor) - .where( - eq(threatActor.mitre_id, mitreId) - ) - .limit(1); + .orderBy(threatActor.mitre_id) + .limit(limit) + .offset(offset); + + + // Hent aktørene og totalt antall parallelt + const [actors, countResult] = await Promise.all([ + actorsSelect, + + //Brukes i pagination + db.select({ totalCount: count() }) + .from(threatActor), + ]); + + return { + items: actors, + totalCount: countResult[0]?.totalCount ?? 0, + }; +} - return result ?? null; -} \ No newline at end of file diff --git a/frontend/src/api/graphql.ts b/frontend/src/api/graphql.ts index c999b76..cec2cb2 100644 --- a/frontend/src/api/graphql.ts +++ b/frontend/src/api/graphql.ts @@ -7,6 +7,9 @@ type GraphQLResponse = { errors?: GraphQLError[]; }; +/** + * Hva gjør dette? Trenger vi det hvis vi har apollo client? + */ const GRAPHQL_URL = import.meta.env.VITE_GRAPHQL_URL ?? "http://localhost:3001/"; export async function graphqlRequest( diff --git a/frontend/src/apollo/client.ts b/frontend/src/apollo/client.ts index 853885a..5f3ee10 100644 --- a/frontend/src/apollo/client.ts +++ b/frontend/src/apollo/client.ts @@ -4,6 +4,9 @@ import { HttpLink, } from "@apollo/client"; +/** + * Hva gjør dette?? + */ export const apolloClient = new ApolloClient({ link: new HttpLink({ diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorCard/ThreatActorCard.css b/frontend/src/components/threatactor/ThreatActorMain/ThreatActorCard/ThreatActorCard.css index 42899e0..2f0296c 100644 --- a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorCard/ThreatActorCard.css +++ b/frontend/src/components/threatactor/ThreatActorMain/ThreatActorCard/ThreatActorCard.css @@ -1,6 +1,7 @@ .threat-actor-card { width: 100%; min-width: 0; + min-height: 200px; display: flex; flex-direction: column; @@ -25,9 +26,6 @@ transform: translateY(-2px); } -.threat-actor-card:hover { - border-color: #0b5558; -} .threat-actor-card-header { display: flex; @@ -93,9 +91,6 @@ border-radius: 6px; - background: rgba(0, 240, 230, 0.1); - color: var(--accent); - font-size: 14px; font-weight: 600; } @@ -106,9 +101,8 @@ border-radius: 50%; - background: #00e6a7; - - box-shadow: 0 0 8px rgba(0, 230, 167, 0.45); + background-color: currentColor; + box-shadow: 0 0 8px currentColor; } .threat-actor-meta { @@ -158,4 +152,47 @@ .open-profile:hover { text-decoration: underline; +} + +/* ACTIVE – grønn */ +.threat-actor-status.active { + background-color: rgba(0, 200, 120, 0.15); + color: #00d89c; +} + +.threat-actor-status.active .status-dot { + background-color: #00d89c; +} + +/* INACTIVE – rød */ +.threat-actor-status.inactive { + background-color: rgba(239, 68, 68, 0.15); + color: #f87171; +} + +.threat-actor-status.inactive .status-dot { + background-color: #f87171; +} + +/* UNKNOWN – grå */ +.threat-actor-status.unknown { + background-color: rgba(148, 163, 184, 0.15); + color: #94a3b8; +} + +.threat-actor-status.unknown .status-dot { + background-color: #94a3b8; +} + +.threat-actor-details { + margin-top: auto; + + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 10px; +} + +.open-profile { + margin-top: 0; } \ No newline at end of file diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorCard/ThreatActorCard.tsx b/frontend/src/components/threatactor/ThreatActorMain/ThreatActorCard/ThreatActorCard.tsx index e48e62f..ef08cdc 100644 --- a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorCard/ThreatActorCard.tsx +++ b/frontend/src/components/threatactor/ThreatActorMain/ThreatActorCard/ThreatActorCard.tsx @@ -1,4 +1,4 @@ -import { Target, Users } from "lucide-react"; +import { Flag, Target, Users } from "lucide-react"; import { Link } from "react-router-dom"; import ReactCountryFlag from "react-country-flag"; import "./ThreatActorCard.css"; @@ -8,7 +8,7 @@ type ThreatActorCardProps = { mitre_id: string; alias: string; country: string; - country_code: string; + country_code?: string; motivation: string; status: string; } @@ -35,28 +35,40 @@ function ThreatActorCard ({ {mitre_id} -
- - {status} -
+
+
+ + {status} +
-
-
- +
+
+ {country_code ? ( + + ) : ( + + )} - {country} -
+ {country} +
-
- - {motivation} +
+ + {motivation} +
diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorGrid/ThreatActorGrid.tsx b/frontend/src/components/threatactor/ThreatActorMain/ThreatActorGrid/ThreatActorGrid.tsx index 8f2b7ec..c30bf87 100644 --- a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorGrid/ThreatActorGrid.tsx +++ b/frontend/src/components/threatactor/ThreatActorMain/ThreatActorGrid/ThreatActorGrid.tsx @@ -1,110 +1,47 @@ import ThreatActorCard from "../ThreatActorCard/ThreatActorCard"; +import type { ThreatActorGridItem } from "../../../../types/ThreatActorTypes"; import "./ThreatActorGrid.css"; +import { getCountryCode } from "../../../../utils/getCountryCode"; -function ThreatActorGrid() { - return ( - <> -
- - - - - - - - +type ThreatActorGridProps = { + actors: ThreatActorGridItem[]; + page: number; + pageSize: number; + actorsCount: number; + onPageChange: (page: number) => void; +}; +function ThreatActorGrid({actors, page, pageSize, actorsCount, onPageChange }: ThreatActorGridProps) { - + //math.ceil(190/9) => math.ceil(21.11) => 22: mulige pages (siste page har bare 1 ThreatActorCard) + const totalPossiblePages = Math.ceil(actorsCount / pageSize); - - - - - + return ( + <> +
+ {actors.map((actor) => ( + + ))}
- - 1–9 + {actorsCount === 0 ? 0 : (page - 1) * pageSize + 1}–{Math.min(page * pageSize, actorsCount)} -
diff --git a/frontend/src/graphql/queries/threatActorQueries.ts b/frontend/src/graphql/queries/threatActorQueries.ts new file mode 100644 index 0000000..50760fa --- /dev/null +++ b/frontend/src/graphql/queries/threatActorQueries.ts @@ -0,0 +1,20 @@ +import { gql, type TypedDocumentNode } from "@apollo/client"; +import type { ThreatActorsGridData, ThreatActorsGridVariables } from "../../types/ThreatActorTypes"; + + +//Query for ThreatActorGrid; viarbler {limit, offset} +//Bruker TypedDocumentNode for at VScode ikke klager. +export const GET_ALL_THREAT_ACTOR_GRID: TypedDocumentNode = gql` + query GetThreatActorsGrid($limit: Int!, $offset: Int!) { + allThreatActorsGrid(limit: $limit, offset: $offset) { + totalCount + items { + name + mitreId + alias + status + country + motivation + } + } + }`; diff --git a/frontend/src/pages/ThreatActors/ThreatActors.tsx b/frontend/src/pages/ThreatActors/ThreatActors.tsx index 1c890ad..5444d22 100644 --- a/frontend/src/pages/ThreatActors/ThreatActors.tsx +++ b/frontend/src/pages/ThreatActors/ThreatActors.tsx @@ -3,15 +3,40 @@ import ThreatActorGrid from "../../components/threatactor/ThreatActorMain/Threat import ThreatActorPageHeader from "../../components/threatactor/ThreatActorMain/ThreatActorPageHeader/ThreatActorPageHeader"; import ThreatActorSearch from "../../components/threatactor/ThreatActorMain/ThreatActorSearch/ThreatActorSearch"; import "./ThreatActors.css" +import { useQuery } from "@apollo/client/react"; +import { GET_ALL_THREAT_ACTOR_GRID } from "../../graphql/queries/threatActorQueries"; +import { useState } from "react"; function ThreatActor(){ + const pageSize = 9; + const [page, setPage] = useState(1); + + const {data, loading, error} = useQuery(GET_ALL_THREAT_ACTOR_GRID, { + variables: { + limit: pageSize, + offset: (page - 1) * pageSize + }, + } + ); + + //Henter data fra querien, [] hvis query feilet + const actors = data?.allThreatActorsGrid.items ?? []; + const actorsCount = data?.allThreatActorsGrid.totalCount ?? 0; + + return (
- +
); } diff --git a/frontend/src/types/ThreatActorTypes.ts b/frontend/src/types/ThreatActorTypes.ts new file mode 100644 index 0000000..e209ce2 --- /dev/null +++ b/frontend/src/types/ThreatActorTypes.ts @@ -0,0 +1,23 @@ +/** Representerer én Threat Actor i gridet. */ +export type ThreatActorGridItem = { + name: string; + mitreId: string; + alias: string; + status: string; + country: string; + motivation: string; +}; + +/** Responsen fra GraphQL-queryen. */ +export type ThreatActorsGridData = { + allThreatActorsGrid: { + totalCount: number; + items: ThreatActorGridItem[]; + }; +}; + +/** Variablene som sendes til GraphQL-queryen. */ +export type ThreatActorsGridVariables = { + limit: number; + offset: number; +}; \ No newline at end of file diff --git a/frontend/src/utils/getCountryCode.ts b/frontend/src/utils/getCountryCode.ts new file mode 100644 index 0000000..8e7ee3c --- /dev/null +++ b/frontend/src/utils/getCountryCode.ts @@ -0,0 +1,27 @@ +/** + * Oversetter landnavn fra databasen til Country_Code. + * Brukes av ReactCountryFlag i ThreatActorCard. + * + * Returnerer undefined dersom landet er ukjent eller ikke støttet, + * slik at frontend ikke viser et feilaktig flagg. + */ +const countryCodes: Record = { + "North Korea": "KP", + "Russia": "RU", + "China": "CN", + "Pakistan": "PK", + "India": "IN", + "Vietnam": "VN", + "Iran": "IR", + "South Korea": "KR", + "Turkey": "TR", + "Lebanon": "LB", + "Belarus": "BY", + "Nigeria": "NG", + "Brazil": "BR", + "Palestinian Territories": "PS", +}; + +export function getCountryCode(country: string): string | undefined { + return countryCodes[country.trim()]; +} \ No newline at end of file From fd01e925d3933d7e6fc50a63b66ef647b74da591 Mon Sep 17 00:00:00 2001 From: nikitita Date: Tue, 13 Oct 2026 10:47:49 +0200 Subject: [PATCH 3/3] feat: integrate Threat Actor details and pagination (#55) --- .../graphql/resolvers/threatActorResolver.ts | 18 +- backend/src/graphql/typeDefs.ts | 35 +++ .../src/repositories/threatActorRepository.ts | 106 ++++++- .../ThreatActorAssociatedList.css | 277 ++++++++++++++++++ .../ThreatActorAssociatedList.tsx | 76 +++++ .../ThreatActorDetailedAssociatedSoftware.css | 184 ------------ .../ThreatActorDetailedAssociatedSoftware.tsx | 49 ---- ...hreatActorDetailedAssociatedTechniques.css | 186 ------------ ...hreatActorDetailedAssociatedTechniques.tsx | 52 ---- .../ThreatActorDetailedHeader.tsx | 9 + .../ThreatActorDetailedInfoCards.css | 50 +++- .../ThreatActorDetailedInfoCards.tsx | 32 +- .../ThreatActorOverview.css | 51 +++- .../ThreatActorOverview.tsx | 31 +- .../src/graphql/queries/threatActorQueries.ts | 64 +++- .../ThreatActors/ThreatActorDetailed.css | 25 +- .../ThreatActors/ThreatActorDetailed.tsx | 167 ++++++----- frontend/src/types/ThreatActorTypes.ts | 81 ++++- 18 files changed, 895 insertions(+), 598 deletions(-) create mode 100644 frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorAssociated/ThreatActorAssociatedList.css create mode 100644 frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorAssociated/ThreatActorAssociatedList.tsx delete mode 100644 frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedAssociatedSoftware/ThreatActorDetailedAssociatedSoftware.css delete mode 100644 frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedAssociatedSoftware/ThreatActorDetailedAssociatedSoftware.tsx delete mode 100644 frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedAssociatedTechniques/ThreatActorDetailedAssociatedTechniques.css delete mode 100644 frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedAssociatedTechniques/ThreatActorDetailedAssociatedTechniques.tsx diff --git a/backend/src/graphql/resolvers/threatActorResolver.ts b/backend/src/graphql/resolvers/threatActorResolver.ts index 662909f..88221fa 100644 --- a/backend/src/graphql/resolvers/threatActorResolver.ts +++ b/backend/src/graphql/resolvers/threatActorResolver.ts @@ -22,7 +22,7 @@ * GraphQL -> Resolver -> Repository -> Drizzle -> MySQL */ -import {getAllThreatActorsGrid} from "../../repositories/threatActorRepository.js"; +import {getAllThreatActorsGrid, getThreatActorByMitreId, getThreatActorSoftwares, getThreatActorTechniques} from "../../repositories/threatActorRepository.js"; export const threatActorResolvers = { Query: { @@ -32,5 +32,21 @@ export const threatActorResolvers = { allThreatActorsGrid: async (_parent: unknown, args: {limit: number; offset: number}) => { return getAllThreatActorsGrid(args.limit, args.offset); }, + + /** + * Henter én Threat Actor med tilhørende detaljer, + * aliaser, sektorer, teknikker og programvare. + */ + getThreatActorByMitreId: async (_parent: unknown, args: {mitreId: string}) => { + return getThreatActorByMitreId(args.mitreId); + }, + + getThreatActorTechniques: async (_parent: unknown, args: {mitreId: string; limit: number; offset: number}) => { + return getThreatActorTechniques(args.mitreId, args.limit, args.offset); + }, + + getThreatActorSoftwares: async (_parent: unknown, args: {mitreId: string; limit: number; offset: number}) => { + return getThreatActorSoftwares(args.mitreId, args.limit, args.offset); + }, }, }; \ No newline at end of file diff --git a/backend/src/graphql/typeDefs.ts b/backend/src/graphql/typeDefs.ts index 4d2e40f..15416a8 100644 --- a/backend/src/graphql/typeDefs.ts +++ b/backend/src/graphql/typeDefs.ts @@ -23,6 +23,35 @@ export const typeDefs = `#graphql items: [ThreatActorGrid!]! totalCount: Int! } + + type AssociatedItem { + mitreId: String! + name: String! + } + + type AssociatedItemsPage { + items: [AssociatedItem!]! + totalCount: Int! + } + + type ThreatActorDetailed { + threatId: 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! + + aliases: [String!]! + sectors: [String!]! + } type Technique { mitreId: String! @@ -36,6 +65,12 @@ export const typeDefs = `#graphql # 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 diff --git a/backend/src/repositories/threatActorRepository.ts b/backend/src/repositories/threatActorRepository.ts index 8e489b9..d424ad5 100644 --- a/backend/src/repositories/threatActorRepository.ts +++ b/backend/src/repositories/threatActorRepository.ts @@ -15,10 +15,10 @@ * !!!ALTSÅ HER SKRIVER VI KODEN SOM FAKTISK HENTER UT DATA FRA DB VIA DRIZZLE!!!! */ -import { asc,count, eq, sql } from "drizzle-orm"; +import { count, eq, sql } from "drizzle-orm"; import { db } from "../db/connection.js"; -import { threatActor, threatActorAlias } from "../db/schema.js"; +import { software, technique, threatActor, threatActorAlias, threatActorSector, threatActorSoftware, threatActorTechnique } from "../db/schema.js"; /** * Henter alle ThreatActors som trengs i frontend gridet. @@ -83,3 +83,105 @@ export async function getAllThreatActorsGrid(limit: number = 9, offset: number = }; } +export async function getThreatActorByMitreId(mitre_id : string){ + const [actor] = await db + .select({ + threatId: threatActor.threat_id, + mitreId: threatActor.mitre_id, + name: threatActor.name, + description: threatActor.description, + threatEntryCreated: sql`DATE_FORMAT(${threatActor.threat_entry_created}, '%Y-%m-%d')`, + country: sql `COALESCE(${threatActor.country}, 'Unknown')`, + motivation: sql `COALESCE(${threatActor.motivation}, 'Unknown')`, + region: sql `COALESCE(${threatActor.region}, 'Unknown')`, + firstSeen: sql `COALESCE(DATE_FORMAT(${threatActor.first_seen}, '%Y-%m-%d'), 'Unknown')`, + lastSeen: sql `COALESCE(DATE_FORMAT(${threatActor.last_seen}, '%Y-%m-%d'), 'Unknown')`, + status: threatActor.status, + dangerLevel: threatActor.danger_level, + xMitreDeprecated: threatActor.x_mitre_deprecated + }) + .from(threatActor) + .where(eq(threatActor.mitre_id, mitre_id)) + .limit(1) + + //validering av mitre_id + if (!actor){ + return null; + } + + const [aliases, sectors] = await Promise.all([ + + //Aliases + db.select({alias: threatActorAlias.alias}).from(threatActorAlias).where(eq(threatActorAlias.threat_id, actor.threatId)), + + //Sectors + db.select({sector: threatActorSector.sector}).from(threatActorSector).where(eq(threatActorSector.threat_id, actor.threatId)), + + ]); + + return { + ...actor, + aliases: aliases.map((row) => row.alias), + sectors: sectors.map((row) => row.sector), + }; +} + +export async function getThreatActorTechniques(mitreId: string, limit: number, offset: number) { + const [techniques, countResult] = await Promise.all([ + + db.select({ + mitreId: technique.mitre_id, + name: technique.name + }) + .from(threatActorTechnique) + .innerJoin(technique, eq(threatActorTechnique.technique_id, technique.technique_id)) + .innerJoin(threatActor, eq(threatActorTechnique.threat_id, threatActor.threat_id)) + .where(eq(threatActor.mitre_id, mitreId)) + .orderBy(technique.mitre_id) + .limit(limit) + .offset(offset), + + //Brukes i pagination + db.select({ totalCount: count() }) + .from(threatActorTechnique) + .innerJoin(technique, eq(threatActorTechnique.technique_id, technique.technique_id)) + .innerJoin(threatActor, eq(threatActorTechnique.threat_id, threatActor.threat_id)) + .where(eq(threatActor.mitre_id, mitreId)) + + ]); + + return { + items: techniques, + totalCount: countResult[0]?.totalCount ?? 0, + }; +} + + +export async function getThreatActorSoftwares(mitreId: string, limit: number, offset: number) { + const [softwares, countResult] = await Promise.all([ + + db.select({ + mitreId: software.mitre_id, + name: software.name + }) + .from(threatActorSoftware) + .innerJoin(software, eq(threatActorSoftware.software_id, software.software_id)) + .innerJoin(threatActor, eq(threatActorSoftware.threat_id, threatActor.threat_id)) + .where(eq(threatActor.mitre_id, mitreId)) + .orderBy(software.mitre_id) + .limit(limit) + .offset(offset), + + //Brukes i pagination + db.select({ totalCount: count() }) + .from(threatActorSoftware) + .innerJoin(software, eq(threatActorSoftware.software_id, software.software_id)) + .innerJoin(threatActor, eq(threatActorSoftware.threat_id, threatActor.threat_id)) + .where(eq(threatActor.mitre_id, mitreId)) + ]); + + return { + items: softwares, + totalCount: countResult[0]?.totalCount ?? 0, + }; +} \ No newline at end of file diff --git a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorAssociated/ThreatActorAssociatedList.css b/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorAssociated/ThreatActorAssociatedList.css new file mode 100644 index 0000000..13a42e7 --- /dev/null +++ b/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorAssociated/ThreatActorAssociatedList.css @@ -0,0 +1,277 @@ + +/* ===== Associated panel ===== */ + +.associated-panel { + min-width: 0; + height: 390px; + + display: flex; + flex-direction: column; + + padding: 16px; + + background: var(--surface); + + border: 1px solid var(--border); + border-radius: 8px; +} + + +/* ===== Panel header ===== */ + +.associated-panel-header { + display: flex; + align-items: center; + justify-content: space-between; + + gap: 16px; + margin-bottom: 12px; +} + +.associated-panel-title { + display: flex; + align-items: center; + gap: 10px; + + min-width: 0; +} + +.associated-panel-title svg { + width: 24px; + height: 24px; + + flex-shrink: 0; + + color: var(--text-heading); +} + +.associated-panel-title h2 { + margin: 0; + + font-size: 20px; + font-weight: 700; +} + + +/* ===== Associated list ===== */ + +.associated-list { + display: flex; + flex-direction: column; + gap: 6px; +} + + +/* ===== Associated row ===== */ + +.associated-row { + min-width: 0; + min-height: 42px; + + display: flex; + align-items: center; + gap: 12px; + + padding: 7px 10px; + + background: var(--bg); + + border: 1px solid var(--border); + border-radius: 7px; +} + + +/* ===== Shared icon ===== */ + +.associated-row-icon { + width: 30px; + height: 30px; + + display: flex; + align-items: center; + justify-content: center; + + flex-shrink: 0; + + border-radius: 6px; +} + +.associated-row-icon svg { + width: 18px; + height: 18px; +} + + +/* ===== MITRE ID ===== */ + +.associated-item-id { + min-width: 72px; + + padding: 4px 8px; + + border-radius: 6px; + + font-size: 14px; + font-weight: 700; + text-align: center; + white-space: nowrap; +} + + +/* ===== Item name ===== */ + +.associated-item-name { + min-width: 0; + flex: 1; + + color: var(--text); + + font-size: 14px; + font-weight: 500; + + overflow-wrap: anywhere; +} + + +/* ===== Row arrow ===== */ + +.associated-row-arrow { + width: 18px; + height: 18px; + + flex-shrink: 0; + margin-left: auto; + + color: var(--text); +} + + +/* ===== Techniques palette ===== */ + +.associated-row-technique .associated-row-icon { + background: var(--accent-soft); + color: var(--accent); +} + +.associated-row-technique .associated-item-id { + background: var(--accent-soft); + color: var(--accent); +} + + +/* ===== Software palette ===== */ + +.associated-row-software .associated-row-icon { + background: rgba(255, 80, 80, 0.14); + color: #ff6b6b; +} + +.associated-row-software .associated-item-id { + background: rgba(255, 80, 80, 0.14); + color: #ff6b6b; +} + + +/* ===== Pagination ===== */ + +.associated-pagination { + display: flex; + align-items: center; + justify-content: center; + gap: 16px; + + margin-top: auto; + padding-top: 16px; +} + +.associated-pagination span { + color: var(--text); + font-size: 13px; + white-space: nowrap; +} + +.associated-pagination button { + width: 34px; + height: 34px; + + display: flex; + align-items: center; + justify-content: center; + + padding: 0; + + background: var(--bg); + color: var(--accent); + + border: 1px solid var(--border); + border-radius: 6px; + + cursor: pointer; +} + +.associated-pagination button:hover:not(:disabled) { + background: var(--accent-soft); + border-color: var(--accent); +} + +.associated-pagination button:disabled { + opacity: 0.35; + cursor: not-allowed; +} + +.associated-pagination button:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + + +/* ===== Mobile ===== */ + +@media (max-width: 520px) { + .associated-panel { + padding: 14px; + } + + .associated-panel-title h2 { + font-size: 18px; + } + + .associated-panel-title svg { + width: 22px; + height: 22px; + } + + .associated-row { + gap: 8px; + padding: 7px 8px; + } + + .associated-row-icon { + width: 28px; + height: 28px; + } + + .associated-row-icon svg { + width: 16px; + height: 16px; + } + + .associated-item-id { + min-width: 64px; + padding: 4px 6px; + font-size: 12px; + } + + .associated-item-name { + font-size: 13px; + } + + .associated-row-arrow { + width: 16px; + height: 16px; + } + + .associated-pagination { + gap: 12px; + } +} diff --git a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorAssociated/ThreatActorAssociatedList.tsx b/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorAssociated/ThreatActorAssociatedList.tsx new file mode 100644 index 0000000..177f0e6 --- /dev/null +++ b/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorAssociated/ThreatActorAssociatedList.tsx @@ -0,0 +1,76 @@ +import { + ChevronLeft, + ChevronRight, + FileText, + Settings, +} from "lucide-react"; +import type { AssociatedItem } from "../../../../types/ThreatActorTypes"; +import "./ThreatActorAssociatedList.css"; + +type ThreatActorAssociatedListProps = { + type: "techniques" | "software"; + items: AssociatedItem[]; + totalCount: number; + page: number; + pageSize: number; + onPageChange: (page: number) => void; +}; + +function ThreatActorAssociatedList({type,items,totalCount,page,pageSize,onPageChange}: ThreatActorAssociatedListProps) { + const isTechnique = type === "techniques"; + const totalPages = Math.ceil(totalCount / pageSize); + + return ( +
+
+
+ {isTechnique ? : } +

+ {isTechnique ? "Associated Techniques" : "Associated Software"} +

+
+
+ +
+ {items.map((item) => ( +
+ + {isTechnique ? : } + + + {item.mitreId} + + {item.name} + + +
+ ))} +
+ + {totalPages > 1 && ( +
+ + + + Page {page + 1} of {totalPages} + + + +
+ )} +
+ ); +} + +export default ThreatActorAssociatedList; \ No newline at end of file diff --git a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedAssociatedSoftware/ThreatActorDetailedAssociatedSoftware.css b/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedAssociatedSoftware/ThreatActorDetailedAssociatedSoftware.css deleted file mode 100644 index 9eccf55..0000000 --- a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedAssociatedSoftware/ThreatActorDetailedAssociatedSoftware.css +++ /dev/null @@ -1,184 +0,0 @@ -/* ===== Software panel ===== */ - -.associated-software { - min-width: 0; - height: 100%; - - display: flex; - flex-direction: column; - - padding: 16px; - - background: var(--surface); - - border: 1px solid var(--border); - border-radius: 8px; -} - - -/* ===== Panel header ===== */ - -.associated-panel-header { - display: flex; - align-items: center; - justify-content: space-between; - - gap: 16px; - - margin-bottom: 12px; -} - -.associated-panel-title { - display: flex; - align-items: center; - gap: 10px; - - min-width: 0; -} - -.associated-panel-title svg { - width: 24px; - height: 24px; - - flex-shrink: 0; - - color: var(--text-heading); -} - -.associated-panel-title h2 { - font-size: 20px; - font-weight: 700; -} - - -/* ===== View all button ===== */ - -.associated-view-all { - display: inline-flex; - align-items: center; - gap: 6px; - - flex-shrink: 0; - - padding: 0; - - border: none; - background: transparent; - - color: var(--accent); - - font-size: 14px; - font-weight: 600; - - cursor: pointer; -} - -.associated-view-all svg { - width: 18px; - height: 18px; -} - -.associated-view-all:hover { - text-decoration: underline; -} - - -/* ===== Software list ===== */ - -.associated-list { - display: flex; - flex-direction: column; - gap: 6px; -} - - -/* ===== Software row ===== */ - -.associated-software-row { - min-width: 0; - min-height: 42px; - - display: flex; - align-items: center; - gap: 12px; - - padding: 7px 10px; - - background: var(--bg); - - border: 1px solid var(--border); - border-radius: 7px; -} - - -/* ===== Software icon ===== */ - -.software-icon { - width: 30px; - height: 30px; - - flex-shrink: 0; - - padding: 6px; - - border-radius: 6px; - - background: rgba(255, 80, 80, 0.14); - color: #ff6b6b; -} - - -/* ===== Software name ===== */ - -.software-name { - min-width: 0; - - color: var(--text); - - font-size: 14px; - font-weight: 500; -} - - -/* ===== Row arrow ===== */ - -.associated-row-arrow { - width: 18px; - height: 18px; - - flex-shrink: 0; - margin-left: auto; - - color: var(--text); -} - - -/* ===== Mobile ===== */ - -@media (max-width: 520px) { - .associated-software { - padding: 14px; - } - - .associated-panel-title h2 { - font-size: 18px; - } - - .associated-panel-title svg { - width: 22px; - height: 22px; - } - - .associated-view-all { - font-size: 13px; - } - - .software-icon { - width: 28px; - height: 28px; - } - - .software-name { - font-size: 13px; - } -} \ No newline at end of file diff --git a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedAssociatedSoftware/ThreatActorDetailedAssociatedSoftware.tsx b/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedAssociatedSoftware/ThreatActorDetailedAssociatedSoftware.tsx deleted file mode 100644 index f7c410e..0000000 --- a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedAssociatedSoftware/ThreatActorDetailedAssociatedSoftware.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { ArrowRight, ChevronRight, Settings } from "lucide-react"; -import "./ThreatActorDetailedAssociatedSoftware.css"; - -type AssociatedSoftware = { - name: string; -}; - -type ThreatActorDetailedAssociatedSoftwareProps = { - software: AssociatedSoftware[]; -}; - -function ThreatActorDetailedAssociatedSoftware({ - software, -}: ThreatActorDetailedAssociatedSoftwareProps) { - return ( -
-
-
- -

Associated Software

-
- - -
- -
- {software.map((item) => ( -
- - - - {item.name} - - - -
- ))} -
-
- ); -} - -export default ThreatActorDetailedAssociatedSoftware; \ No newline at end of file diff --git a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedAssociatedTechniques/ThreatActorDetailedAssociatedTechniques.css b/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedAssociatedTechniques/ThreatActorDetailedAssociatedTechniques.css deleted file mode 100644 index ab859be..0000000 --- a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedAssociatedTechniques/ThreatActorDetailedAssociatedTechniques.css +++ /dev/null @@ -1,186 +0,0 @@ -/* ===== Techniques panel ===== */ - -.associated-techniques { - min-width: 0; - height: 100%; - - display: flex; - flex-direction: column; - - padding: 16px; - - background: var(--surface); - - border: 1px solid var(--border); - border-radius: 8px; -} - - -/* ===== Panel header ===== */ - -.associated-panel-header { - display: flex; - align-items: center; - justify-content: space-between; - - gap: 16px; - - margin-bottom: 12px; -} - -.associated-panel-title { - display: flex; - align-items: center; - gap: 10px; - - min-width: 0; -} - -.associated-panel-title svg { - width: 24px; - height: 24px; - - flex-shrink: 0; - - color: var(--text-heading); -} - -.associated-panel-title h2 { - font-size: 20px; - font-weight: 700; -} - - -/* ===== View all button ===== */ - -.associated-view-all { - display: inline-flex; - align-items: center; - gap: 6px; - - flex-shrink: 0; - - padding: 0; - - border: none; - background: transparent; - - color: var(--accent); - - font-size: 14px; - font-weight: 600; - - cursor: pointer; -} - -.associated-view-all svg { - width: 18px; - height: 18px; -} - -.associated-view-all:hover { - text-decoration: underline; -} - - -/* ===== Techniques list ===== */ - -.associated-list { - display: flex; - flex-direction: column; - gap: 6px; -} - - -/* ===== Technique row ===== */ - -.associated-technique-row { - min-width: 0; - min-height: 42px; - - display: flex; - align-items: center; - gap: 12px; - - padding: 7px 10px; - - background: var(--bg); - - border: 1px solid var(--border); - border-radius: 7px; -} - - -/* ===== Technique ID ===== */ - -.technique-id { - min-width: 72px; - - padding: 4px 8px; - - border-radius: 6px; - - background: var(--accent-soft); - color: var(--accent); - - font-size: 14px; - font-weight: 700; - - text-align: center; -} - - -/* ===== Technique name ===== */ - -.technique-name { - min-width: 0; - - color: var(--text); - - font-size: 14px; - font-weight: 500; -} - - -/* ===== Row arrow ===== */ - -.associated-row-arrow { - width: 18px; - height: 18px; - - flex-shrink: 0; - margin-left: auto; - - color: var(--text); -} - - -/* ===== Mobile ===== */ - -@media (max-width: 520px) { - .associated-techniques { - padding: 14px; - } - - .associated-panel-title h2 { - font-size: 18px; - } - - .associated-panel-title svg { - width: 22px; - height: 22px; - } - - .associated-view-all { - font-size: 13px; - } - - .technique-id { - min-width: 64px; - font-size: 13px; - } - - .technique-name { - font-size: 13px; - } -} \ No newline at end of file diff --git a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedAssociatedTechniques/ThreatActorDetailedAssociatedTechniques.tsx b/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedAssociatedTechniques/ThreatActorDetailedAssociatedTechniques.tsx deleted file mode 100644 index ba1b117..0000000 --- a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedAssociatedTechniques/ThreatActorDetailedAssociatedTechniques.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { ArrowRight, ChevronRight, FileText } from "lucide-react"; -import "./ThreatActorDetailedAssociatedTechniques.css"; - -type AssociatedTechnique = { - mitreId: string; - name: string; -}; - -type ThreatActorDetailedAssociatedTechniquesProps = { - techniques: AssociatedTechnique[]; -}; - -function ThreatActorDetailedAssociatedTechniques({ - techniques, -}: ThreatActorDetailedAssociatedTechniquesProps) { - return ( -
-
-
- -

Associated Techniques

-
- - -
- -
- {techniques.map((technique) => ( -
- - {technique.mitreId} - - - - {technique.name} - - - -
- ))} -
-
- ); -} - -export default ThreatActorDetailedAssociatedTechniques; \ No newline at end of file diff --git a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader.tsx b/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader.tsx index 8af335a..63c36a7 100644 --- a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader.tsx +++ b/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader.tsx @@ -6,12 +6,14 @@ type ThreatActorDetailsHeaderProps = { name: string; mitreId: string; aliases: string[]; + targetSectors: string[]; }; function ThreatActorDetailedHeader({ name, mitreId, aliases, + targetSectors }: ThreatActorDetailsHeaderProps){ return (
@@ -40,6 +42,13 @@ function ThreatActorDetailedHeader({ ))}
+
+ Target Sectors: + + {targetSectors.map((sector) => ( + {sector} + ))} +
); diff --git a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.css b/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.css index 630e7df..b9f1569 100644 --- a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.css +++ b/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.css @@ -2,7 +2,7 @@ .detailed-info-cards { display: grid; - grid-template-columns: repeat(5, minmax(150px, 1fr)); + grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; } @@ -21,29 +21,63 @@ } + /* ===== Status ===== */ +.status-info-card.status-active { + --status-color: #00d89c; +} + +.status-info-card.status-inactive { + --status-color: #ff6b6b; +} + +.status-info-card.status-unknown { + --status-color: #9ca3af; +} + .status-info-card .detailed-info-card-icon, .status-info-card .detailed-info-card-value { - color: var(--accent); + color: var(--status-color); } -/* ===== Danger level ===== */ +/* ===== Danger Level ===== */ + +/* Farger basert på risikonivå */ +.danger-info-card.danger-low { + --danger-color: #22c55e; + --danger-background: rgba(34, 197, 94, 0.18); +} +.danger-info-card.danger-moderate { + --danger-color: #eab308; + --danger-background: rgba(234, 179, 8, 0.18); +} + +.danger-info-card.danger-high { + --danger-color: #f97316; + --danger-background: rgba(249, 115, 22, 0.18); +} + +.danger-info-card.danger-critical { + --danger-color: #ef4444; + --danger-background: rgba(239, 68, 68, 0.18); +} + +/* Ikonet */ .danger-info-card .detailed-info-card-icon { - color: #ff6b6b; + color: var(--danger-color); } +/* Badge med dynamisk tekst- og bakgrunnsfarge */ .danger-info-card .detailed-info-card-value { width: fit-content; - padding: 3px 10px; - border-radius: 999px; - background: rgba(255, 80, 80, 0.18); - color: #ff6b6b; + color: var(--danger-color); + background: var(--danger-background); font-weight: 700; } diff --git a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.tsx b/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.tsx index ccee6c7..814f832 100644 --- a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.tsx +++ b/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.tsx @@ -1,4 +1,4 @@ -import {AlertTriangle,Circle,Flag,MapPin,Target} from "lucide-react"; +import {AlertTriangle,Circle,Flag,MapPin} from "lucide-react"; import ReactCountryFlag from "react-country-flag"; import ThreatActorDetailedInfoCard from "./ThreatActorDetailedInfoCard"; import "./ThreatActorDetailedInfoCards.css"; @@ -6,9 +6,8 @@ import "./ThreatActorDetailedInfoCards.css"; type ThreatActorDetailedInfoCardsProps = { country: string; - countryCode: string; + countryCode?: string; motivation: string; - targetSectors: string[]; status: string; dangerLevel: string; }; @@ -24,7 +23,7 @@ type ThreatActorDetailedInfoCardsProps = { *└── data: danger level * */ -function ThreatActorDetailedInfoCards({country,countryCode,motivation,targetSectors,status,dangerLevel}: ThreatActorDetailedInfoCardsProps) { +function ThreatActorDetailedInfoCards({country,countryCode,motivation,status,dangerLevel}: ThreatActorDetailedInfoCardsProps) { return (
- + {countryCode ? ( + + ) : ( + + )} {country} } @@ -48,25 +51,18 @@ function ThreatActorDetailedInfoCards({country,countryCode,motivation,targetSect value={motivation} /> - } - title="Target Sectors" - value={targetSectors.join(", ")} - variant="target-sectors-card" - /> - } title="Status" value={status} - variant="status-info-card" + variant={`status-info-card status-${status.toLowerCase()}`} /> } title="Danger Level" value={dangerLevel} - variant="danger-info-card" + variant={`danger-info-card danger-${dangerLevel.toLowerCase()}`} />
); diff --git a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedOverview/ThreatActorOverview.css b/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedOverview/ThreatActorOverview.css index f53841b..a87453e 100644 --- a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedOverview/ThreatActorOverview.css +++ b/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedOverview/ThreatActorOverview.css @@ -62,27 +62,62 @@ } + /* ===== Status ===== */ .overview-status { - color: var(--accent) !important; font-weight: 600; } +.overview-status.status-active { + color: #00d89c; +} -/* ===== Danger level ===== */ +.overview-status.status-inactive { + color: #ff6b6b; +} -.overview-danger { - width: fit-content; +.overview-status.status-unknown { + color: #9ca3af; +} - padding: 4px 10px; - border-radius: 999px; - background: rgba(255, 80, 80, 0.16); - color: #ff6b6b !important; +/* ===== Danger Level ===== */ + +.overview-row dd.overview-danger { + width: fit-content; + padding: 3px 10px; + border-radius: 999px; font-weight: 700; + + color: var(--danger-color); + background: var(--danger-background); +} + +/* LOW – Green */ +.overview-danger.danger-low { + --danger-color: #22c55e; + --danger-background: rgba(34, 197, 94, 0.18); +} + +/* MODERATE – Yellow */ +.overview-danger.danger-moderate { + --danger-color: #eab308; + --danger-background: rgba(234, 179, 8, 0.18); +} + +/* HIGH – Orange */ +.overview-danger.danger-high { + --danger-color: #f97316; + --danger-background: rgba(249, 115, 22, 0.18); +} + +/* CRITICAL – Red */ +.overview-danger.danger-critical { + --danger-color: #ef4444; + --danger-background: rgba(239, 68, 68, 0.18); } diff --git a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedOverview/ThreatActorOverview.tsx b/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedOverview/ThreatActorOverview.tsx index 5a5f520..d94151d 100644 --- a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedOverview/ThreatActorOverview.tsx +++ b/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedOverview/ThreatActorOverview.tsx @@ -1,17 +1,18 @@ -import { ExternalLink } from "lucide-react"; +import { ExternalLink, Flag } from "lucide-react"; import ReactCountryFlag from "react-country-flag"; import "./ThreatActorOverview.css"; type ThreatActorOverviewProps = { mitreId: string; country: string; - countryCode: string; + countryCode?: string; region: string; motivation: string; status: string; dangerLevel: string; firstSeen: string; lastSeen: string; + xMitreDeprecated: boolean; }; function ThreatActorOverview({ @@ -24,6 +25,7 @@ function ThreatActorOverview({ dangerLevel, firstSeen, lastSeen, + xMitreDeprecated // Ny }: ThreatActorOverviewProps) { return (