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 0000000..fe901cd Binary files /dev/null and b/StrukturMinneLap.png differ diff --git a/backend/src/graphql/resolvers/threatActorResolver.ts b/backend/src/graphql/resolvers/threatActorResolver.ts index ba49d94..88221fa 100644 --- a/backend/src/graphql/resolvers/threatActorResolver.ts +++ b/backend/src/graphql/resolvers/threatActorResolver.ts @@ -22,25 +22,31 @@ * GraphQL -> Resolver -> Repository -> Drizzle -> MySQL */ -import {getAllThreatActors,getThreatActorByMitreId} from "../../repositories/threatActorRepository.js"; +import {getAllThreatActorsGrid, getThreatActorByMitreId, getThreatActorSoftwares, getThreatActorTechniques} from "../../repositories/threatActorRepository.js"; export const threatActorResolvers = { Query: { /** - * Henter alle ThreatActors. + * Henter alle ThreatActors for Grid i front end. */ - allThreatActors: async () => { - return getAllThreatActors(); + allThreatActorsGrid: async (_parent: unknown, args: {limit: number; offset: number}) => { + return getAllThreatActorsGrid(args.limit, args.offset); }, /** - * Henter én ThreatActor basert på MITRE-ID. - * - * Eksempel: - * G0016 -> APT3 + * Henter én Threat Actor med tilhørende detaljer, + * aliaser, sektorer, teknikker og programvare. */ - threatActorByMitreId: async (_parent: unknown,args: { mitreId: string }) => { + 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 d83fb7d..15416a8 100644 --- a/backend/src/graphql/typeDefs.ts +++ b/backend/src/graphql/typeDefs.ts @@ -10,20 +10,47 @@ 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 { + type ThreatActorGridInfo { + items: [ThreatActorGrid!]! + totalCount: Int! + } + + type AssociatedItem { + mitreId: String! + name: String! + } - # Henter alle threat actors - allThreatActors: [ThreatActor!]! + 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! - # Henter én threat actor basert på MITRE-ID - threatActorByMitreId(mitreId: String!): ThreatActor + aliases: [String!]! + sectors: [String!]! } type Technique { @@ -33,11 +60,34 @@ export const typeDefs = `#graphql } type Query { + ######################################## ThreatActors vvvv + + # Henter alle threat actors som trengs i grid + allThreatActorsGrid(limit: Int!, offset: Int!): ThreatActorGridInfo! + + # Henter alt nødvending info for ThreatActorDetailed i frontend + getThreatActorByMitreId(mitreId: String!): ThreatActorDetailed + + getThreatActorTechniques(mitreId: String!, limit: Int!, offset: Int!): AssociatedItemsPage! + + getThreatActorSoftwares(mitreId: String!, limit: Int!, offset: Int!): AssociatedItemsPage! + + ######################################## Techinques vvvv + # Henter alle Techniques allTechniques: [Technique!]! # Henter én Technique basert på MITRE-ID techniqueByMitreId(mitreId: String!): Technique + + + ######################################## Software vvvv + + + + ######################################## Tactics vvvv + + } `; \ No newline at end of file diff --git a/backend/src/repositories/threatActorRepository.ts b/backend/src/repositories/threatActorRepository.ts index 5c59727..d424ad5 100644 --- a/backend/src/repositories/threatActorRepository.ts +++ b/backend/src/repositories/threatActorRepository.ts @@ -15,72 +15,173 @@ * !!!ALTSÅ HER SKRIVER VI KODEN SOM FAKTISK HENTER UT DATA FRA DB VIA DRIZZLE!!!! */ +import { count, eq, sql } from "drizzle-orm"; + +import { db } from "../db/connection.js"; +import { software, technique, threatActor, threatActorAlias, threatActorSector, threatActorSoftware, threatActorTechnique } from "../db/schema.js"; + /** - * 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. + * 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()) * - * !!!ALTSÅ HER SKRIVER VI KODEN SOM FAKTISK HENTER UT DATA FRA DB VIA DRIZZLE!!!! + * [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 getAllThreatActorsGrid(limit: number = 9, offset: number = 0) { + const actorsSelect = db + .select({ + name: threatActor.name, + mitreId: threatActor.mitre_id, -/** - * 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 - */ + 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) + .orderBy(threatActor.mitre_id) + .limit(limit) + .offset(offset); -import { eq } from "drizzle-orm"; -import { db } from "../db/connection.js"; -import { threatActor } from "../db/schema.js"; + // Hent aktørene og totalt antall parallelt + const [actors, countResult] = await Promise.all([ + actorsSelect, + //Brukes i pagination + db.select({ totalCount: count() }) + .from(threatActor), + ]); -/** - * Henter alle ThreatActors fra databasen. - */ -export async function getAllThreatActors() { - return db - .select({ - mitreId: threatActor.mitre_id, - name: threatActor.name, - description: threatActor.description, - }) - .from(threatActor); + return { + items: actors, + totalCount: countResult[0]?.totalCount ?? 0, + }; } -/** - * Henter én ThreatActor basert på MITRE-ID. - * - * Eksempel: - * G0016 -> APT3 - */ -export async function getThreatActorByMitreId( - mitreId: string -) { - const [result] = await db +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, mitreId) - ) - .limit(1); + .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 result ?? null; + return { + items: softwares, + totalCount: countResult[0]?.totalCount ?? 0, + }; } \ 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/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 (