diff --git a/StrukturMinneLap.png b/StrukturMinneLap.png deleted file mode 100644 index fe901cd..0000000 Binary files a/StrukturMinneLap.png and /dev/null differ diff --git a/frontend/InitialSetup.md b/frontend/InitialSetup.md deleted file mode 100644 index a6b4d60..0000000 --- a/frontend/InitialSetup.md +++ /dev/null @@ -1,76 +0,0 @@ -# React + TypeScript + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) - -## React Compiler - -The React Compiler is enabled on this template. See [this documentation](https://react.dev/learn/react-compiler) for more information. - -Note: This will impact Vite dev & build performances. -You can also try [the experimental native React Compiler support in plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md#rust-react-compiler) by using `compiler: true` in the plugin options instead of using the Babel plugin. - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: - -```js -export default defineConfig([ - globalIgnores(["dist"]), - { - files: ["**/*.{ts,tsx}"], - extends: [ - // Other configs... - - // Remove tseslint.configs.recommended and replace with this - tseslint.configs.recommendedTypeChecked, - // Alternatively, use this for stricter rules - tseslint.configs.strictTypeChecked, - // Optionally, add this for stylistic rules - tseslint.configs.stylisticTypeChecked, - - // Other configs... - ], - languageOptions: { - parserOptions: { - project: ["./tsconfig.node.json", "./tsconfig.app.json"], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]); -``` - -You can also install [eslint-plugin-react-x](https://npmx.dev/package/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://npmx.dev/package/eslint-plugin-react-dom) for React-specific lint rules: - -```js -// eslint.config.js -import reactX from "eslint-plugin-react-x"; -import reactDom from "eslint-plugin-react-dom"; - -export default defineConfig([ - globalIgnores(["dist"]), - { - files: ["**/*.{ts,tsx}"], - extends: [ - // Other configs... - // Enable lint rules for React - reactX.configs["recommended-typescript"], - // Enable lint rules for React DOM - reactDom.configs.recommended, - ], - languageOptions: { - parserOptions: { - project: ["./tsconfig.node.json", "./tsconfig.app.json"], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]); -``` diff --git a/frontend/src/api/graphql.ts b/frontend/src/api/graphql.ts index 2e1192e..c6812be 100644 --- a/frontend/src/api/graphql.ts +++ b/frontend/src/api/graphql.ts @@ -36,4 +36,4 @@ export async function graphqlRequest( } return result.data; -} \ No newline at end of file +} diff --git a/frontend/src/api/graphqlUrl.ts b/frontend/src/api/graphqlUrl.ts index 1d91417..94e4352 100644 --- a/frontend/src/api/graphqlUrl.ts +++ b/frontend/src/api/graphqlUrl.ts @@ -3,4 +3,4 @@ const defaultGraphqlUrl = import.meta.env.DEV : "/graphql"; export const GRAPHQL_URL = - import.meta.env.VITE_GRAPHQL_URL || defaultGraphqlUrl; \ No newline at end of file + import.meta.env.VITE_GRAPHQL_URL || defaultGraphqlUrl; diff --git a/frontend/src/apollo/client.ts b/frontend/src/apollo/client.ts index 7fb7994..2feb94a 100644 --- a/frontend/src/apollo/client.ts +++ b/frontend/src/apollo/client.ts @@ -1,14 +1,9 @@ -import { - ApolloClient, - InMemoryCache, - HttpLink, -} from "@apollo/client"; +import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client"; import { GRAPHQL_URL } from "../api/graphqlUrl"; -export const apolloClient = - new ApolloClient({ - link: new HttpLink({ - uri: GRAPHQL_URL, - }), - cache: new InMemoryCache(), - }); \ No newline at end of file +export const apolloClient = new ApolloClient({ + link: new HttpLink({ + uri: GRAPHQL_URL, + }), + cache: new InMemoryCache(), +}); diff --git a/frontend/src/components/common/FeedBack/FeedbackCard.css b/frontend/src/components/common/FeedBack/FeedbackCard.css new file mode 100644 index 0000000..50c3a65 --- /dev/null +++ b/frontend/src/components/common/FeedBack/FeedbackCard.css @@ -0,0 +1,56 @@ +.feedback-card { + min-height: 220px; + padding: 32px 20px; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12px; + + text-align: center; + + background: var(--surface); + border: 1px solid var(--border); + border-radius: 10px; +} + +.feedback-card > svg { + color: var(--accent); +} + +.feedback-card--error { + border-color: #78424c; +} + +.feedback-card--error > svg { + color: #fb7185; +} + +.feedback-card h2 { + margin: 0; + color: var(--text-heading); + font-size: 20px; +} + +.feedback-card p { + margin: 0; + color: var(--text); + font-size: 14px; +} + +.feedback-spinner { + animation: feedback-spin 1s linear infinite; +} + +@keyframes feedback-spin { + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: reduce) { + .feedback-spinner { + animation: none; + } +} diff --git a/frontend/src/components/common/FeedBack/FeedbackCard.tsx b/frontend/src/components/common/FeedBack/FeedbackCard.tsx new file mode 100644 index 0000000..3771bdb --- /dev/null +++ b/frontend/src/components/common/FeedBack/FeedbackCard.tsx @@ -0,0 +1,37 @@ +import { AlertCircle, LoaderCircle, SearchX } from "lucide-react"; +import "./FeedbackCard.css"; + +type FeedbackCardProps = { + variant: "loading" | "error" | "empty"; + title: string; + message?: string; +}; + +function FeedbackCard({ variant, title, message }: FeedbackCardProps) { + const icons = { + loading: LoaderCircle, + error: AlertCircle, + empty: SearchX, + }; + + const Icon = icons[variant]; + + return ( +
+
+ ); +} + +export default FeedbackCard; diff --git a/frontend/src/components/layout/AppLayout.css b/frontend/src/components/common/Layout/AppLayout.css similarity index 99% rename from frontend/src/components/layout/AppLayout.css rename to frontend/src/components/common/Layout/AppLayout.css index b8da619..112d8a0 100644 --- a/frontend/src/components/layout/AppLayout.css +++ b/frontend/src/components/common/Layout/AppLayout.css @@ -20,4 +20,4 @@ .page-content { padding: 20px; } -} \ No newline at end of file +} diff --git a/frontend/src/components/layout/AppLayout.tsx b/frontend/src/components/common/Layout/AppLayout.tsx similarity index 92% rename from frontend/src/components/layout/AppLayout.tsx rename to frontend/src/components/common/Layout/AppLayout.tsx index 6b6aa1f..8f6c7ef 100644 --- a/frontend/src/components/layout/AppLayout.tsx +++ b/frontend/src/components/common/Layout/AppLayout.tsx @@ -4,12 +4,12 @@ import SideBar from "./sidebar/SideBar"; import "./AppLayout.css"; /** - * + * * Definerer felles layot som brukes av alle pages på nettsiden. - * - * Layoten består av en Header og en Sidebar. Det aktive page innholdet rendres - * inne i , bassert på hvilken Routte brukeren er på nå . - * + * + * Layoten består av en Header og en Sidebar. Det aktive page innholdet rendres + * inne i , bassert på hvilken Routte brukeren er på nå . + * * På denne måten kan Header og Sidebar gjenbrukes på tvers av alle sider uten at de må * importeres og rendres separat i hver page-komponent. */ @@ -29,4 +29,4 @@ function AppLayout() { ); } -export default AppLayout; \ No newline at end of file +export default AppLayout; diff --git a/frontend/src/components/layout/header/Header.css b/frontend/src/components/common/Layout/header/Header.css similarity index 99% rename from frontend/src/components/layout/header/Header.css rename to frontend/src/components/common/Layout/header/Header.css index 6cba2ea..d6d1312 100644 --- a/frontend/src/components/layout/header/Header.css +++ b/frontend/src/components/common/Layout/header/Header.css @@ -209,4 +209,4 @@ width: 19px; height: 19px; } -} \ No newline at end of file +} diff --git a/frontend/src/components/layout/header/Header.tsx b/frontend/src/components/common/Layout/header/Header.tsx similarity index 77% rename from frontend/src/components/layout/header/Header.tsx rename to frontend/src/components/common/Layout/header/Header.tsx index 48e16ec..3b7458e 100644 --- a/frontend/src/components/layout/header/Header.tsx +++ b/frontend/src/components/common/Layout/header/Header.tsx @@ -1,13 +1,20 @@ import { NavLink } from "react-router-dom"; -import {Bookmark, Crosshair,Home, Settings,Shield,Users,Database, UserRound} from "lucide-react"; +import { + Bookmark, + Crosshair, + Home, + Settings, + Shield, + Users, + Database, + UserRound, +} from "lucide-react"; import "./Header.css"; - - /** * Header inneholder logoen CyberThreats, med NavLink for å sende til HomePage, * samt link til MITRE for å sjekke ut datakilden, samt en profil-knapp - * + * * Siden inneholder også en media-query for som viser sidebar navigasjon-icons ved mindre skjærmer */ function Header() { @@ -44,16 +51,23 @@ function Header() { ); } export default Header; - diff --git a/frontend/src/components/layout/sidebar/SideBar.css b/frontend/src/components/common/Layout/sidebar/SideBar.css similarity index 100% rename from frontend/src/components/layout/sidebar/SideBar.css rename to frontend/src/components/common/Layout/sidebar/SideBar.css diff --git a/frontend/src/components/layout/sidebar/SideBar.tsx b/frontend/src/components/common/Layout/sidebar/SideBar.tsx similarity index 89% rename from frontend/src/components/layout/sidebar/SideBar.tsx rename to frontend/src/components/common/Layout/sidebar/SideBar.tsx index e567f11..711b6f5 100644 --- a/frontend/src/components/layout/sidebar/SideBar.tsx +++ b/frontend/src/components/common/Layout/sidebar/SideBar.tsx @@ -1,15 +1,22 @@ -import {Bookmark,Crosshair,Home,Settings,Shield,Users} from "lucide-react"; +import { + Bookmark, + Crosshair, + Home, + Settings, + Shield, + Users, +} from "lucide-react"; import { NavLink } from "react-router-dom"; import "./SideBar.css"; /** * Oppretter sidebar navigasjon på nettsiden - * + * * Tilbyr navigasjons lenker til ulike pages via React Router NavLink. - * + * * På mindre skjærmer er sidebar skjult gjennom media-query, og icons fra NavLinks * blir istedet vist i header.tsx componenten. Responsiv design - * + * */ function SideBar() { return ( @@ -49,4 +56,4 @@ function SideBar() { ); } -export default SideBar; \ No newline at end of file +export default SideBar; diff --git a/frontend/src/components/common/Navigation/BackButton.css b/frontend/src/components/common/Navigation/BackButton.css new file mode 100644 index 0000000..419ce3c --- /dev/null +++ b/frontend/src/components/common/Navigation/BackButton.css @@ -0,0 +1,26 @@ +.back-navigation { + display: inline-flex; + align-items: center; + gap: 6px; + width: fit-content; + + padding: 0; + border: none; + background: none; + + color: var(--accent); + font: inherit; + font-size: 14px; + font-weight: 600; + + cursor: pointer; +} + +.back-navigation:hover { + text-decoration: underline; +} + +.back-navigation:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 4px; +} diff --git a/frontend/src/components/common/Navigation/BackButton.tsx b/frontend/src/components/common/Navigation/BackButton.tsx new file mode 100644 index 0000000..9fb4524 --- /dev/null +++ b/frontend/src/components/common/Navigation/BackButton.tsx @@ -0,0 +1,43 @@ +import { ArrowLeft } from "lucide-react"; +import { useNavigate } from "react-router-dom"; +import "./BackButton.css"; + +type BackButtonProps = { + fallbackTo: string; +}; + +/** + * Gjenbrukbar tilbakeknapp for detaljsider. + * Navigerer til forrige side, eller til fallbackTo dersom + * det ikke finnes en tidligere side i React Routers historikk. + * + * @param fallbackTo - Standardsiden brukeren sendes til. + */ +function BackButton({ fallbackTo }: BackButtonProps) { + /** Innebygd React-Hook som lar oss navigere mellom sider + * Den "husker" den browse-historikken og dermed + * kan man bruke navigate(-1) for å gå tilbake. + */ + const navigate = useNavigate(); + + /** Håndterer trykking av tilbake knappen */ + const handleBack = () => { + /** Hvis de er noe å gå tilbake til */ + if (window.history.state?.idx > 0) { + /** Gå tilbkae */ + navigate(-1); + return; + } + /** Hvis de er ikk noe å gå tilbake til: gå til startpage */ + navigate(fallbackTo, { replace: true }); + }; + + return ( + + ); +} + +export default BackButton; diff --git a/frontend/src/components/common/PageHeader/PageHeader.css b/frontend/src/components/common/PageHeader/PageHeader.css new file mode 100644 index 0000000..5e3a030 --- /dev/null +++ b/frontend/src/components/common/PageHeader/PageHeader.css @@ -0,0 +1,30 @@ +/* ===== Felles Page Header ===== */ + +.page-header { + display: flex; + flex-direction: column; + gap: 8px; +} + +.page-header__eyebrow { + color: var(--text-muted); + + font-size: 13px; + font-weight: 700; + letter-spacing: 0.08em; +} + +.page-header__title { + font-size: clamp(28px, 3vw, 40px); + font-weight: 800; + line-height: 1; +} + +.page-header__description { + max-width: 760px; + + color: var(--text); + + font-size: 16px; + line-height: 1.5; +} diff --git a/frontend/src/components/common/PageHeader/PageHeader.tsx b/frontend/src/components/common/PageHeader/PageHeader.tsx new file mode 100644 index 0000000..0c23f53 --- /dev/null +++ b/frontend/src/components/common/PageHeader/PageHeader.tsx @@ -0,0 +1,33 @@ +/** Én felles css for header design */ +import "./PageHeader.css"; + +type PageHeaderProps = { + title: string; + description: string; + children?: React.ReactNode; // brukes i software +}; + +/** + * Gjenbrukbar header-komponent for applikasjonens sider. + * + * Viser en felles overskrift (eyebrow), sidetittel og beskrivelse. + * Bruker samme CSS på tvers av sidene for å sikre konsistent design. + * + * @param title - Sidens hovedoverskrift. + * @param description - Kort beskrivelse av sidens innhold. + * @param children - Valgfritt React-innhold som vises under beskrivelsen. + */ +function PageHeader({ title, description }: PageHeaderProps) { + const eyebrow = "THREAT INTELLIGENCE"; + return ( +
+ {eyebrow && {eyebrow}} + +

{title}

+ +

{description}

+
+ ); +} + +export default PageHeader; diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorSearch/ThreatActorSearch.css b/frontend/src/components/common/SearchBar/SearchBar.css similarity index 52% rename from frontend/src/components/threatactor/ThreatActorMain/ThreatActorSearch/ThreatActorSearch.css rename to frontend/src/components/common/SearchBar/SearchBar.css index 466d447..159196b 100644 --- a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorSearch/ThreatActorSearch.css +++ b/frontend/src/components/common/SearchBar/SearchBar.css @@ -1,4 +1,6 @@ -.threat-actor-search { +/* ===== Felles SearchBar ===== */ + +.search-bar { width: 100%; min-height: 52px; @@ -17,11 +19,22 @@ background-color 0.2s ease; } -.threat-actor-search:focus-within { +/* Marker søkefeltet når input har fokus */ +.search-bar:focus-within { border-color: var(--accent); } -.threat-actor-search > svg { +/* Container for søkeikon og input */ +.search-bar__field { + flex: 1; + min-width: 0; + + display: flex; + align-items: center; +} + +/* Søkeikon */ +.search-bar__field > svg { width: 22px; height: 22px; @@ -31,11 +44,12 @@ flex-shrink: 0; } -.threat-actor-search:focus-within > svg { +.search-bar:focus-within .search-bar__field > svg { color: var(--accent); } -.threat-actor-search input { +/* Inputfelt */ +.search-bar__field input { flex: 1; min-width: 0; @@ -47,10 +61,10 @@ background: transparent; color: #f0f7f7; font-size: 14px; - } -.threat-actor-search button { +/* Search-knapp */ +.search-bar__button { align-self: stretch; min-width: 120px; @@ -62,6 +76,19 @@ color: var(--bg); font-weight: 700; - cursor: pointer; -} \ No newline at end of file +} + +/* Tastaturnavigasjon */ +.search-bar__button:focus-visible { + outline: 2px solid var(--text); + outline-offset: -4px; +} + +/* Tilpasning for mindre skjermer */ +@media (max-width: 480px) { + .search-bar__button { + min-width: 90px; + padding: 0 12px; + } +} diff --git a/frontend/src/components/common/SearchBar/SearchBar.tsx b/frontend/src/components/common/SearchBar/SearchBar.tsx new file mode 100644 index 0000000..2d5e709 --- /dev/null +++ b/frontend/src/components/common/SearchBar/SearchBar.tsx @@ -0,0 +1,51 @@ +import { Search } from "lucide-react"; +import "./SearchBar.css"; + +type SearchBarProps = { + value: string; + placeholder: string; + onChange: (value: string) => void; + onSearch?: (value: string) => void; +}; + +/** + * Felles søkefelt for katalogsidene. + * + * Komponenten håndterer kun presentasjon og brukerinteraksjon. + * Selve søkelogikken bestemmes av komponenten som bruker SearchBar. + * + * @param value - Nåværende tekst i søkefeltet. + * @param placeholder - Hjelpetekst i inputfeltet. + * @param onChange - Kalles når søketeksten endres. + * @param onSearch - Valgfri callback når søket sendes inn. + */ +function SearchBar({ value, placeholder, onChange, onSearch }: SearchBarProps) { + return ( +
{ + event.preventDefault(); + onSearch?.(value); + }} + > +
+
+ + +
+ ); +} + +export default SearchBar; diff --git a/frontend/src/components/feedback/FeedbackCard.css b/frontend/src/components/feedback/FeedbackCard.css deleted file mode 100644 index 0c4332c..0000000 --- a/frontend/src/components/feedback/FeedbackCard.css +++ /dev/null @@ -1,57 +0,0 @@ - -.feedback-card { - min-height: 220px; - padding: 32px 20px; - - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 12px; - - text-align: center; - - background: var(--surface); - border: 1px solid var(--border); - border-radius: 10px; -} - -.feedback-card > svg { - color: var(--accent); -} - -.feedback-card--error { - border-color: #78424c; -} - -.feedback-card--error > svg { - color: #fb7185; -} - -.feedback-card h2 { - margin: 0; - color: var(--text-heading); - font-size: 20px; -} - -.feedback-card p { - margin: 0; - color: var(--text); - font-size: 14px; -} - -.feedback-spinner { - animation: feedback-spin 1s linear infinite; -} - -@keyframes feedback-spin { - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: reduce) { - .feedback-spinner { - animation: none; - } -} diff --git a/frontend/src/components/feedback/FeedbackCard.tsx b/frontend/src/components/feedback/FeedbackCard.tsx deleted file mode 100644 index 098f9f7..0000000 --- a/frontend/src/components/feedback/FeedbackCard.tsx +++ /dev/null @@ -1,39 +0,0 @@ - -import { AlertCircle, LoaderCircle, SearchX } from "lucide-react"; -import "./FeedbackCard.css"; - -type FeedbackCardProps = { - variant: "loading" | "error" | "empty"; - title: string; - message?: string; -}; - -function FeedbackCard({ variant, title, message }: FeedbackCardProps) { - - const icons = { - loading: LoaderCircle, - error: AlertCircle, - empty: SearchX, - }; - - const Icon = icons[variant]; - - return ( -
-
- ); -} - -export default FeedbackCard; diff --git a/frontend/src/components/navigation/BackButton.css b/frontend/src/components/navigation/BackButton.css deleted file mode 100644 index 1d8a4c4..0000000 --- a/frontend/src/components/navigation/BackButton.css +++ /dev/null @@ -1,27 +0,0 @@ - -.back-navigation { - display: inline-flex; - align-items: center; - gap: 6px; - width: fit-content; - - padding: 0; - border: none; - background: none; - - color: var(--accent); - font: inherit; - font-size: 14px; - font-weight: 600; - - cursor: pointer; -} - -.back-navigation:hover { - text-decoration: underline; -} - -.back-navigation:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 4px; -} diff --git a/frontend/src/components/navigation/BackButton.tsx b/frontend/src/components/navigation/BackButton.tsx deleted file mode 100644 index bd93832..0000000 --- a/frontend/src/components/navigation/BackButton.tsx +++ /dev/null @@ -1,43 +0,0 @@ - -import { ArrowLeft } from "lucide-react"; -import { useNavigate } from "react-router-dom"; -import "./BackButton.css"; - -type BackButtonProps = { - fallbackTo: string; -}; - -/** - * Gjenbrukbar tilbakeknapp for detaljsider. - * Navigerer til forrige side, eller til fallbackTo dersom - * det ikke finnes en tidligere side i React Routers historikk. - * - * @param fallbackTo - Standardsiden brukeren sendes til. - */ -function BackButton({ fallbackTo }: BackButtonProps) { - /** Innebygd React-Hook som lar oss navigere mellom sider - * Den "husker" den browse-historikken og dermed - * kan man bruke navigate(-1) for å gå tilbake. - */ - const navigate = useNavigate(); - - /** Håndterer trykking av tilbake knappen */ - const handleBack = () => { - /** Hvis de er noe å gå tilbake til */ - if (window.history.state?.idx > 0) { - /** Gå tilbkae */ - navigate(-1); - return; - } - /** Hvis de er ikk noe å gå tilbake til: gå til startpage */ - navigate(fallbackTo, { replace: true }); - }; - - return ( - - ); -} - -export default BackButton; diff --git a/frontend/src/components/tactic/TacticDetailed/RelatedTechniques/RelatedTechniques.css b/frontend/src/components/tactic/TacticDetailed/RelatedTechniques/RelatedTechniques.css index 6cffe66..863e831 100644 --- a/frontend/src/components/tactic/TacticDetailed/RelatedTechniques/RelatedTechniques.css +++ b/frontend/src/components/tactic/TacticDetailed/RelatedTechniques/RelatedTechniques.css @@ -9,7 +9,6 @@ border-radius: 8px; } - /* ===== Header ===== */ .related-techniques-header { @@ -43,7 +42,6 @@ line-height: 1.4; } - /* ===== Search ===== */ .related-techniques-search { @@ -115,7 +113,6 @@ opacity: 0.9; } - /* ===== List ===== */ .related-techniques-list { @@ -173,7 +170,6 @@ outline-offset: 2px; } - /* ===== Technique row ===== */ .related-technique-row { @@ -199,7 +195,6 @@ background: var(--surface-hover); } - /* ===== Icon ===== */ .related-technique-icon { @@ -224,7 +219,6 @@ height: 18px; } - /* ===== MITRE ID ===== */ .related-technique-id { @@ -246,7 +240,6 @@ grid-row: 1; } - /* ===== Technique text ===== */ .related-technique-content { @@ -262,7 +255,6 @@ text-align: center; } - /* ===== Arrow ===== */ .related-technique-arrow { @@ -279,7 +271,6 @@ color: var(--accent); } - /* ===== Tablet / mobile ===== */ @media (max-width: 700px) { @@ -310,4 +301,4 @@ padding: 4px 6px; font-size: 12px; } -} \ No newline at end of file +} diff --git a/frontend/src/components/tactic/TacticDetailed/RelatedTechniques/RelatedTechniques.tsx b/frontend/src/components/tactic/TacticDetailed/RelatedTechniques/RelatedTechniques.tsx index 9464fe0..f704543 100644 --- a/frontend/src/components/tactic/TacticDetailed/RelatedTechniques/RelatedTechniques.tsx +++ b/frontend/src/components/tactic/TacticDetailed/RelatedTechniques/RelatedTechniques.tsx @@ -38,37 +38,25 @@ function RelatedTechniques({
-

- Related Techniques ({totalCount}) -

+

Related Techniques ({totalCount})

-

- Techniques that can be used to achieve {tacticName}. -

+

Techniques that can be used to achieve {tacticName}.

- +
- +
{techniques.map((technique) => ( -
+
@@ -115,4 +103,4 @@ function RelatedTechniques({ ); } -export default RelatedTechniques; \ No newline at end of file +export default RelatedTechniques; diff --git a/frontend/src/components/tactic/TacticDetailed/TacticDescription/TacticDescription.css b/frontend/src/components/tactic/TacticDetailed/TacticDescription/TacticDescription.css index 69c7e4e..193c775 100644 --- a/frontend/src/components/tactic/TacticDetailed/TacticDescription/TacticDescription.css +++ b/frontend/src/components/tactic/TacticDetailed/TacticDescription/TacticDescription.css @@ -35,4 +35,4 @@ .tactic-description p { font-size: 12px; } -} \ No newline at end of file +} diff --git a/frontend/src/components/tactic/TacticDetailed/TacticDescription/TacticDescription.tsx b/frontend/src/components/tactic/TacticDetailed/TacticDescription/TacticDescription.tsx index 2a6cfc9..959fb65 100644 --- a/frontend/src/components/tactic/TacticDetailed/TacticDescription/TacticDescription.tsx +++ b/frontend/src/components/tactic/TacticDetailed/TacticDescription/TacticDescription.tsx @@ -4,9 +4,7 @@ type TacticDescriptionProps = { description: string; }; -function TacticDescription({ - description, -}: TacticDescriptionProps) { +function TacticDescription({ description }: TacticDescriptionProps) { return (

Description

@@ -15,4 +13,4 @@ function TacticDescription({ ); } -export default TacticDescription; \ No newline at end of file +export default TacticDescription; diff --git a/frontend/src/components/tactic/TacticDetailed/TacticDetailedHeader/TacticDetailedHeader.css b/frontend/src/components/tactic/TacticDetailed/TacticDetailedHeader/TacticDetailedHeader.css index 25c21f8..bb17697 100644 --- a/frontend/src/components/tactic/TacticDetailed/TacticDetailedHeader/TacticDetailedHeader.css +++ b/frontend/src/components/tactic/TacticDetailed/TacticDetailedHeader/TacticDetailedHeader.css @@ -6,7 +6,6 @@ gap: 8px; } - /* ===== Back link ===== */ .tactic-detailed-back-link { @@ -32,7 +31,6 @@ text-decoration: underline; } - /* ===== Type ===== */ .tactic-detailed-type { @@ -45,7 +43,6 @@ letter-spacing: 0.08em; } - /* ===== Title ===== */ .tactic-detailed-title-row { @@ -68,7 +65,6 @@ line-height: 1; } - /* MITRE ID badge */ .tactic-detailed-title-row p { @@ -86,7 +82,6 @@ font-weight: 700; } - /* ===== Favorite ===== */ .tactic-detailed-favorite-button { @@ -116,7 +111,6 @@ color: var(--accent); } - /* ===== Responsive ===== */ @media (max-width: 520px) { @@ -132,4 +126,4 @@ width: 32px; height: 32px; } -} \ No newline at end of file +} diff --git a/frontend/src/components/tactic/TacticDetailed/TacticDetailedHeader/TacticDetailedHeader.tsx b/frontend/src/components/tactic/TacticDetailed/TacticDetailedHeader/TacticDetailedHeader.tsx index 3f0335d..447da79 100644 --- a/frontend/src/components/tactic/TacticDetailed/TacticDetailedHeader/TacticDetailedHeader.tsx +++ b/frontend/src/components/tactic/TacticDetailed/TacticDetailedHeader/TacticDetailedHeader.tsx @@ -1,25 +1,19 @@ -import { ArrowLeft, Star } from "lucide-react"; -import { Link } from "react-router-dom"; +import { Star } from "lucide-react"; import "./TacticDetailedHeader.css"; -import BackButton from "../../../navigation/BackButton"; +import BackButton from "../../../common/Navigation/BackButton"; type TacticDetailedHeaderProps = { mitreId: string; name: string; }; -function TacticDetailedHeader({ - mitreId, - name, -}: TacticDetailedHeaderProps) { +function TacticDetailedHeader({ mitreId, name }: TacticDetailedHeaderProps) { return (
- - TACTIC - + TACTIC
@@ -39,4 +33,4 @@ function TacticDetailedHeader({ ); } -export default TacticDetailedHeader; \ No newline at end of file +export default TacticDetailedHeader; diff --git a/frontend/src/components/tactic/TacticMain/TacticCard/TacticCard.css b/frontend/src/components/tactic/TacticMain/TacticCard/TacticCard.css index 64c3269..813cd27 100644 --- a/frontend/src/components/tactic/TacticMain/TacticCard/TacticCard.css +++ b/frontend/src/components/tactic/TacticMain/TacticCard/TacticCard.css @@ -16,7 +16,6 @@ border-radius: 8px; } - /* ===== Header ===== */ .tactic-card-header { @@ -57,7 +56,6 @@ font-weight: 700; } - /* ===== Content ===== */ .tactic-card-content { @@ -78,7 +76,6 @@ line-height: 1.4; } - /* ===== Footer ===== */ .tactic-card-footer { @@ -102,7 +99,6 @@ flex-shrink: 0; } - /* ===== Hover ===== */ .tactic-card:hover { @@ -113,7 +109,6 @@ text-decoration: underline; } - /* ===== Individual tactic colors ===== */ .tactic-initial-access .tactic-card-icon { @@ -124,7 +119,6 @@ background: rgba(43, 179, 255, 0.12); } - .tactic-execution .tactic-card-icon { color: #3df0b3; } @@ -133,7 +127,6 @@ background: rgba(61, 240, 179, 0.12); } - .tactic-persistence .tactic-card-icon { color: #b56cff; } @@ -142,7 +135,6 @@ background: rgba(181, 108, 255, 0.12); } - .tactic-privilege-escalation .tactic-card-icon { color: #ff5d73; } @@ -151,7 +143,6 @@ background: rgba(255, 93, 115, 0.12); } - .tactic-defense-evasion .tactic-card-icon { color: #e8eeee; } @@ -160,7 +151,6 @@ background: rgba(232, 238, 238, 0.1); } - .tactic-credential-access .tactic-card-icon { color: #ffd84a; } @@ -169,7 +159,6 @@ background: rgba(255, 216, 74, 0.12); } - .tactic-discovery .tactic-card-icon { color: #26e4ff; } @@ -178,7 +167,6 @@ background: rgba(38, 228, 255, 0.12); } - .tactic-lateral-movement .tactic-card-icon { color: #ff5ccf; } @@ -187,7 +175,6 @@ background: rgba(255, 92, 207, 0.12); } - .tactic-collection .tactic-card-icon { color: #ffad33; } @@ -196,7 +183,6 @@ background: rgba(255, 173, 51, 0.12); } - .tactic-exfiltration .tactic-card-icon { color: #35f0d2; } @@ -205,7 +191,6 @@ background: rgba(53, 240, 210, 0.12); } - .tactic-command-control .tactic-card-icon { color: #b96cff; } @@ -214,7 +199,6 @@ background: rgba(185, 108, 255, 0.12); } - .tactic-impact .tactic-card-icon { color: #ff5d73; } @@ -223,7 +207,6 @@ background: rgba(255, 93, 115, 0.12); } - .tactic-resource-development .tactic-card-icon { color: #52d67a; } @@ -232,7 +215,6 @@ background: rgba(82, 214, 122, 0.12); } - .tactic-reconnaissance .tactic-card-icon { color: #56b6ff; } @@ -241,7 +223,6 @@ background: rgba(86, 182, 255, 0.12); } - .tactic-defense-impairment .tactic-card-icon { color: #ff9f43; } @@ -250,14 +231,12 @@ background: rgba(255, 159, 67, 0.12); } - /* ===== Fallback ===== */ .tactic-default .tactic-card-icon { color: var(--accent); } - /* ===== Mobile ===== */ @media (max-width: 520px) { @@ -279,4 +258,4 @@ .tactic-card-content h2 { font-size: 14px; } -} \ No newline at end of file +} diff --git a/frontend/src/components/tactic/TacticMain/TacticCard/TacticCard.tsx b/frontend/src/components/tactic/TacticMain/TacticCard/TacticCard.tsx index 03687b9..3982861 100644 --- a/frontend/src/components/tactic/TacticMain/TacticCard/TacticCard.tsx +++ b/frontend/src/components/tactic/TacticMain/TacticCard/TacticCard.tsx @@ -18,10 +18,7 @@ function TacticCard({ description, techniqueCount, }: TacticCardProps) { - const { - icon: Icon, - colorClass, - } = getTacticVisual(mitreId); + const { icon: Icon, colorClass } = getTacticVisual(mitreId); return (
@@ -30,9 +27,7 @@ function TacticCard({
- - {mitreId} - + {mitreId}
@@ -41,10 +36,7 @@ function TacticCard({

{description}

- + {techniqueCount} techniques @@ -53,4 +45,4 @@ function TacticCard({ ); } -export default TacticCard; \ No newline at end of file +export default TacticCard; diff --git a/frontend/src/components/tactic/TacticMain/TacticGrid/TacticGrid.css b/frontend/src/components/tactic/TacticMain/TacticGrid/TacticGrid.css index 2c52142..35155d0 100644 --- a/frontend/src/components/tactic/TacticMain/TacticGrid/TacticGrid.css +++ b/frontend/src/components/tactic/TacticMain/TacticGrid/TacticGrid.css @@ -8,7 +8,6 @@ gap: 12px 16px; } - /* ===== Responsive ===== */ @media (max-width: 1200px) { @@ -27,4 +26,4 @@ .tactic-grid { grid-template-columns: 1fr; } -} \ No newline at end of file +} diff --git a/frontend/src/components/tactic/TacticMain/TacticGrid/TacticGrid.tsx b/frontend/src/components/tactic/TacticMain/TacticGrid/TacticGrid.tsx index 2d1a31f..0ac5c5c 100644 --- a/frontend/src/components/tactic/TacticMain/TacticGrid/TacticGrid.tsx +++ b/frontend/src/components/tactic/TacticMain/TacticGrid/TacticGrid.tsx @@ -28,4 +28,4 @@ function TacticGrid({ tactics }: TacticGridProps) { ); } -export default TacticGrid; \ No newline at end of file +export default TacticGrid; diff --git a/frontend/src/components/tactic/TacticMain/TacticToolbar/TacticToolbar.css b/frontend/src/components/tactic/TacticMain/TacticToolbar/TacticToolbar.css index e2126fe..7dae615 100644 --- a/frontend/src/components/tactic/TacticMain/TacticToolbar/TacticToolbar.css +++ b/frontend/src/components/tactic/TacticMain/TacticToolbar/TacticToolbar.css @@ -6,7 +6,6 @@ gap: 20px; } - /* ===== Header ===== */ .tactic-toolbar-header { @@ -38,7 +37,6 @@ line-height: 1.5; } - /* ===== Search ===== */ .tactic-toolbar-search { @@ -105,7 +103,6 @@ opacity: 0.9; } - /* ===== Controls ===== */ .tactic-toolbar-controls { @@ -177,7 +174,6 @@ border-color: var(--accent); } - /* ===== Tablet ===== */ @media (max-width: 768px) { @@ -194,7 +190,6 @@ } } - /* ===== Mobile ===== */ @media (max-width: 520px) { @@ -213,4 +208,4 @@ .tactic-search-button { padding: 13px; } -} \ No newline at end of file +} diff --git a/frontend/src/components/tactic/TacticMain/TacticToolbar/TacticToolbar.tsx b/frontend/src/components/tactic/TacticMain/TacticToolbar/TacticToolbar.tsx index 8bbe305..8499bf3 100644 --- a/frontend/src/components/tactic/TacticMain/TacticToolbar/TacticToolbar.tsx +++ b/frontend/src/components/tactic/TacticMain/TacticToolbar/TacticToolbar.tsx @@ -1,22 +1,14 @@ import { Search, ArrowUpDown, ChevronDown } from "lucide-react"; import "./TacticToolbar.css"; - +import PageHeader from "../../../common/PageHeader/PageHeader"; function TacticToolbar() { return (
-
- - THREAT INTELLIGENCE - - -

Tactics

- -

- Explore ATT&CK tactics and adversary objectives to understand - the goals behind cyber attacks. -

-
+
@@ -34,21 +26,18 @@ function TacticToolbar() {
-
- - Sort by - + Sort by - +
); } -export default TacticToolbar; \ No newline at end of file +export default TacticToolbar; diff --git a/frontend/src/components/techniques/allTechniques/TechniqueCard/TechniqueCard.css b/frontend/src/components/techniques/allTechniques/TechniqueCard/TechniqueCard.css index 39035ef..923b191 100644 --- a/frontend/src/components/techniques/allTechniques/TechniqueCard/TechniqueCard.css +++ b/frontend/src/components/techniques/allTechniques/TechniqueCard/TechniqueCard.css @@ -67,8 +67,8 @@ } .technique-card-content h2 { -font-size: 18px; -font-weight: 700; + font-size: 18px; + font-weight: 700; } .technique-card-content p { @@ -101,13 +101,12 @@ font-weight: 700; } .technique-tactic-tag { -background: rgba(181, 108, 255, 0.18); -color: #d58cff; + background: rgba(181, 108, 255, 0.18); + color: #d58cff; } -  -.technique-platform-tag { -background: rgba(59, 130, 246, 0.18); -color: #a5d4ff; +  .technique-platform-tag { + background: rgba(59, 130, 246, 0.18); + color: #a5d4ff; } /* ===== Footer ===== */ @@ -233,4 +232,4 @@ color: #a5d4ff; .technique-credential-access .technique-tactic-tag { background: rgba(86, 182, 255, 0.18); color: #8fd3ff; -} \ 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 2aa9514..e7d172f 100644 --- a/frontend/src/components/techniques/allTechniques/TechniqueCard/TechniqueCard.tsx +++ b/frontend/src/components/techniques/allTechniques/TechniqueCard/TechniqueCard.tsx @@ -16,47 +16,40 @@ function TechniqueCard({ tactic, platform, }: TechniqueCardProps) { - console.log("TACTIC:", tactic); - const { - icon: TechniqueIcon, - colorClass, - } = getTechniqueCardVisual(tactic); - return ( -
-
-
- -
- -
- - {mitreId} - -

- {name} -

-
-
+ console.log("TACTIC:", tactic); + const { icon: TechniqueIcon, colorClass } = getTechniqueCardVisual(tactic); + return ( +
+
+
+ +
-
- - {tactic ?? "Unknown"} - +
+ {mitreId} +

{name}

+
+
- - {platform ?? "Unknown"} - -
+
+ + {tactic ?? "Unknown"} + - - Open profile - - - - ); + + {platform ?? "Unknown"} + +
+ + + Open profile + + + + ); } -export default TechniqueCard; \ No newline at end of file +export default TechniqueCard; diff --git a/frontend/src/components/techniques/allTechniques/TechniqueCard/getTechniqueCardVisual.tsx b/frontend/src/components/techniques/allTechniques/TechniqueCard/getTechniqueCardVisual.tsx index 763b1b3..18babc2 100644 --- a/frontend/src/components/techniques/allTechniques/TechniqueCard/getTechniqueCardVisual.tsx +++ b/frontend/src/components/techniques/allTechniques/TechniqueCard/getTechniqueCardVisual.tsx @@ -10,7 +10,7 @@ import { FileText, KeyRound, Upload, - AlertTriangle + AlertTriangle, } from "lucide-react"; import type { LucideIcon } from "lucide-react"; @@ -26,12 +26,12 @@ const tacticVisualMap: Record = { colorClass: "technique-initial-access", }, - "Execution": { + Execution: { icon: TerminalSquare, colorClass: "technique-execution", }, - "Persistence": { + Persistence: { icon: Layers3, colorClass: "technique-persistence", }, @@ -51,7 +51,7 @@ const tacticVisualMap: Record = { colorClass: "technique-credential-access", }, - "Discovery": { + Discovery: { icon: Eye, colorClass: "technique-discovery", }, @@ -61,7 +61,7 @@ const tacticVisualMap: Record = { colorClass: "technique-lateral-movement", }, - "Collection": { + Collection: { icon: FileText, colorClass: "technique-collection", }, @@ -71,20 +71,18 @@ const tacticVisualMap: Record = { colorClass: "technique-command-and-control", }, - "Exfiltration": { + Exfiltration: { icon: Upload, colorClass: "technique-exfiltration", }, - "Impact": { + Impact: { icon: AlertTriangle, colorClass: "technique-impact", }, }; -function getTechniqueCardVisual( - tactic: string, -): TechniqueVisual { +function getTechniqueCardVisual(tactic: string): TechniqueVisual { return ( tacticVisualMap[tactic] ?? { icon: FileText, @@ -93,4 +91,4 @@ function getTechniqueCardVisual( ); } -export default getTechniqueCardVisual; \ No newline at end of file +export default getTechniqueCardVisual; diff --git a/frontend/src/components/techniques/allTechniques/TechniqueGrid/TechniqueGrid.tsx b/frontend/src/components/techniques/allTechniques/TechniqueGrid/TechniqueGrid.tsx index 685dc03..8f5888e 100644 --- a/frontend/src/components/techniques/allTechniques/TechniqueGrid/TechniqueGrid.tsx +++ b/frontend/src/components/techniques/allTechniques/TechniqueGrid/TechniqueGrid.tsx @@ -15,9 +15,7 @@ type TechniqueGridProps = { pageSize: number; techniquesCount: number; - onPageChange: ( - page: number, - ) => void; + onPageChange: (page: number) => void; }; function TechniqueGrid({ @@ -27,54 +25,45 @@ function TechniqueGrid({ techniquesCount, onPageChange, }: TechniqueGridProps) { - const totalPossiblePages = - Math.ceil( - techniquesCount / pageSize, - ); + const totalPossiblePages = Math.ceil(techniquesCount / pageSize); return ( - <> -
- {techniques.map((technique) => ( - - ))} -
+ <> +
+ {techniques.map((technique) => ( + + ))} +
-
- +
+ - - {techniquesCount === 0 - ? 0 - : (page - 1) * pageSize + 1} - – - {Math.min( - page * pageSize, - techniquesCount, - )} - + + {techniquesCount === 0 ? 0 : (page - 1) * pageSize + 1}– + {Math.min(page * pageSize, techniquesCount)} + - -
- -); + +
+ + ); } -export default TechniqueGrid; \ No newline at end of file +export default TechniqueGrid; diff --git a/frontend/src/components/techniques/allTechniques/TechniqueToolbar/TechniqueToolbar.css b/frontend/src/components/techniques/allTechniques/TechniqueToolbar/TechniqueToolbar.css index e22ce89..d5bb60b 100644 --- a/frontend/src/components/techniques/allTechniques/TechniqueToolbar/TechniqueToolbar.css +++ b/frontend/src/components/techniques/allTechniques/TechniqueToolbar/TechniqueToolbar.css @@ -167,4 +167,4 @@ .technique-control-button:hover { background: var(--surface-hover); -} \ No newline at end of file +} diff --git a/frontend/src/components/techniques/allTechniques/TechniqueToolbar/TechniqueToolbar.tsx b/frontend/src/components/techniques/allTechniques/TechniqueToolbar/TechniqueToolbar.tsx index d1b17ff..8976e56 100644 --- a/frontend/src/components/techniques/allTechniques/TechniqueToolbar/TechniqueToolbar.tsx +++ b/frontend/src/components/techniques/allTechniques/TechniqueToolbar/TechniqueToolbar.tsx @@ -1,26 +1,15 @@ -import { - Search, - ArrowUpDown, - ChevronDown, -} from "lucide-react"; +import { Search, ArrowUpDown, ChevronDown } from "lucide-react"; import "./TechniqueToolbar.css"; +import PageHeader from "../../../common/PageHeader/PageHeader"; function TechniqueToolbar() { return (
-
- - THREAT INTELLIGENCE - - -

Techniques

- -

- Browse documented techniques and search by name, - MITRE ID, or keyword. -

-
+
@@ -32,46 +21,30 @@ function TechniqueToolbar() { />
-
- - Filters - + Filters - -
- - Sort by - + Sort by -
-
-

{description}

-
+
+

{description}

+
); } diff --git a/frontend/src/components/techniques/specificTechnique/TechniqueDetailedHeader/TechniqueDetailedHeader.css b/frontend/src/components/techniques/specificTechnique/TechniqueDetailedHeader/TechniqueDetailedHeader.css index a36c055..6ff5a23 100644 --- a/frontend/src/components/techniques/specificTechnique/TechniqueDetailedHeader/TechniqueDetailedHeader.css +++ b/frontend/src/components/techniques/specificTechnique/TechniqueDetailedHeader/TechniqueDetailedHeader.css @@ -64,7 +64,7 @@ border-radius: 999px; - background: rgba(0, 0, 0, 0.946);; + background: rgba(0, 0, 0, 0.946); color: #6d3bf7; } @@ -75,4 +75,4 @@ background: rgba(0, 0, 0, 0.85); color: #c7ddff; -} \ No newline at end of file +} diff --git a/frontend/src/components/techniques/specificTechnique/TechniqueDetailedHeader/TechniqueDetailedHeader.tsx b/frontend/src/components/techniques/specificTechnique/TechniqueDetailedHeader/TechniqueDetailedHeader.tsx index 65fbb41..1551da8 100644 --- a/frontend/src/components/techniques/specificTechnique/TechniqueDetailedHeader/TechniqueDetailedHeader.tsx +++ b/frontend/src/components/techniques/specificTechnique/TechniqueDetailedHeader/TechniqueDetailedHeader.tsx @@ -1,8 +1,7 @@ -import { ArrowLeft, Star } from "lucide-react"; -import { Link } from "react-router-dom"; +import { Star } from "lucide-react"; import "./TechniqueDetailedHeader.css"; -import BackButton from "../../../navigation/BackButton"; +import BackButton from "../../../common/Navigation/BackButton"; type TechniqueDetailedHeaderProps = { mitreId: string; @@ -20,10 +19,8 @@ function TechniqueDetailedHeader({ return (
- - - TECHNIQUE - + + TECHNIQUE
@@ -41,15 +38,10 @@ function TechniqueDetailedHeader({
- - {tactic} - + {tactic} {platforms.map((platform) => ( - + {platform} ))} @@ -58,4 +50,4 @@ function TechniqueDetailedHeader({ ); } -export default TechniqueDetailedHeader; \ No newline at end of file +export default TechniqueDetailedHeader; diff --git a/frontend/src/components/techniques/specificTechnique/TechniqueOverview/TechniqueOverview.css b/frontend/src/components/techniques/specificTechnique/TechniqueOverview/TechniqueOverview.css index ad25410..b589f36 100644 --- a/frontend/src/components/techniques/specificTechnique/TechniqueOverview/TechniqueOverview.css +++ b/frontend/src/components/techniques/specificTechnique/TechniqueOverview/TechniqueOverview.css @@ -25,4 +25,4 @@ .technique-overview-value { font-weight: 600; -} \ No newline at end of file +} diff --git a/frontend/src/components/techniques/specificTechnique/TechniqueOverview/TechniqueOverview.tsx b/frontend/src/components/techniques/specificTechnique/TechniqueOverview/TechniqueOverview.tsx index fc290f2..5b96369 100644 --- a/frontend/src/components/techniques/specificTechnique/TechniqueOverview/TechniqueOverview.tsx +++ b/frontend/src/components/techniques/specificTechnique/TechniqueOverview/TechniqueOverview.tsx @@ -25,21 +25,16 @@ function TechniqueOverview({

Related Tactic: {tactic}

-

- Platforms: {platform.join(", ")} -

+

Platforms: {platform.join(", ")}

Parent Technique: None

- Sub-techniques: { - subTechniqueCount > 0 - ? `Yes (${subTechniqueCount})` - : "No" - } + Sub-techniques:{" "} + {subTechniqueCount > 0 ? `Yes (${subTechniqueCount})` : "No"}

); } -export default TechniqueOverview; \ No newline at end of file +export default TechniqueOverview; diff --git a/frontend/src/components/techniques/specificTechnique/TechniqueSubTechnique/SubTechniques.css b/frontend/src/components/techniques/specificTechnique/TechniqueSubTechnique/SubTechniques.css index 86339fd..7e6f40d 100644 --- a/frontend/src/components/techniques/specificTechnique/TechniqueSubTechnique/SubTechniques.css +++ b/frontend/src/components/techniques/specificTechnique/TechniqueSubTechnique/SubTechniques.css @@ -20,9 +20,9 @@ } .sub-technique-card { - display: flex; - justify-content: space-between; - align-items: center; + display: flex; + justify-content: space-between; + align-items: center; } .sub-technique-left { @@ -33,4 +33,4 @@ .sub-technique-card + .sub-technique-card { margin-top: 10px; -} \ No newline at end of file +} diff --git a/frontend/src/components/techniques/specificTechnique/TechniqueSubTechnique/SubTechniques.tsx b/frontend/src/components/techniques/specificTechnique/TechniqueSubTechnique/SubTechniques.tsx index 04640ae..c119d6b 100644 --- a/frontend/src/components/techniques/specificTechnique/TechniqueSubTechnique/SubTechniques.tsx +++ b/frontend/src/components/techniques/specificTechnique/TechniqueSubTechnique/SubTechniques.tsx @@ -10,9 +10,7 @@ type SubTechniquesProps = { subTechniques: SubTechnique[]; }; -function SubTechniques({ - subTechniques, -}: SubTechniquesProps) { +function SubTechniques({ subTechniques }: SubTechniquesProps) { return (
@@ -22,10 +20,7 @@ function SubTechniques({
{subTechniques.map((technique) => ( -
+
@@ -41,4 +36,4 @@ function SubTechniques({ ); } -export default SubTechniques; \ No newline at end of file +export default SubTechniques; diff --git a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorAssociated/ThreatActorAssociatedList.tsx b/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorAssociated/ThreatActorAssociatedList.tsx deleted file mode 100644 index e83a091..0000000 --- a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorAssociated/ThreatActorAssociatedList.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { - ChevronLeft, - ChevronRight, - FileText, - Settings, -} from "lucide-react"; -import type { AssociatedItem } from "../../../../types/ThreatActorTypes"; -import "./ThreatActorAssociatedList.css"; -import FeedbackCard from "../../../feedback/FeedbackCard"; -import { Link } from "react-router-dom"; - -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"} -

-
-
- {/* Vis empty state dersom ingen tilknyttede entiteter finnes */} - {totalCount === 0 && ( - - )} - - {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/ThreatActorDetailedHeader/ThreatActorDetailedHeader.tsx b/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader.tsx deleted file mode 100644 index e97ca98..0000000 --- a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Star } from "lucide-react"; -import { Link } from "react-router-dom"; -import "./ThreatActorDetailedHeader.css"; -import BackButton from "../../../navigation/BackButton"; - -type ThreatActorDetailsHeaderProps = { - name: string; - mitreId: string; - aliases: string[]; - targetSectors: string[]; -}; - -function ThreatActorDetailedHeader({ - name, - mitreId, - aliases, - targetSectors - }: ThreatActorDetailsHeaderProps){ - return ( -
- - -
-

{name}

- - {mitreId} - -
- -
- Also known as: - - {aliases.map((alias) => ( - {alias} - ))} -
- -
- Target Sectors: - - {targetSectors.map((sector) => ( - {sector} - ))} -
-
- - ); -} - -export default ThreatActorDetailedHeader; \ 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 deleted file mode 100644 index ef08cdc..0000000 --- a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorCard/ThreatActorCard.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { Flag, Target, Users } from "lucide-react"; -import { Link } from "react-router-dom"; -import ReactCountryFlag from "react-country-flag"; -import "./ThreatActorCard.css"; - -type ThreatActorCardProps = { - name: string; - mitre_id: string; - alias: string; - country: string; - country_code?: string; - motivation: string; - status: string; -} - -function ThreatActorCard ({ - name, - mitre_id, - alias, - country, - country_code, - motivation, - status -}: ThreatActorCardProps){ - return ( -
-
- - -
-

{name}

-

Also known as {alias}

-
- - {mitre_id} -
- -
-
- - {status} -
- -
-
- {country_code ? ( - - ) : ( - - )} - - {country} -
- -
- - {motivation} -
-
-
- - - Open profile → - -
- ); -} -export default ThreatActorCard; \ No newline at end of file diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorPageHeader/ThreatActorPageHeader.css b/frontend/src/components/threatactor/ThreatActorMain/ThreatActorPageHeader/ThreatActorPageHeader.css deleted file mode 100644 index 09ac9ab..0000000 --- a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorPageHeader/ThreatActorPageHeader.css +++ /dev/null @@ -1,21 +0,0 @@ -.threat-actor-page-header { - display: flex; - flex-direction: column; - gap: 6px; - - margin-bottom: 4px; -} - -.threat-actor-page-header h1 { - font-size: clamp(28px, 3vw, 38px); - line-height: 1.1; -} - -.page-description { - max-width: 720px; - - color: var(--text-muted); - - font-size: 16px; - line-height: 1.5; -} \ No newline at end of file diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorPageHeader/ThreatActorPageHeader.tsx b/frontend/src/components/threatactor/ThreatActorMain/ThreatActorPageHeader/ThreatActorPageHeader.tsx deleted file mode 100644 index 8ebe5e7..0000000 --- a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorPageHeader/ThreatActorPageHeader.tsx +++ /dev/null @@ -1,15 +0,0 @@ - - -function ThreatActorPageHeader(){ - return ( -
-

Threat Actors

- -

- Browse documented threat groups and search by name, alias, MITRE ID, or keyword. -

-
- ); -} - -export default ThreatActorPageHeader; \ No newline at end of file diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorSearch/ThreatActorSearch.tsx b/frontend/src/components/threatactor/ThreatActorMain/ThreatActorSearch/ThreatActorSearch.tsx deleted file mode 100644 index a9a05c5..0000000 --- a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorSearch/ThreatActorSearch.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Search } from "lucide-react"; -import "./ThreatActorSearch.css"; -import {type SubmitEvent } from "react"; - -type ThreatActorSearchProps = { - value: string; - onChange: (value: string) => void; - onSearch: (searchWord: string) => void; -}; - -function ThreatActorSearch({ value, onChange, onSearch}: ThreatActorSearchProps) { - - const handleSearch = (event: SubmitEvent) => { - event.preventDefault(); - onSearch(value.trim()); - }; - - return ( -
- - - onChange(event.target.value)} - /> - - - - ); -} - -export default ThreatActorSearch \ No newline at end of file diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorCard/ThreatActorCard.css b/frontend/src/components/threatactor/catalog/ThreatActorCard/ThreatActorCard.css similarity index 84% rename from frontend/src/components/threatactor/ThreatActorMain/ThreatActorCard/ThreatActorCard.css rename to frontend/src/components/threatactor/catalog/ThreatActorCard/ThreatActorCard.css index 2f0296c..255771d 100644 --- a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorCard/ThreatActorCard.css +++ b/frontend/src/components/threatactor/catalog/ThreatActorCard/ThreatActorCard.css @@ -26,7 +26,6 @@ transform: translateY(-2px); } - .threat-actor-card-header { display: flex; align-items: flex-start; @@ -156,43 +155,43 @@ /* ACTIVE – grønn */ .threat-actor-status.active { - background-color: rgba(0, 200, 120, 0.15); - color: #00d89c; + background-color: rgba(0, 200, 120, 0.15); + color: #00d89c; } .threat-actor-status.active .status-dot { - background-color: #00d89c; + background-color: #00d89c; } /* INACTIVE – rød */ .threat-actor-status.inactive { - background-color: rgba(239, 68, 68, 0.15); - color: #f87171; + background-color: rgba(239, 68, 68, 0.15); + color: #f87171; } .threat-actor-status.inactive .status-dot { - background-color: #f87171; + background-color: #f87171; } /* UNKNOWN – grå */ .threat-actor-status.unknown { - background-color: rgba(148, 163, 184, 0.15); - color: #94a3b8; + background-color: rgba(148, 163, 184, 0.15); + color: #94a3b8; } .threat-actor-status.unknown .status-dot { - background-color: #94a3b8; + background-color: #94a3b8; } .threat-actor-details { - margin-top: auto; + margin-top: auto; - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 10px; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 10px; } .open-profile { - margin-top: 0; -} \ No newline at end of file + margin-top: 0; +} diff --git a/frontend/src/components/threatactor/catalog/ThreatActorCard/ThreatActorCard.tsx b/frontend/src/components/threatactor/catalog/ThreatActorCard/ThreatActorCard.tsx new file mode 100644 index 0000000..5e8f08f --- /dev/null +++ b/frontend/src/components/threatactor/catalog/ThreatActorCard/ThreatActorCard.tsx @@ -0,0 +1,81 @@ +import { Flag, Target, Users } from "lucide-react"; +import { Link } from "react-router-dom"; +import ReactCountryFlag from "react-country-flag"; +import "./ThreatActorCard.css"; + +type ThreatActorCardProps = { + name: string; + mitre_id: string; + alias: string; + country: string; + country_code?: string; + motivation: string; + status: string; +}; + +function ThreatActorCard({ + name, + mitre_id, + alias, + country, + country_code, + motivation, + status, +}: ThreatActorCardProps) { + return ( +
+
+ + +
+

{name}

+

Also known as {alias}

+
+ + {mitre_id} +
+ +
+
+ + {status} +
+ +
+
+ {country_code ? ( + + ) : ( + + )} + + {country} +
+ +
+ + {motivation} +
+
+
+ + + Open profile → + +
+ ); +} +export default ThreatActorCard; diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorActiveFilters.css b/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorActiveFilters.css similarity index 95% rename from frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorActiveFilters.css rename to frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorActiveFilters.css index 42c33e9..19ca2b3 100644 --- a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorActiveFilters.css +++ b/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorActiveFilters.css @@ -1,4 +1,3 @@ - /* Rad med aktive søk, filtre og sortering */ .active-threat-actor-filters { display: flex; @@ -50,17 +49,12 @@ background: transparent; color: var(--text-muted); border: none; - color: #b8cccc; -font-size: 13px; -font-weight: 500; + color: #b8cccc; + font-size: 13px; + font-weight: 500; font-size: 12px; cursor: pointer; - - - - - } .active-filter-clear:hover { diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorActiveFilters.tsx b/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorActiveFilters.tsx similarity index 72% rename from frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorActiveFilters.tsx rename to frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorActiveFilters.tsx index b99d524..4669c2b 100644 --- a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorActiveFilters.tsx +++ b/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorActiveFilters.tsx @@ -1,8 +1,6 @@ - import { X } from "lucide-react"; -import type { ThreatActorFilterValues } from "../ThreatActorFilter/ThreatActorFilter"; import "./ThreatActorActiveFilters.css"; - +import type { ThreatActorFilterValues } from "../../../../types/ThreatActorTypes"; // Alle filterfelter og navnene som skal vises const filterFields = [ @@ -31,18 +29,27 @@ type ActiveFiltersProps = { onClearAll: () => void; }; -export default function ThreatActorActiveFilters({search, filters, onRemoveSearch, onRemoveFilter, onClearAll}: ActiveFiltersProps) { - +export default function ThreatActorActiveFilters({ + search, + filters, + onRemoveSearch, + onRemoveFilter, + onClearAll, +}: ActiveFiltersProps) { // Finn filtrene som faktisk er aktive - const activeFilters = filterFields.filter(({ key }) => key === "sortBy" ? filters.sortBy !== "name-asc" : filters[key] !== ""); + const activeFilters = filterFields.filter(({ key }) => + key === "sortBy" ? filters.sortBy !== "name-asc" : filters[key] !== "", + ); // Ikke vis raden hvis ingen kriterier er aktive if (!search && activeFilters.length === 0) return null; return ( -
+
- {/* Search-chip */} {search && ( ))}
-
diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilter.css b/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilter.css similarity index 99% rename from frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilter.css rename to frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilter.css index 1dc2d99..fa8d61a 100644 --- a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilter.css +++ b/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilter.css @@ -1,4 +1,3 @@ - .threat-actor-filters { display: flex; flex-direction: column; @@ -112,4 +111,4 @@ .reset-filters-button:focus-visible { outline: 2px solid var(--text-heading); outline-offset: 3px; -} \ No newline at end of file +} diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilter.tsx b/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilter.tsx similarity index 62% rename from frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilter.tsx rename to frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilter.tsx index 1807a5d..0725724 100644 --- a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilter.tsx +++ b/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilter.tsx @@ -12,7 +12,11 @@ import { useState } from "react"; import FilterDropdown from "./ThreatActorFilterDropdown"; import type { FilterOption } from "./ThreatActorFilterDropdown"; import "./ThreatActorFilter.css"; -import type { ThreatActorFilterOptionsData } from "../../../../types/ThreatActorTypes"; +import { + DEFAULT_FILTERS, + type ThreatActorFilterOptionsData, + type ThreatActorFilterValues, +} from "../../../../types/ThreatActorTypes"; const sortOptions: FilterOption[] = [ { value: "name-asc", label: "Name (A - Z)" }, @@ -21,65 +25,50 @@ const sortOptions: FilterOption[] = [ { value: "mitre-desc", label: "MITRE ID (Descending)" }, ]; -export type ThreatActorFilterValues = { - country: string; - region: string; - motivation: string; - status: string; - dangerLevel: string; - sector: string; - sortBy: string; -}; - -export const DEFAULT_FILTERS: ThreatActorFilterValues = { - country: "", - region: "", - motivation: "", - sector: "", - status: "", - dangerLevel: "", - sortBy: "name-asc", -}; - type ThreatActorFiltersProps = { - filterOptions?: ThreatActorFilterOptionsData["threatActorFilterOptions"]; - value: ThreatActorFilterValues; - onChange: (filters: ThreatActorFilterValues) => void; - onApply: (filters: ThreatActorFilterValues) => void; + filterOptions?: ThreatActorFilterOptionsData["threatActorFilterOptions"]; + value: ThreatActorFilterValues; + onChange: (filters: ThreatActorFilterValues) => void; + onApply: (filters: ThreatActorFilterValues) => void; }; - - function ThreatActorFilters({ - value, - onChange, - onApply, - filterOptions + value, + onChange, + onApply, + filterOptions, }: ThreatActorFiltersProps) { + const [openFilter, setOpenFilter] = useState(null); - const [openFilter, setOpenFilter] = useState(null); + const { country, region, motivation, sector, status, dangerLevel, sortBy } = + value; - const {country,region, motivation, sector, status, dangerLevel, sortBy} = value; + // Oppdaterer ett bestemt filter + const updateFilter = ( + key: keyof ThreatActorFilterValues, + newValue: string, + ) => { + onChange({ ...value, [key]: newValue }); + }; - // Oppdaterer ett bestemt filter - const updateFilter = (key: keyof ThreatActorFilterValues, newValue: string) => {onChange({...value, [key]: newValue})}; + const toggleFilter = (filter: string) => { + setOpenFilter((previous) => (previous === filter ? null : filter)); + }; - const toggleFilter = (filter: string) => {setOpenFilter((previous) => previous === filter ? null : filter)}; + const handleApplyFilters = () => { + setOpenFilter(null); + onApply(value); + }; - const handleApplyFilters = () => { - setOpenFilter(null); - onApply(value); - }; + const handleResetFilters = () => { + setOpenFilter(null); - const handleResetFilters = () => { - setOpenFilter(null); + const resetFilters = { ...DEFAULT_FILTERS }; - const resetFilters = { ...DEFAULT_FILTERS }; + onChange(resetFilters); + onApply(resetFilters); + }; - onChange(resetFilters); - onApply(resetFilters); - }; - return (

Filters

@@ -163,28 +152,28 @@ function ThreatActorFilters({ showAllOption={false} /> -
- - - -
+
+ + + +
); } -export default ThreatActorFilters; \ No newline at end of file +export default ThreatActorFilters; diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilterDropdown.css b/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilterDropdown.css similarity index 99% rename from frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilterDropdown.css rename to frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilterDropdown.css index a11c218..21f4a9e 100644 --- a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilterDropdown.css +++ b/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilterDropdown.css @@ -1,4 +1,3 @@ - .filter-dropdown { position: relative; min-width: 0; diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilterDropdown.tsx b/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilterDropdown.tsx similarity index 79% rename from frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilterDropdown.tsx rename to frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilterDropdown.tsx index 884840f..08b7ec3 100644 --- a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilterDropdown.tsx +++ b/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilterDropdown.tsx @@ -19,19 +19,30 @@ type FilterDropdownProps = { showAllOption?: boolean; }; -export default function FilterDropdown({label, placeholder,icon,options,value,onChange,isOpen,onToggle,showAllOption = true}: FilterDropdownProps) { +export default function FilterDropdown({ + label, + placeholder, + icon, + options, + value, + onChange, + isOpen, + onToggle, + showAllOption = true, +}: FilterDropdownProps) { // Sorterer filteralternativer etter antall treff. // Alternativer uten count (f.eks. Sort by) beholder sin opprinnelige rekkefølge. - const sortedOptions = [...options].sort((a, b) => { + const sortedOptions = [...options].sort((a, b) => { if (a.count === undefined || b.count === undefined) { - return 0; + return 0; } return b.count - a.count || a.label.localeCompare(b.label); - }); + }); // Viser valgt alternativ i dropdown-knappen. - const selectedLabel =options.find((option) => option.value === value)?.label ?? placeholder; + const selectedLabel = + options.find((option) => option.value === value)?.label ?? placeholder; return (
); -} \ No newline at end of file +} diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorGrid/ThreatActorGrid.css b/frontend/src/components/threatactor/catalog/ThreatActorGrid/ThreatActorGrid.css similarity index 93% rename from frontend/src/components/threatactor/ThreatActorMain/ThreatActorGrid/ThreatActorGrid.css rename to frontend/src/components/threatactor/catalog/ThreatActorGrid/ThreatActorGrid.css index 86d55b8..9db71e3 100644 --- a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorGrid/ThreatActorGrid.css +++ b/frontend/src/components/threatactor/catalog/ThreatActorGrid/ThreatActorGrid.css @@ -6,8 +6,7 @@ display: grid; - grid-template-columns: - repeat(auto-fit, minmax(300px, 340px)); + grid-template-columns: repeat(auto-fit, minmax(300px, 340px)); justify-content: center; @@ -62,4 +61,4 @@ .threat-actor-pagination button:disabled { opacity: 0.4; cursor: not-allowed; -} \ No newline at end of file +} diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorGrid/ThreatActorGrid.tsx b/frontend/src/components/threatactor/catalog/ThreatActorGrid/ThreatActorGrid.tsx similarity index 66% rename from frontend/src/components/threatactor/ThreatActorMain/ThreatActorGrid/ThreatActorGrid.tsx rename to frontend/src/components/threatactor/catalog/ThreatActorGrid/ThreatActorGrid.tsx index c30bf87..1bd9d7d 100644 --- a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorGrid/ThreatActorGrid.tsx +++ b/frontend/src/components/threatactor/catalog/ThreatActorGrid/ThreatActorGrid.tsx @@ -3,7 +3,6 @@ import type { ThreatActorGridItem } from "../../../../types/ThreatActorTypes"; import "./ThreatActorGrid.css"; import { getCountryCode } from "../../../../utils/getCountryCode"; - type ThreatActorGridProps = { actors: ThreatActorGridItem[]; page: number; @@ -11,8 +10,13 @@ type ThreatActorGridProps = { actorsCount: number; onPageChange: (page: number) => void; }; -function ThreatActorGrid({actors, page, pageSize, actorsCount, onPageChange }: ThreatActorGridProps) { - +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); @@ -20,7 +24,7 @@ function ThreatActorGrid({actors, page, pageSize, actorsCount, onPageChange }: T <>
{actors.map((actor) => ( -
- - - {actorsCount === 0 ? 0 : (page - 1) * pageSize + 1}–{Math.min(page * pageSize, actorsCount)} + + {actorsCount === 0 ? 0 : (page - 1) * pageSize + 1}– + {Math.min(page * pageSize, actorsCount)} + -
@@ -49,4 +63,4 @@ function ThreatActorGrid({actors, page, pageSize, actorsCount, onPageChange }: T ); } -export default ThreatActorGrid; \ No newline at end of file +export default ThreatActorGrid; diff --git a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorAssociated/ThreatActorAssociatedList.css b/frontend/src/components/threatactor/detailed/ThreatActorAssociated/ThreatActorAssociatedList.css similarity index 95% rename from frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorAssociated/ThreatActorAssociatedList.css rename to frontend/src/components/threatactor/detailed/ThreatActorAssociated/ThreatActorAssociatedList.css index 97941b5..bb47ab0 100644 --- a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorAssociated/ThreatActorAssociatedList.css +++ b/frontend/src/components/threatactor/detailed/ThreatActorAssociated/ThreatActorAssociatedList.css @@ -1,4 +1,3 @@ - /* ===== Associated panel ===== */ .associated-panel { @@ -16,7 +15,6 @@ border-radius: 8px; } - /* ===== Panel header ===== */ .associated-panel-header { @@ -52,7 +50,6 @@ font-weight: 700; } - /* ===== Associated list ===== */ .associated-list { @@ -61,7 +58,6 @@ gap: 6px; } - /* ===== Associated row ===== */ .associated-row { @@ -80,7 +76,6 @@ border-radius: 7px; } - /* ===== Shared icon ===== */ .associated-row-icon { @@ -101,7 +96,6 @@ height: 18px; } - /* ===== MITRE ID ===== */ .associated-item-id { @@ -117,7 +111,6 @@ white-space: nowrap; } - /* ===== Item name ===== */ .associated-item-name { @@ -132,7 +125,6 @@ overflow-wrap: anywhere; } - /* ===== Row arrow ===== */ .associated-row-arrow { @@ -145,7 +137,6 @@ color: var(--text); } - /* ===== Techniques palette ===== */ .associated-row-technique .associated-row-icon { @@ -158,7 +149,6 @@ color: var(--accent); } - /* ===== Software palette ===== */ .associated-row-software .associated-row-icon { @@ -171,7 +161,6 @@ color: #ff6b6b; } - /* ===== Pagination ===== */ .associated-pagination { @@ -224,7 +213,6 @@ outline-offset: 2px; } - /* ===== Mobile ===== */ @media (max-width: 520px) { @@ -277,16 +265,16 @@ } .associated-row, .associated-row:visited { - text-decoration: none; - color: inherit; + text-decoration: none; + color: inherit; } .associated-row:hover { - border-color: var(--accent); - cursor: pointer; + border-color: var(--accent); + cursor: pointer; } .associated-row:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 2px; -} \ No newline at end of file + outline: 2px solid var(--accent); + outline-offset: 2px; +} diff --git a/frontend/src/components/threatactor/detailed/ThreatActorAssociated/ThreatActorAssociatedList.tsx b/frontend/src/components/threatactor/detailed/ThreatActorAssociated/ThreatActorAssociatedList.tsx new file mode 100644 index 0000000..a943abd --- /dev/null +++ b/frontend/src/components/threatactor/detailed/ThreatActorAssociated/ThreatActorAssociatedList.tsx @@ -0,0 +1,100 @@ +import { ChevronLeft, ChevronRight, FileText, Settings } from "lucide-react"; +import type { AssociatedItem } from "../../../../types/ThreatActorTypes"; +import "./ThreatActorAssociatedList.css"; +import FeedbackCard from "../../../common/FeedBack/FeedbackCard"; +import { Link } from "react-router-dom"; + +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"} +

+
+
+ {/* Vis empty state dersom ingen tilknyttede entiteter finnes */} + {totalCount === 0 && ( + + )} + + {items.map((item) => ( + + + {isTechnique ? : } + + + {item.mitreId} + {item.name} + + + + ))} + + {totalPages > 1 && ( +
+ + + + Page {page + 1} of {totalPages} + + + +
+ )} +
+ ); +} + +export default ThreatActorAssociatedList; diff --git a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedDescription/ThreatActorDetailedDescription.css b/frontend/src/components/threatactor/detailed/ThreatActorDetailedDescription/ThreatActorDetailedDescription.css similarity index 99% rename from frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedDescription/ThreatActorDetailedDescription.css rename to frontend/src/components/threatactor/detailed/ThreatActorDetailedDescription/ThreatActorDetailedDescription.css index 7663612..adeb9cf 100644 --- a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedDescription/ThreatActorDetailedDescription.css +++ b/frontend/src/components/threatactor/detailed/ThreatActorDetailedDescription/ThreatActorDetailedDescription.css @@ -21,4 +21,4 @@ font-size: 14px; line-height: 1.6; -} \ No newline at end of file +} diff --git a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedDescription/ThreatActorDetailedDescription.tsx b/frontend/src/components/threatactor/detailed/ThreatActorDetailedDescription/ThreatActorDetailedDescription.tsx similarity index 88% rename from frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedDescription/ThreatActorDetailedDescription.tsx rename to frontend/src/components/threatactor/detailed/ThreatActorDetailedDescription/ThreatActorDetailedDescription.tsx index 34486d9..d4264b2 100644 --- a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedDescription/ThreatActorDetailedDescription.tsx +++ b/frontend/src/components/threatactor/detailed/ThreatActorDetailedDescription/ThreatActorDetailedDescription.tsx @@ -15,4 +15,4 @@ function ThreatActorDetailedDescription({ ); } -export default ThreatActorDetailedDescription; \ No newline at end of file +export default ThreatActorDetailedDescription; diff --git a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader.css b/frontend/src/components/threatactor/detailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader.css similarity index 99% rename from frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader.css rename to frontend/src/components/threatactor/detailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader.css index 5831469..0180542 100644 --- a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader.css +++ b/frontend/src/components/threatactor/detailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader.css @@ -6,7 +6,6 @@ gap: 14px; } - /* ===== Back link ===== */ .back-button { @@ -23,7 +22,6 @@ text-decoration: underline; } - /* ===== Actor title ===== */ .detailed-title-row { @@ -53,7 +51,6 @@ font-weight: 700; } - /* ===== Favorite button ===== */ .favorite-button { @@ -82,7 +79,6 @@ color: var(--accent); } - /* ===== Aliases ===== */ .aliases { @@ -111,4 +107,4 @@ color: var(--text); font-size: 13px; -} \ No newline at end of file +} diff --git a/frontend/src/components/threatactor/detailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader.tsx b/frontend/src/components/threatactor/detailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader.tsx new file mode 100644 index 0000000..918fb2c --- /dev/null +++ b/frontend/src/components/threatactor/detailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader.tsx @@ -0,0 +1,54 @@ +import { Star } from "lucide-react"; +import "./ThreatActorDetailedHeader.css"; +import BackButton from "../../../common/Navigation/BackButton"; + +type ThreatActorDetailsHeaderProps = { + name: string; + mitreId: string; + aliases: string[]; + targetSectors: string[]; +}; + +function ThreatActorDetailedHeader({ + name, + mitreId, + aliases, + targetSectors, +}: ThreatActorDetailsHeaderProps) { + return ( +
+ + +
+

{name}

+ + {mitreId} + +
+ +
+ Also known as: + + {aliases.map((alias) => ( + {alias} + ))} +
+ +
+ Target Sectors: + + {targetSectors.map((sector) => ( + {sector} + ))} +
+
+ ); +} + +export default ThreatActorDetailedHeader; diff --git a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCard.css b/frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCard.css similarity index 99% rename from frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCard.css rename to frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCard.css index 91668c6..af17230 100644 --- a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCard.css +++ b/frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCard.css @@ -15,7 +15,6 @@ border-radius: 8px; } - /* ===== Icon ===== */ .detailed-info-card-icon { @@ -33,7 +32,6 @@ height: 28px; } - /* ===== Text content ===== */ .detailed-info-card-content { @@ -57,4 +55,4 @@ font-size: 14px; font-weight: 500; line-height: 1.3; -} \ No newline at end of file +} diff --git a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCard.tsx b/frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCard.tsx similarity index 64% rename from frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCard.tsx rename to frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCard.tsx index 53cf7eb..b5d0eff 100644 --- a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCard.tsx +++ b/frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCard.tsx @@ -3,11 +3,11 @@ import "./ThreatActorDetailedInfoCard.css"; /** * Tar inn en ReactNode icons som - * + * * Tar inn et navn på boksen - * - * Tar inn verdien fra DB som vises - * + * + * Tar inn verdien fra DB som vises + * * Tar inn en type som brukes for spesifikk styling (optional) */ type ThreatActorDetailedInfoCardProps = { @@ -17,7 +17,6 @@ type ThreatActorDetailedInfoCardProps = { variant?: string; }; - function ThreatActorDetailedInfoCard({ icon, title, @@ -26,21 +25,15 @@ function ThreatActorDetailedInfoCard({ }: ThreatActorDetailedInfoCardProps) { return (
-
- {icon} -
+
{icon}
- - {title} - + {title} -
- {value} -
+
{value}
); } -export default ThreatActorDetailedInfoCard; \ No newline at end of file +export default ThreatActorDetailedInfoCard; diff --git a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.css b/frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.css similarity index 99% rename from frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.css rename to frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.css index b9f1569..47edac7 100644 --- a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.css +++ b/frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.css @@ -6,7 +6,6 @@ gap: 12px; } - /* ===== Country ===== */ .detailed-info-country { @@ -20,8 +19,6 @@ height: 16px !important; } - - /* ===== Status ===== */ .status-info-card.status-active { @@ -41,7 +38,6 @@ color: var(--status-color); } - /* ===== Danger Level ===== */ /* Farger basert på risikonivå */ @@ -82,7 +78,6 @@ font-weight: 700; } - /* ===== Smaller desktop ===== */ @media (max-width: 1200px) { @@ -91,7 +86,6 @@ } } - /* ===== Tablet ===== */ @media (max-width: 900px) { @@ -104,7 +98,6 @@ } } - /* ===== Mobile ===== */ @media (max-width: 520px) { @@ -116,4 +109,4 @@ .target-sectors-card { grid-column: auto; } -} \ No newline at end of file +} diff --git a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.tsx b/frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.tsx similarity index 79% rename from frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.tsx rename to frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.tsx index 814f832..2950809 100644 --- a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.tsx +++ b/frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.tsx @@ -1,9 +1,8 @@ -import {AlertTriangle,Circle,Flag,MapPin} from "lucide-react"; +import { AlertTriangle, Circle, Flag, MapPin } from "lucide-react"; import ReactCountryFlag from "react-country-flag"; import ThreatActorDetailedInfoCard from "./ThreatActorDetailedInfoCard"; import "./ThreatActorDetailedInfoCards.css"; - type ThreatActorDetailedInfoCardsProps = { country: string; countryCode?: string; @@ -21,9 +20,15 @@ type ThreatActorDetailedInfoCardsProps = { *├── data: sectors *├── data: status *└── data: danger level - * + * */ -function ThreatActorDetailedInfoCards({country,countryCode,motivation,status,dangerLevel}: ThreatActorDetailedInfoCardsProps) { +function ThreatActorDetailedInfoCards({ + country, + countryCode, + motivation, + status, + dangerLevel, +}: ThreatActorDetailedInfoCardsProps) { return (
{countryCode ? ( - + ) : ( )} @@ -68,4 +69,4 @@ function ThreatActorDetailedInfoCards({country,countryCode,motivation,status,dan ); } -export default ThreatActorDetailedInfoCards; \ No newline at end of file +export default ThreatActorDetailedInfoCards; diff --git a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedOverview/ThreatActorOverview.css b/frontend/src/components/threatactor/detailed/ThreatActorDetailedOverview/ThreatActorOverview.css similarity index 99% rename from frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedOverview/ThreatActorOverview.css rename to frontend/src/components/threatactor/detailed/ThreatActorDetailedOverview/ThreatActorOverview.css index a87453e..fc91735 100644 --- a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedOverview/ThreatActorOverview.css +++ b/frontend/src/components/threatactor/detailed/ThreatActorDetailedOverview/ThreatActorOverview.css @@ -16,7 +16,6 @@ font-weight: 700; } - /* ===== Overview data ===== */ .overview-list { @@ -47,7 +46,6 @@ font-size: 14px; } - /* ===== Country ===== */ .overview-country { @@ -61,8 +59,6 @@ height: 16px !important; } - - /* ===== Status ===== */ .overview-status { @@ -81,11 +77,8 @@ color: #9ca3af; } - - /* ===== Danger Level ===== */ - .overview-row dd.overview-danger { width: fit-content; padding: 3px 10px; @@ -120,7 +113,6 @@ --danger-background: rgba(239, 68, 68, 0.18); } - /* ===== MITRE link ===== */ .mitre-link { @@ -143,7 +135,6 @@ text-decoration: underline; } - /* ===== Laptop: Overview is below the main content ===== */ @media (max-width: 1500px) and (min-width: 1101px) { @@ -153,7 +144,6 @@ } } - /* ===== Mobile ===== */ @media (max-width: 520px) { @@ -161,4 +151,4 @@ grid-template-columns: 1fr; gap: 4px; } -} \ No newline at end of file +} diff --git a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedOverview/ThreatActorOverview.tsx b/frontend/src/components/threatactor/detailed/ThreatActorDetailedOverview/ThreatActorOverview.tsx similarity index 86% rename from frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedOverview/ThreatActorOverview.tsx rename to frontend/src/components/threatactor/detailed/ThreatActorDetailedOverview/ThreatActorOverview.tsx index d94151d..902565f 100644 --- a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedOverview/ThreatActorOverview.tsx +++ b/frontend/src/components/threatactor/detailed/ThreatActorDetailedOverview/ThreatActorOverview.tsx @@ -25,7 +25,7 @@ function ThreatActorOverview({ dangerLevel, firstSeen, lastSeen, - xMitreDeprecated // Ny + xMitreDeprecated, // Ny }: ThreatActorOverviewProps) { return (
MITRE Deprecated
-
+
{xMitreDeprecated ? "Yes" : "No"}
Danger Level
-
{dangerLevel}
+
+ {dangerLevel} +
@@ -107,4 +111,4 @@ function ThreatActorOverview({ ); } -export default ThreatActorOverview; \ No newline at end of file +export default ThreatActorOverview; diff --git a/frontend/src/data/softwareData.ts b/frontend/src/data/softwareData.ts index def4431..c787578 100644 --- a/frontend/src/data/softwareData.ts +++ b/frontend/src/data/softwareData.ts @@ -5,7 +5,10 @@ export type { SoftwareEntry } from "./softwareMockData"; export const USE_SOFTWARE_MOCK_DATA = false; // ENDRE DENNE FOR Å IKKE BRUKE MOCK!!! -export type SoftwareCatalogEntry = Omit; +export type SoftwareCatalogEntry = Omit< + SoftwareEntry, + "associatedTechniques" | "usedByThreatActors" +>; export async function getSoftwareCatalog(): Promise { if (USE_SOFTWARE_MOCK_DATA) return mockSoftware; @@ -27,12 +30,17 @@ export async function getSoftwareCatalog(): Promise { return data.software; } -export async function getSoftwareByMitreId(mitreId: string): Promise { +export async function getSoftwareByMitreId( + mitreId: string, +): Promise { if (USE_SOFTWARE_MOCK_DATA) { return mockSoftware.find((entry) => entry.mitreId === mitreId) ?? null; } - const data = await graphqlRequest<{ softwareByMitreId: SoftwareEntry | null }>(` + const data = await graphqlRequest<{ + softwareByMitreId: SoftwareEntry | null; + }>( + ` query SoftwareDetail($mitreId: String!) { softwareByMitreId(mitreId: $mitreId) { id @@ -46,7 +54,9 @@ export async function getSoftwareByMitreId(mitreId: string): Promise = gql` } `; -export const GET_TACTIC_DETAILS: TypedDocumentNode = gql` +export const GET_TACTIC_DETAILS: TypedDocumentNode< + TacticDetailsData, + TacticDetailsVariables +> = gql` query GetTacticDetails($mitreId: String!, $limit: Int!, $offset: Int!) { getTacticByMitreId(mitreId: $mitreId) { mitreId @@ -64,4 +67,4 @@ export const GET_TACTIC_DETAILS: TypedDocumentNode = gql` +export const GET_THREAT_ACTOR_BY_MITRE_ID: TypedDocumentNode< + ThreatActorDetailedData, + ThreatActorDetailedVariables +> = gql` query GetThreatActorByMitreId($mitreId: String!) { getThreatActorByMitreId(mitreId: $mitreId) { threatId @@ -55,7 +66,10 @@ export const GET_THREAT_ACTOR_BY_MITRE_ID: TypedDocumentNode = gql` +export const GET_THREAT_ACTOR_TECHNIQUES: TypedDocumentNode< + ThreatActorTechniquesData, + ThreatActorAssociatedVariables +> = gql` query GetThreatActorTechniques( $mitreId: String! $limit: Int! @@ -75,17 +89,16 @@ export const GET_THREAT_ACTOR_TECHNIQUES: TypedDocumentNode = gql` +export const GET_THREAT_ACTOR_SOFTWARES: TypedDocumentNode< + ThreatActorSoftwaresData, + ThreatActorAssociatedVariables +> = gql` query GetThreatActorSoftwares( $mitreId: String! $limit: Int! $offset: Int! ) { - getThreatActorSoftwares( - mitreId: $mitreId - limit: $limit - offset: $offset - ) { + getThreatActorSoftwares(mitreId: $mitreId, limit: $limit, offset: $offset) { totalCount items { mitreId @@ -95,7 +108,7 @@ export const GET_THREAT_ACTOR_SOFTWARES: TypedDocumentNode = gql` +export const GET_THREAT_ACTOR_FILTER_OPTIONS: TypedDocumentNode = gql` query GetThreatActorFilterOptions { threatActorFilterOptions { countries { @@ -130,4 +143,4 @@ export const GET_THREAT_ACTOR_FILTER_OPTIONS: TypedDocumentNode("threat-actors:filters", { + ...DEFAULT_FILTERS, + }); + /** Filterkombinasjon som enda ikke har blitt sendt */ + const [draftFilters, setDraftFilters] = useState( + () => ({ ...activeFilters }), + ); + + /** + * Aktiverer valgte filtre. + */ + const handleApplyFilters = (filters: ThreatActorFilterValues) => { + setActiveFilters(filters); + setPage(1); + }; + + /** + * Aktiverer søkeordet. + */ + const handleSearch = (searchWord: string) => { + const normalizedSearch = searchWord.trim(); + + setSearchInput(normalizedSearch); + setSearch(normalizedSearch); + setPage(1); + }; + + /** + * Fjerner søkeordet fra både inputfeltet og GraphQL. + */ + const handleRemoveSearch = () => { + setSearchInput(""); + setSearch(""); + setPage(1); + }; + + /** + * Fjerner ett aktivt filter uten å påvirke de andre. + */ + const handleRemoveFilter = (key: keyof ThreatActorFilterValues) => { + setActiveFilters((previous) => ({ + ...previous, + [key]: DEFAULT_FILTERS[key], + })); + + setDraftFilters((previous) => ({ + ...previous, + [key]: DEFAULT_FILTERS[key], + })); + + setPage(1); + }; + + /** + * Nullstiller hele katalogtilstanden. + */ + const handleClearAll = () => { + setSearchInput(""); + setSearch(""); + + setDraftFilters({ ...DEFAULT_FILTERS }); + setActiveFilters({ ...DEFAULT_FILTERS }); + + setPage(1); + }; + return { + // State som brukes av komponentene + page, + setPage, + search, + searchInput, + setSearchInput, + activeFilters, + draftFilters, + setDraftFilters, + + // Event handlers + handleApplyFilters, + handleSearch, + handleRemoveSearch, + handleRemoveFilter, + handleClearAll, + }; +} diff --git a/frontend/src/hooks/threatactor/useThreatActorCatalogQueries.ts b/frontend/src/hooks/threatactor/useThreatActorCatalogQueries.ts new file mode 100644 index 0000000..73a1dbe --- /dev/null +++ b/frontend/src/hooks/threatactor/useThreatActorCatalogQueries.ts @@ -0,0 +1,71 @@ +import { useQuery } from "@apollo/client/react"; +import { + GET_ALL_THREAT_ACTOR_GRID, + GET_THREAT_ACTOR_FILTER_OPTIONS, +} from "../../graphql/threatActorQueries"; +import type { ThreatActorFilterValues } from "../../types/ThreatActorTypes"; + +type UseThreatActorQueriesProps = { + page: number; + pageSize: number; + search: string; + activeFilters: ThreatActorFilterValues; +}; + +/** + * Henter trusselaktører fra GraphQL basert på katalogens + * aktive søk, filtre, sortering og paginering. + * + * @param page - Gjeldende sidetall. + * @param pageSize - Antall trusselaktører per side. + * @param search - Aktivt søkeord. + * @param activeFilters - Aktive filtre og sorteringsvalg. + */ +export function useThreatActorQueries({ + page, + pageSize, + search, + activeFilters, +}: UseThreatActorQueriesProps) { + /** + * Skiller sortering fra filtrering ved hjelp av object destructuring. + * sortBy hentes ut separat, mens ...filters samler de resterende + * egenskapene i et nytt objekt. + * GraphQL forventer filters og sortBy som separate argumenter. + */ + const { sortBy, ...filters } = activeFilters; + + const { + data: filterData, + loading: filtersLoading, + error: filtersError, + } = useQuery(GET_THREAT_ACTOR_FILTER_OPTIONS, { + fetchPolicy: "cache-first", + }); + /** Selve query for threat-actors*/ + const { data, loading, error } = useQuery(GET_ALL_THREAT_ACTOR_GRID, { + variables: { + limit: pageSize, + offset: (page - 1) * pageSize, + search, + filters, + sortBy, + }, + }); + + /** Returnerer en tom liste dersom data ennå ikke er tilgjengelig. */ + const actors = data?.allThreatActorsGrid.items ?? []; + const actorsCount = data?.allThreatActorsGrid.totalCount ?? 0; + + const filterOptions = filterData?.threatActorFilterOptions; + + return { + actors, + actorsCount, + loading, + error, + filterOptions, + filtersLoading, + filtersError, + }; +} diff --git a/frontend/src/hooks/threatactor/useThreatActorDetail.ts b/frontend/src/hooks/threatactor/useThreatActorDetail.ts new file mode 100644 index 0000000..9361df3 --- /dev/null +++ b/frontend/src/hooks/threatactor/useThreatActorDetail.ts @@ -0,0 +1,19 @@ +import { useState } from "react"; + +/** + * Håndterer pagineringen på detaljsiden til en Threat Actor. + * + * Teknikker og programvarer har uavhengig paginering, + * slik at brukeren kan bla i én liste uten å påvirke den andre. + */ +export function useThreatActorDetail() { + const [techniquesPage, setTechniquesPage] = useState(0); + const [softwaresPage, setSoftwaresPage] = useState(0); + + return { + techniquesPage, + setTechniquesPage, + softwaresPage, + setSoftwaresPage, + }; +} diff --git a/frontend/src/hooks/threatactor/useThreatActorDetailQueries.ts b/frontend/src/hooks/threatactor/useThreatActorDetailQueries.ts new file mode 100644 index 0000000..a2ab806 --- /dev/null +++ b/frontend/src/hooks/threatactor/useThreatActorDetailQueries.ts @@ -0,0 +1,95 @@ +import { useQuery } from "@apollo/client/react"; +import { + GET_THREAT_ACTOR_BY_MITRE_ID, + GET_THREAT_ACTOR_TECHNIQUES, + GET_THREAT_ACTOR_SOFTWARES, +} from "../../graphql/threatActorQueries"; + +type UseThreatActorDetailQueriesProps = { + mitreId: string | undefined; + techniquesPage: number; + softwaresPage: number; + pageSize: number; +}; + +/** + * Henter detaljinformasjon om en Threat Actor, + * samt tilknyttede teknikker og programvarer. + * + * @param mitreId - Aktørens MITRE-ID fra URL-en. + * @param techniquesPage - Gjeldende side for teknikker (nullbasert). + * @param softwaresPage - Gjeldende side for programvarer (nullbasert). + * @param pageSize - Antall elementer per side. + */ +export function useThreatActorDetailQueries({ + mitreId, + techniquesPage, + softwaresPage, + pageSize, +}: UseThreatActorDetailQueriesProps) { + /** Henter aktørens detaljinformasjon. */ + const { + data: detailedData, + loading: detailedLoading, + error: detailedError, + } = useQuery(GET_THREAT_ACTOR_BY_MITRE_ID, { + variables: { mitreId: mitreId ?? "" }, + skip: !mitreId, + }); + + /** Henter tilknyttede teknikker. */ + const { + data: techniquesData, + loading: techniquesLoading, + error: techniquesError, + } = useQuery(GET_THREAT_ACTOR_TECHNIQUES, { + variables: { + mitreId: mitreId ?? "", + limit: pageSize, + offset: techniquesPage * pageSize, + }, + skip: !mitreId, + }); + + /** Henter tilknyttede programvarer. */ + const { + data: softwaresData, + loading: softwaresLoading, + error: softwaresError, + } = useQuery(GET_THREAT_ACTOR_SOFTWARES, { + variables: { + mitreId: mitreId ?? "", + limit: pageSize, + offset: softwaresPage * pageSize, + }, + skip: !mitreId, + }); + + /** Henter ut aktørens profil fra GraphQL-responsen. */ + const actor = detailedData?.getThreatActorByMitreId; + + /** Henter ut tilknyttede teknikker og antall treff. */ + const techniques = techniquesData?.getThreatActorTechniques.items ?? []; + const techniquesCount = + techniquesData?.getThreatActorTechniques.totalCount ?? 0; + + /** Henter ut tilknyttet programvare og antall treff. */ + const softwares = softwaresData?.getThreatActorSoftwares.items ?? []; + const softwaresCount = softwaresData?.getThreatActorSoftwares.totalCount ?? 0; + + return { + actor, + detailedLoading, + detailedError, + + techniques, + techniquesCount, + techniquesLoading, + techniquesError, + + softwares, + softwaresCount, + softwaresLoading, + softwaresError, + }; +} diff --git a/frontend/src/hooks/useSessionStorage.ts b/frontend/src/hooks/useSessionStorage.ts index f95a790..5be0df2 100644 --- a/frontend/src/hooks/useSessionStorage.ts +++ b/frontend/src/hooks/useSessionStorage.ts @@ -10,15 +10,15 @@ import { useState, useEffect } from "react"; */ function getSavedValue(key: string, initialValue: T): T { - try { - const saved = sessionStorage.getItem(key); + try { + const saved = sessionStorage.getItem(key); - if (saved === null) return initialValue; + if (saved === null) return initialValue; - return JSON.parse(saved) as T; - } catch { - return initialValue; - } + return JSON.parse(saved) as T; + } catch { + return initialValue; + } } /** @@ -31,15 +31,15 @@ function getSavedValue(key: string, initialValue: T): T { * @returns Et state-par [value, setValue], tilsvarende useState. */ export function useSessionStorage(key: string, initialValue: T) { - const [value, setValue] = useState(() => getSavedValue(key, initialValue)); + const [value, setValue] = useState(() => getSavedValue(key, initialValue)); - useEffect(() => { - try { - sessionStorage.setItem(key, JSON.stringify(value)); - } catch { - // Fortsett uten lagring. - } - }, [key, value]); + useEffect(() => { + try { + sessionStorage.setItem(key, JSON.stringify(value)); + } catch { + // Fortsett uten lagring. + } + }, [key, value]); - return [value, setValue] as const; -} \ No newline at end of file + return [value, setValue] as const; +} diff --git a/frontend/src/index.css b/frontend/src/index.css index cfeb55c..9430f48 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -69,4 +69,4 @@ input, select, textarea { font: inherit; -} \ No newline at end of file +} diff --git a/frontend/src/pages/Home/Home.css b/frontend/src/pages/Home/Home.css index 9cd05e1..8d9d1d9 100644 --- a/frontend/src/pages/Home/Home.css +++ b/frontend/src/pages/Home/Home.css @@ -3,7 +3,7 @@ width: 100%; max-width: none; margin: 0; - padding: 18px 0 32px; + padding: 0 0 32px; } /* Innvendig wrapper som holder alt innholdet i samme seksjon og sørger for at det følger riktig z-index og full bredde. */ @@ -124,10 +124,22 @@ } /* Fargevarianter for ikonene i kortene. Hver kategori får sin egen accentfarge for å skape visuell kategorisering. */ -.icon-wrap.cyan { background: rgba(0, 240, 230, 0.12); color: #52f2ea; } -.icon-wrap.purple { background: rgba(168, 85, 247, 0.12); color: #c084fc; } -.icon-wrap.green { background: rgba(34, 197, 94, 0.12); color: #4ade80; } -.icon-wrap.amber { background: rgba(251, 146, 60, 0.12); color: #fbbf24; } +.icon-wrap.cyan { + background: rgba(0, 240, 230, 0.12); + color: #52f2ea; +} +.icon-wrap.purple { + background: rgba(168, 85, 247, 0.12); + color: #c084fc; +} +.icon-wrap.green { + background: rgba(34, 197, 94, 0.12); + color: #4ade80; +} +.icon-wrap.amber { + background: rgba(251, 146, 60, 0.12); + color: #fbbf24; +} .browse-card svg { width: 30px; diff --git a/frontend/src/pages/Home/Home.tsx b/frontend/src/pages/Home/Home.tsx index 9a2ca3a..559e9a1 100644 --- a/frontend/src/pages/Home/Home.tsx +++ b/frontend/src/pages/Home/Home.tsx @@ -10,6 +10,7 @@ import { Users, } from "lucide-react"; import "./Home.css"; +import PageHeader from "../../components/common/PageHeader/PageHeader"; // Dette er oversikten over kategorier som vises på forsiden. // Hvert kort leder brukeren til en egen side for den aktuelle datakategorien. @@ -67,12 +68,11 @@ function Home() { return (
-
-

Threat intelligence

-

Threat Intelligence Dashboard

-

- Explore threat actors, techniques, software, and tactics to better understand cyber threats. -

+
+
+
-
+

@@ -151,7 +175,9 @@ function SavedItems() {
- + {item.type} @@ -167,13 +193,25 @@ function SavedItems() {
- - -
@@ -183,4 +221,4 @@ function SavedItems() { ); } -export default SavedItems; \ No newline at end of file +export default SavedItems; diff --git a/frontend/src/pages/Software/Software.css b/frontend/src/pages/Software/Software.css index 69da973..5862658 100644 --- a/frontend/src/pages/Software/Software.css +++ b/frontend/src/pages/Software/Software.css @@ -9,7 +9,6 @@ .software-heading { position: relative; - min-height: 102px; display: flex; align-items: center; overflow: hidden; @@ -320,14 +319,25 @@ padding: 13px 15px 12px; border: 1px solid var(--border); border-radius: 7px; - background: linear-gradient(145deg, rgba(0, 34, 36, 0.96), rgba(0, 20, 22, 0.94)); - transition: border-color 160ms ease, transform 160ms ease, background 160ms ease; + background: linear-gradient( + 145deg, + rgba(0, 34, 36, 0.96), + rgba(0, 20, 22, 0.94) + ); + transition: + border-color 160ms ease, + transform 160ms ease, + background 160ms ease; } .software-card:hover { transform: translateY(-2px); border-color: rgba(0, 240, 230, 0.55); - background: linear-gradient(145deg, rgba(3, 49, 51, 0.98), rgba(0, 24, 26, 0.96)); + background: linear-gradient( + 145deg, + rgba(3, 49, 51, 0.98), + rgba(0, 24, 26, 0.96) + ); } .software-card-icon { @@ -579,4 +589,4 @@ .software-card { transition: none; } -} \ No newline at end of file +} diff --git a/frontend/src/pages/Software/Software.tsx b/frontend/src/pages/Software/Software.tsx index 4679e60..bf8232e 100644 --- a/frontend/src/pages/Software/Software.tsx +++ b/frontend/src/pages/Software/Software.tsx @@ -1,285 +1,312 @@ import { useEffect, useMemo, useState } from "react"; import { Link } from "react-router-dom"; import { - ArrowDownUp, - ArrowLeft, - ArrowRight, - Bug, - Database, - Layers3, - Radio, - Search, - Shield, - TerminalSquare, + ArrowDownUp, + ArrowLeft, + ArrowRight, + Bug, + Database, + Layers3, + Radio, + Search, + Shield, + TerminalSquare, } from "lucide-react"; -import { getSoftwareCatalog, type SoftwareCatalogEntry } from "../../data/softwareData"; +import { + getSoftwareCatalog, + type SoftwareCatalogEntry, +} from "../../data/softwareData"; import "./Software.css"; +import PageHeader from "../../components/common/PageHeader/PageHeader"; const PAGE_SIZE = 12; function Software() { - const [softwareEntries, setSoftwareEntries] = useState([]); - const [loading, setLoading] = useState(true); - const [loadError, setLoadError] = useState(""); - const [search, setSearch] = useState(""); - const [type, setType] = useState("All types"); - const [platform, setPlatform] = useState("All platforms"); - const [sortOrder, setSortOrder] = useState("Name (A - Z)"); - const [currentPage, setCurrentPage] = useState(1); + const [softwareEntries, setSoftwareEntries] = useState< + SoftwareCatalogEntry[] + >([]); + const [loading, setLoading] = useState(true); + const [loadError, setLoadError] = useState(""); + const [search, setSearch] = useState(""); + const [type, setType] = useState("All types"); + const [platform, setPlatform] = useState("All platforms"); + const [sortOrder, setSortOrder] = useState("Name (A - Z)"); + const [currentPage, setCurrentPage] = useState(1); - useEffect(() => { - let cancelled = false; - getSoftwareCatalog() - .then((entries) => { - if (!cancelled) setSoftwareEntries(entries); - }) - .catch((error: unknown) => { - if (!cancelled) { - setLoadError(error instanceof Error ? error.message : "Unable to load software."); - } - }) - .finally(() => { - if (!cancelled) setLoading(false); - }); + useEffect(() => { + let cancelled = false; + getSoftwareCatalog() + .then((entries) => { + if (!cancelled) setSoftwareEntries(entries); + }) + .catch((error: unknown) => { + if (!cancelled) { + setLoadError( + error instanceof Error ? error.message : "Unable to load software.", + ); + } + }) + .finally(() => { + if (!cancelled) setLoading(false); + }); - return () => { - cancelled = true; - }; - }, []); + return () => { + cancelled = true; + }; + }, []); - const platformOptions = useMemo( - () => ["All platforms", ...new Set(softwareEntries.flatMap((entry) => entry.platforms).sort())], - [softwareEntries], - ); + const platformOptions = useMemo( + () => [ + "All platforms", + ...new Set(softwareEntries.flatMap((entry) => entry.platforms).sort()), + ], + [softwareEntries], + ); - const visibleEntries = useMemo(() => { - const query = search.trim().toLocaleLowerCase(); + const visibleEntries = useMemo(() => { + const query = search.trim().toLocaleLowerCase(); - return softwareEntries - .filter((entry) => { - const matchesQuery = [ - entry.name, - entry.id, - entry.mitreId, - entry.description, - ...entry.aliases, - ...entry.platforms, - ] - .join(" ") - .toLocaleLowerCase() - .includes(query); + return softwareEntries + .filter((entry) => { + const matchesQuery = [ + entry.name, + entry.id, + entry.mitreId, + entry.description, + ...entry.aliases, + ...entry.platforms, + ] + .join(" ") + .toLocaleLowerCase() + .includes(query); - return ( - matchesQuery && - (type === "All types" || entry.type === type.toUpperCase()) && - (platform === "All platforms" || entry.platforms.includes(platform)) - ); - }) - .sort((first, second) => { - const comparison = first.name.localeCompare(second.name); - return sortOrder === "Name (A - Z)" ? comparison : -comparison; - }); - }, [platform, search, softwareEntries, sortOrder, type]); + return ( + matchesQuery && + (type === "All types" || entry.type === type.toUpperCase()) && + (platform === "All platforms" || entry.platforms.includes(platform)) + ); + }) + .sort((first, second) => { + const comparison = first.name.localeCompare(second.name); + return sortOrder === "Name (A - Z)" ? comparison : -comparison; + }); + }, [platform, search, softwareEntries, sortOrder, type]); - const pageCount = Math.ceil(visibleEntries.length / PAGE_SIZE); - const pageEntries = visibleEntries.slice( - (currentPage - 1) * PAGE_SIZE, - currentPage * PAGE_SIZE, - ); - const firstVisibleEntry = visibleEntries.length === 0 ? 0 : (currentPage - 1) * PAGE_SIZE + 1; - const lastVisibleEntry = Math.min(currentPage * PAGE_SIZE, visibleEntries.length); + const pageCount = Math.ceil(visibleEntries.length / PAGE_SIZE); + const pageEntries = visibleEntries.slice( + (currentPage - 1) * PAGE_SIZE, + currentPage * PAGE_SIZE, + ); + const firstVisibleEntry = + visibleEntries.length === 0 ? 0 : (currentPage - 1) * PAGE_SIZE + 1; + const lastVisibleEntry = Math.min( + currentPage * PAGE_SIZE, + visibleEntries.length, + ); - return ( -
-
-
-

Threat intelligence

-

Software

-

- Browse documented malware and tools by name, alias, MITRE ID, or - keyword. -

-
- -
+ return ( +
+
+ -
event.preventDefault()} - > -
-
-
-

Filters

-
- +
event.preventDefault()} + > +
-
+
+
+

Filters

+
+ - -
+ +
+
+ + + -
-

- Showing {firstVisibleEntry}-{lastVisibleEntry} of {visibleEntries.length} software entries -

+
+

+ Showing{" "} + + {firstVisibleEntry}-{lastVisibleEntry} + {" "} + of {visibleEntries.length} software entries +

- {loading ? ( -
-
- ) : loadError ? ( -
-
- ) : visibleEntries.length > 0 ? ( -
- {pageEntries.map((entry, index) => { - const Icon = - entry.type === "TOOL" - ? index % 2 === 0 - ? TerminalSquare - : Radio - : Bug; + {loading ? ( +
+
+ ) : loadError ? ( +
+
+ ) : visibleEntries.length > 0 ? ( +
+ {pageEntries.map((entry, index) => { + const Icon = + entry.type === "TOOL" + ? index % 2 === 0 + ? TerminalSquare + : Radio + : Bug; - return ( -
-
-
-
- {entry.mitreId} - - {entry.type === "TOOL" ? "Tool" : "Malware"} - -
-

{entry.name}

-

{("summary" in entry && entry.summary) || entry.description}

-

- {entry.platforms.join(" · ")} -

- - Open profile
- ); - })} -
- ) : ( -
-
- )} + return ( +
+
+
+
+ {entry.mitreId} + + {entry.type === "TOOL" ? "Tool" : "Malware"} + +
+

{entry.name}

+

+ {("summary" in entry && entry.summary) || entry.description} +

+

+ {entry.platforms.join(" · ")} +

+ + Open profile
+ ); + })} +
+ ) : ( +
+
+ )} - {!loading && !loadError && pageCount > 1 && ( - - )} -
- - ); + {!loading && !loadError && pageCount > 1 && ( + + )} +
+ + ); } -export default Software; \ No newline at end of file +export default Software; diff --git a/frontend/src/pages/Software/SoftwareDetail.css b/frontend/src/pages/Software/SoftwareDetail.css index a561c64..67d952f 100644 --- a/frontend/src/pages/Software/SoftwareDetail.css +++ b/frontend/src/pages/Software/SoftwareDetail.css @@ -133,7 +133,11 @@ .software-overview { border: 1px solid var(--border); border-radius: 7px; - background: linear-gradient(145deg, rgba(0, 32, 34, 0.94), rgba(0, 19, 21, 0.94)); + background: linear-gradient( + 145deg, + rgba(0, 32, 34, 0.94), + rgba(0, 19, 21, 0.94) + ); } .software-detail-description { @@ -387,4 +391,4 @@ a.software-related-name:hover { grid-template-columns: 61px minmax(0, 1fr) 14px; gap: 7px; } -} \ No newline at end of file +} diff --git a/frontend/src/pages/Software/SoftwareDetail.tsx b/frontend/src/pages/Software/SoftwareDetail.tsx index 17552a4..3e7a515 100644 --- a/frontend/src/pages/Software/SoftwareDetail.tsx +++ b/frontend/src/pages/Software/SoftwareDetail.tsx @@ -1,10 +1,18 @@ import { useEffect, useState } from "react"; -import { ArrowLeft, ArrowRight, Bug, Database, Network, Radio, TerminalSquare } from "lucide-react"; +import { + ArrowLeft, + ArrowRight, + Bug, + Database, + Network, + Radio, + TerminalSquare, +} from "lucide-react"; import { Link, useParams } from "react-router-dom"; import { getSoftwareByMitreId } from "../../data/softwareData"; import type { SoftwareEntry } from "../../data/softwareData"; import "./SoftwareDetail.css"; -import BackButton from "../../components/navigation/BackButton"; +import BackButton from "../../components/common/Navigation/BackButton"; function cleanDescription(description: string): string { return description @@ -40,7 +48,10 @@ function SoftwareDetail() { mitreId, status: "error", software: null, - error: requestError instanceof Error ? requestError.message : "Unable to load software details.", + error: + requestError instanceof Error + ? requestError.message + : "Unable to load software details.", }); } }); @@ -50,8 +61,10 @@ function SoftwareDetail() { }; }, [mitreId]); - const loading = detailState.mitreId !== mitreId || detailState.status === "loading"; - const software = detailState.mitreId === mitreId ? detailState.software : null; + const loading = + detailState.mitreId !== mitreId || detailState.status === "loading"; + const software = + detailState.mitreId === mitreId ? detailState.software : null; const error = detailState.mitreId === mitreId ? detailState.error : ""; if (loading) { @@ -72,9 +85,14 @@ function SoftwareDetail() {
-
+

Description

-

{cleanDescription(software.description) || "No description is available."}

+

+ {cleanDescription(software.description) || + "No description is available."} +

-
+
- {software.associatedTechniques.length} + + {software.associatedTechniques.length} +
{software.associatedTechniques.length ? (
    {software.associatedTechniques .slice(0, showAllTechniques ? undefined : 5) .map((technique) => ( -
  • - {technique.mitreId} - {technique.name} -
  • - ))} +
  • + + {technique.mitreId} + + + {technique.name} + +
  • + ))}
) : ( -

No associated techniques are recorded.

+

+ No associated techniques are recorded. +

)} {software.associatedTechniques.length > 5 && ( )}
-
+
- {software.usedByThreatActors.length} + + {software.usedByThreatActors.length} +
{software.usedByThreatActors.length ? (
    {software.usedByThreatActors .slice(0, showAllActors ? undefined : 5) .map((actor) => ( -
  • - {actor.mitreId} - - {actor.name} - -
  • - ))} +
  • + + {actor.mitreId} + + + {actor.name} + +
  • + ))}
) : ( -

No threat actor relationships are recorded.

+

+ No threat actor relationships are recorded. +

)} {software.usedByThreatActors.length > 5 && ( )}
-
@@ -208,4 +280,4 @@ function SoftwareDetail() { ); } -export default SoftwareDetail; \ No newline at end of file +export default SoftwareDetail; diff --git a/frontend/src/pages/Tactics/TacticDetailed.css b/frontend/src/pages/Tactics/TacticDetailed.css index c39c1e5..55f8c58 100644 --- a/frontend/src/pages/Tactics/TacticDetailed.css +++ b/frontend/src/pages/Tactics/TacticDetailed.css @@ -25,4 +25,4 @@ .tactic-detailed-page { gap: 16px; } -} \ No newline at end of file +} diff --git a/frontend/src/pages/Tactics/TacticDetailed.tsx b/frontend/src/pages/Tactics/TacticDetailed.tsx index 2d94392..903a8cd 100644 --- a/frontend/src/pages/Tactics/TacticDetailed.tsx +++ b/frontend/src/pages/Tactics/TacticDetailed.tsx @@ -4,7 +4,7 @@ import TacticDetailedHeader from "../../components/tactic/TacticDetailed/TacticD import { useQuery } from "@apollo/client/react"; import { useParams } from "react-router-dom"; import { useState } from "react"; -import { GET_TACTIC_DETAILS } from "../../graphql/queries/tacticQueries"; +import { GET_TACTIC_DETAILS } from "../../graphql/tacticQueries"; import "./TacticDetailed.css"; @@ -41,10 +41,7 @@ function TacticDetailed() { return (
- + @@ -60,4 +57,4 @@ function TacticDetailed() { ); } -export default TacticDetailed; \ No newline at end of file +export default TacticDetailed; diff --git a/frontend/src/pages/Tactics/Tactics.css b/frontend/src/pages/Tactics/Tactics.css index f17f3d5..ba6aa22 100644 --- a/frontend/src/pages/Tactics/Tactics.css +++ b/frontend/src/pages/Tactics/Tactics.css @@ -15,4 +15,4 @@ .tactics-page { gap: 20px; } -} \ No newline at end of file +} diff --git a/frontend/src/pages/Tactics/Tactics.tsx b/frontend/src/pages/Tactics/Tactics.tsx index 482f3ef..9eebd3c 100644 --- a/frontend/src/pages/Tactics/Tactics.tsx +++ b/frontend/src/pages/Tactics/Tactics.tsx @@ -1,7 +1,7 @@ import TacticGrid from "../../components/tactic/TacticMain/TacticGrid/TacticGrid"; import TacticToolbar from "../../components/tactic/TacticMain/TacticToolbar/TacticToolbar"; import { useQuery } from "@apollo/client/react"; -import { GET_ALL_TACTICS } from "../../graphql/queries/tacticQueries"; +import { GET_ALL_TACTICS } from "../../graphql/tacticQueries"; import "./Tactics.css"; function Tactics() { @@ -17,4 +17,4 @@ function Tactics() { ); } -export default Tactics; \ No newline at end of file +export default Tactics; diff --git a/frontend/src/pages/Techniques/TechniqueDetailed.css b/frontend/src/pages/Techniques/TechniqueDetailed.css index 835732d..039d198 100644 --- a/frontend/src/pages/Techniques/TechniqueDetailed.css +++ b/frontend/src/pages/Techniques/TechniqueDetailed.css @@ -7,10 +7,10 @@ background: linear-gradient( to bottom, rgba(0, 0, 0, 0.45) 0%, - rgba(0, 0, 0, 0.20) 40%, - rgba(0, 0, 0, 0.10) 70%, -#001819 100% -); + rgba(0, 0, 0, 0.2) 40%, + rgba(0, 0, 0, 0.1) 70%, + #001819 100% + ); z-index: 0; } @@ -41,8 +41,8 @@ } .technique-detailed-top > * { -position: relative; -z-index: 1; + position: relative; + z-index: 1; } /* Tablet */ @@ -92,7 +92,6 @@ z-index: 1; margin-bottom: 24px; } - .technique-description { margin-top: -24px; width: 100%; diff --git a/frontend/src/pages/Techniques/TechniqueDetailed.tsx b/frontend/src/pages/Techniques/TechniqueDetailed.tsx index 2348718..9883b5b 100644 --- a/frontend/src/pages/Techniques/TechniqueDetailed.tsx +++ b/frontend/src/pages/Techniques/TechniqueDetailed.tsx @@ -15,7 +15,7 @@ import { GET_TECHNIQUE_SUBTECHNIQUES, GET_TECHNIQUE_SOFTWARE, GET_TECHNIQUE_THREAT_ACTORS, -} from "../../graphql/queries/techniqueQueries"; +} from "../../graphql/techniqueQueries"; interface Technique { mitreId: string; @@ -35,18 +35,15 @@ interface ThreatActorItem { } interface GetTechniqueSubTechniquesData { - getTechniqueSubTechniques: - AssociatedItem[]; + getTechniqueSubTechniques: AssociatedItem[]; } interface GetTechniqueSoftwareData { - getTechniqueSoftware: - AssociatedItem[]; + getTechniqueSoftware: AssociatedItem[]; } interface GetTechniqueThreatActorsData { - getTechniqueThreatActors: - ThreatActorItem[]; + getTechniqueThreatActors: ThreatActorItem[]; } interface GetTechniqueData { @@ -56,38 +53,28 @@ interface GetTechniqueData { function TechniqueDetailed() { const { mitreId } = useParams(); - const { - data, - loading, - error, - } = useQuery( + const { data, loading, error } = useQuery( GET_TECHNIQUE_BY_MITRE_ID, { variables: { mitreId }, }, ); - const { - data: subTechniquesData, - } = useQuery( + const { data: subTechniquesData } = useQuery( GET_TECHNIQUE_SUBTECHNIQUES, { variables: { mitreId }, }, ); - const { - data: softwareData, - } = useQuery( + const { data: softwareData } = useQuery( GET_TECHNIQUE_SOFTWARE, { variables: { mitreId }, }, ); - const { - data: threatActorData, - } = useQuery( + const { data: threatActorData } = useQuery( GET_TECHNIQUE_THREAT_ACTORS, { variables: { mitreId }, @@ -103,38 +90,27 @@ function TechniqueDetailed() { return

Error: {error.message}

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

Technique not found.

; } - const subTechniques = - subTechniquesData?.getTechniqueSubTechniques ?? - []; + const subTechniques = subTechniquesData?.getTechniqueSubTechniques ?? []; - const software = - softwareData?.getTechniqueSoftware ?? - []; + const software = softwareData?.getTechniqueSoftware ?? []; - const actors = - threatActorData?.getTechniqueThreatActors ?? - []; + const actors = threatActorData?.getTechniqueThreatActors ?? []; return (
-
@@ -143,10 +119,7 @@ function TechniqueDetailed() { @@ -154,38 +127,23 @@ function TechniqueDetailed() {
- +
+ - - - - +
- - - +
-
); } -export default TechniqueDetailed; \ No newline at end of file +export default TechniqueDetailed; diff --git a/frontend/src/pages/Techniques/Techniques.css b/frontend/src/pages/Techniques/Techniques.css index eb927ef..7f6c7b6 100644 --- a/frontend/src/pages/Techniques/Techniques.css +++ b/frontend/src/pages/Techniques/Techniques.css @@ -15,4 +15,4 @@ .techniques-page { gap: 20px; } -} \ No newline at end of file +} diff --git a/frontend/src/pages/Techniques/Techniques.tsx b/frontend/src/pages/Techniques/Techniques.tsx index e8ed173..714af59 100644 --- a/frontend/src/pages/Techniques/Techniques.tsx +++ b/frontend/src/pages/Techniques/Techniques.tsx @@ -2,9 +2,9 @@ import TechniqueGrid from "../../components/techniques/allTechniques/TechniqueGr import TechniqueToolbar from "../../components/techniques/allTechniques/TechniqueToolbar/TechniqueToolbar"; import "./Techniques.css"; -import { useState} from "react"; +import { useState } from "react"; import { useQuery } from "@apollo/client/react"; -import { GET_TECHNIQUES } from "../../graphql/queries/techniqueQueries"; +import { GET_TECHNIQUES } from "../../graphql/techniqueQueries"; interface Technique { mitreId: string; @@ -20,33 +20,23 @@ interface GetTechniquesData { }; } - function Techniques() { const pageSize = 12; const [page, setPage] = useState(1); - const { - data, - loading, - error, - } = useQuery( - GET_TECHNIQUES, - { - variables: { - limit: pageSize, - offset: (page - 1) * pageSize, - }, + const { data, loading, error } = useQuery(GET_TECHNIQUES, { + variables: { + limit: pageSize, + offset: (page - 1) * pageSize, }, - ); + }); if (loading) return

Loading...

; if (error) return

Error: {error.message}

; - const techniques = - data?.allTechniques.items ?? []; + const techniques = data?.allTechniques.items ?? []; - const techniquesCount = - data?.allTechniques.totalCount ?? 0; + const techniquesCount = data?.allTechniques.totalCount ?? 0; return (
@@ -63,5 +53,4 @@ function Techniques() { ); } - -export default Techniques; \ No newline at end of file +export default Techniques; diff --git a/frontend/src/pages/ThreatActors/ThreatActorDetailed.css b/frontend/src/pages/ThreatActor/ThreatActorDetailed.css similarity index 99% rename from frontend/src/pages/ThreatActors/ThreatActorDetailed.css rename to frontend/src/pages/ThreatActor/ThreatActorDetailed.css index 924ca37..5d7a80d 100644 --- a/frontend/src/pages/ThreatActors/ThreatActorDetailed.css +++ b/frontend/src/pages/ThreatActor/ThreatActorDetailed.css @@ -5,7 +5,6 @@ margin: 0 auto; } - /* ===== Desktop layout ===== */ .threat-actor-detailed-layout { @@ -17,14 +16,12 @@ align-items: start; } - /* Header spans both columns */ .threat-actor-detailed-header { grid-column: 1 / -1; } - /* ===== Main content ===== */ .threat-actor-detailed-main { @@ -36,8 +33,6 @@ gap: 20px; } - - /* ===== Associated Techniques & Software ===== */ .threat-actor-associated { @@ -54,7 +49,6 @@ min-width: 0; } - /* ===== Responsive Associated panels ===== */ @media (max-width: 1100px) { @@ -63,8 +57,6 @@ } } - - /* ===== Laptop / medium desktop ===== */ @media (max-width: 1500px) { @@ -82,7 +74,6 @@ } } - /* ===== Tablet ===== */ @media (max-width: 1100px) { @@ -91,9 +82,6 @@ } } - - - /* ===== Small tablet ===== */ @media (max-width: 768px) { @@ -103,7 +91,6 @@ } } - /* ===== Mobile ===== */ @media (max-width: 520px) { @@ -118,4 +105,4 @@ .threat-actor-associated { gap: 16px; } -} \ No newline at end of file +} diff --git a/frontend/src/pages/ThreatActor/ThreatActorDetailed.tsx b/frontend/src/pages/ThreatActor/ThreatActorDetailed.tsx new file mode 100644 index 0000000..6085836 --- /dev/null +++ b/frontend/src/pages/ThreatActor/ThreatActorDetailed.tsx @@ -0,0 +1,153 @@ +import ThreatActorDetailedHeader from "../../components/threatactor/detailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader"; +import ThreatActorOverview from "../../components/threatactor/detailed/ThreatActorDetailedOverview/ThreatActorOverview"; +import ThreatActorDetailedInfoCards from "../../components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards"; +import ThreatActorDetailedDescription from "../../components/threatactor/detailed/ThreatActorDetailedDescription/ThreatActorDetailedDescription"; +import ThreatActorAssociatedList from "../../components/threatactor/detailed/ThreatActorAssociated/ThreatActorAssociatedList"; +import { useThreatActorDetail } from "../../hooks/threatactor/useThreatActorDetail"; +import { useThreatActorDetailQueries } from "../../hooks/threatactor/useThreatActorDetailQueries"; +import FeedbackCard from "../../components/common/FeedBack/FeedbackCard"; +import { getCountryCode } from "../../utils/getCountryCode"; +import "./ThreatActorDetailed.css"; +import { useParams } from "react-router-dom"; + +const PAGE_SIZE = 5; + +function ThreatActorDetailed() { + //Henter inn mitre_id fra URL til siden som ble åpnet + const params = useParams(); + const mitreId = params.mitreId; + + const { techniquesPage, setTechniquesPage, softwaresPage, setSoftwaresPage } = + useThreatActorDetail(); + + const { + actor, + detailedLoading, + detailedError, + + techniques, + techniquesCount, + techniquesLoading, + techniquesError, + + softwares, + softwaresCount, + softwaresLoading, + softwaresError, + } = useThreatActorDetailQueries({ + mitreId, + techniquesPage, + softwaresPage, + pageSize: PAGE_SIZE, + }); + + if (!mitreId) { + return ; + } + + if (detailedLoading) { + return ; + } + + if (detailedError) { + return ( + + ); + } + + if (!actor) { + return ( + + ); + } + + return ( +
+
+ + +
+ + + +
+ + + +
+ {/* Associated Techniques */} + {techniquesLoading ? ( + + ) : techniquesError ? ( + + ) : ( + + )} + + {/* Associated Software */} + {softwaresLoading ? ( + + ) : softwaresError ? ( + + ) : ( + + )} +
+
+
+ ); +} + +export default ThreatActorDetailed; diff --git a/frontend/src/pages/ThreatActors/ThreatActors.css b/frontend/src/pages/ThreatActor/ThreatActors.css similarity index 99% rename from frontend/src/pages/ThreatActors/ThreatActors.css rename to frontend/src/pages/ThreatActor/ThreatActors.css index 2552fa4..9b4eac0 100644 --- a/frontend/src/pages/ThreatActors/ThreatActors.css +++ b/frontend/src/pages/ThreatActor/ThreatActors.css @@ -45,4 +45,4 @@ min-width: 0; flex: 1; } -} \ No newline at end of file +} diff --git a/frontend/src/pages/ThreatActor/ThreatActors.tsx b/frontend/src/pages/ThreatActor/ThreatActors.tsx new file mode 100644 index 0000000..e28f791 --- /dev/null +++ b/frontend/src/pages/ThreatActor/ThreatActors.tsx @@ -0,0 +1,119 @@ +import ThreatActorFilters from "../../components/threatactor/catalog/ThreatActorFilter/ThreatActorFilter"; +import ThreatActorGrid from "../../components/threatactor/catalog/ThreatActorGrid/ThreatActorGrid"; +import ThreatActorActiveFilters from "../../components/threatactor/catalog/ThreatActorFilter/ThreatActorActiveFilters"; +import FeedbackCard from "../../components/common/FeedBack/FeedbackCard"; +import PageHeader from "../../components/common/PageHeader/PageHeader"; +import SearchBar from "../../components/common/SearchBar/SearchBar"; +import { useThreatActorCatalog } from "../../hooks/threatactor/useThreatActorCatalog"; +import { useThreatActorQueries } from "../../hooks/threatactor/useThreatActorCatalogQueries"; +import "./ThreatActors.css"; +function ThreatActor() { + const pageSize = 9; + + const { + page, + setPage, + search, + searchInput, + setSearchInput, + activeFilters, + draftFilters, + setDraftFilters, + + handleApplyFilters, + handleSearch, + handleRemoveSearch, + handleRemoveFilter, + handleClearAll, + } = useThreatActorCatalog(); + + // Henter ut threat-actors og filteroptions + const { + actors, + actorsCount, + loading, + error, + filterOptions, + filtersLoading, + filtersError, + } = useThreatActorQueries({ + page, + pageSize, + search, + activeFilters, + }); + + return ( +
+ + + + {filtersError ? ( + + ) : filtersLoading && !filterOptions ? ( + + ) : ( + + )} + + + + {loading ? ( + + ) : error ? ( + + ) : actors.length === 0 ? ( + + ) : ( + + )} +
+ ); +} + +export default ThreatActor; diff --git a/frontend/src/pages/ThreatActors/ThreatActorDetailed.tsx b/frontend/src/pages/ThreatActors/ThreatActorDetailed.tsx deleted file mode 100644 index 3553dda..0000000 --- a/frontend/src/pages/ThreatActors/ThreatActorDetailed.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import ThreatActorDetailedHeader from "../../components/threatactor/ThreatActorDetailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader"; -import ThreatActorOverview from "../../components/threatactor/ThreatActorDetailed/ThreatActorDetailedOverview/ThreatActorOverview"; -import ThreatActorDetailedInfoCards from "../../components/threatactor/ThreatActorDetailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards"; -import FeedbackCard from "../../components/feedback/FeedbackCard"; -import "./ThreatActorDetailed.css"; -import ThreatActorDetailedDescription from "../../components/threatactor/ThreatActorDetailed/ThreatActorDetailedDescription/ThreatActorDetailedDescription"; -import { useParams } from "react-router-dom"; -import { useQuery } from "@apollo/client/react"; -import { GET_THREAT_ACTOR_BY_MITRE_ID, GET_THREAT_ACTOR_TECHNIQUES, GET_THREAT_ACTOR_SOFTWARES, } from "../../graphql/queries/threatActorQueries"; -import { getCountryCode } from "../../utils/getCountryCode"; -import ThreatActorAssociatedList from "../../components/threatactor/ThreatActorDetailed/ThreatActorAssociated/ThreatActorAssociatedList"; -import { useState } from "react"; - -function ThreatActorDetailed() { - //Henter inn mitre_id fra URL til siden som ble åpnet - const params = useParams(); - const mitreId = params.mitreId - - const PAGE_SIZE = 5; - - const [techniquesPage, setTechniquesPage] = useState(0); - const [softwaresPage, setSoftwaresPage] = useState(0); - - //Henter ut Detailed Info - const {data: detailedData, loading: detailedLoading, error: detailedError} = useQuery(GET_THREAT_ACTOR_BY_MITRE_ID, - { - variables: {mitreId: mitreId ?? ""}, - skip: !mitreId - } - ); - - //Henter ut Assosierte Techniques - const {data: techniquesData, loading: techniquesLoading, error: techniquesError} = useQuery(GET_THREAT_ACTOR_TECHNIQUES, { - variables: { - mitreId: mitreId ?? "", - limit: PAGE_SIZE, - offset: techniquesPage * PAGE_SIZE, - }, - skip: !mitreId, - }); - - //Henter ut Assosierte Softwares - const {data: softwaresData, loading: softwaresLoading, error: softwaresError} = useQuery(GET_THREAT_ACTOR_SOFTWARES, { - variables: { - mitreId: mitreId ?? "", - limit: PAGE_SIZE, - offset: softwaresPage * PAGE_SIZE, - }, - skip: !mitreId, - }); - - if (!mitreId) { - return ; - } - - if (detailedLoading) { - return ; - } - - if (detailedError) { - return ; - } - - const actor = detailedData?.getThreatActorByMitreId; - - if (!actor) { - return ; - } - - return ( -
-
- - - -
- - - -
- - - - -
- - {/* Associated Techniques */} - {techniquesLoading ? ( - - ) : techniquesError ? ( - - ) : ( - - )} - - {/* Associated Software */} - {softwaresLoading ? ( - - ) : softwaresError ? ( - - ) : ( - - )} - -
- - -
-
- ); -} - -export default ThreatActorDetailed; \ No newline at end of file diff --git a/frontend/src/pages/ThreatActors/ThreatActors.tsx b/frontend/src/pages/ThreatActors/ThreatActors.tsx deleted file mode 100644 index 1dcb5b0..0000000 --- a/frontend/src/pages/ThreatActors/ThreatActors.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import ThreatActorFilters, {DEFAULT_FILTERS, type ThreatActorFilterValues} from "../../components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilter"; -import ThreatActorGrid from "../../components/threatactor/ThreatActorMain/ThreatActorGrid/ThreatActorGrid"; -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, GET_THREAT_ACTOR_FILTER_OPTIONS } from "../../graphql/queries/threatActorQueries"; -import { useState } from "react"; -import ThreatActorActiveFilters from "../../components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorActiveFilters"; -import FeedbackCard from "../../components/feedback/FeedbackCard"; -import { useSessionStorage } from "../../hooks/useSessionStorage"; - - -function ThreatActor(){ - const pageSize = 9; - const [page, setPage] = useSessionStorage("threat-actors:page", 1);; - // Search - const [search, setSearch] = useSessionStorage("threat-actors:search", ""); - const [searchInput, setSearchInput] = useState(search); - // Filters - const [activeFilters, setActiveFilters] = useSessionStorage("threat-actors:filters", { ...DEFAULT_FILTERS }); - const [draftFilters, setDraftFilters] = useState(() => ({ ...activeFilters })); - - /** - * Aktiverer valgte filtre. - */ - const handleApplyFilters = (filters: ThreatActorFilterValues) => { - setActiveFilters(filters); - setPage(1); - }; - - /** - * Aktiverer søkeordet. - */ - const handleSearch = (searchWord: string) => { - const normalizedSearch = searchWord.trim(); - - setSearchInput(normalizedSearch); - setSearch(normalizedSearch); - setPage(1); - }; - - /** - * Fjerner søkeordet fra både inputfeltet og GraphQL. - */ - const handleRemoveSearch = () => { - setSearchInput(""); - setSearch(""); - setPage(1); - }; - - /** - * Fjerner ett aktivt filter uten å påvirke de andre. - */ - const handleRemoveFilter = (key: keyof ThreatActorFilterValues) => { - setActiveFilters((previous) => ({ - ...previous, - [key]: DEFAULT_FILTERS[key], - })); - - setDraftFilters((previous) => ({ - ...previous, - [key]: DEFAULT_FILTERS[key], - })); - - setPage(1); - }; - - /** - * Nullstiller hele katalogtilstanden. - */ - const handleClearAll = () => { - setSearchInput(""); - setSearch(""); - - setDraftFilters({ ...DEFAULT_FILTERS }); - setActiveFilters({ ...DEFAULT_FILTERS }); - - setPage(1); - }; - - const { sortBy, ...filters } = activeFilters; - - const {data, loading, error} = useQuery(GET_ALL_THREAT_ACTOR_GRID, { - variables: { - limit: pageSize, - offset: (page - 1) * pageSize, - search, - filters, - sortBy, - }, - } - ); - - //Henter data fra querien, [] hvis query feilet - const actors = data?.allThreatActorsGrid.items ?? []; - const actorsCount = data?.allThreatActorsGrid.totalCount ?? 0; - - //Henter ut data for filter - const {data: filterData, loading: filtersLoading, error: filtersError} = useQuery(GET_THREAT_ACTOR_FILTER_OPTIONS, {fetchPolicy: "cache-first"}) - const filterOptions = filterData?.threatActorFilterOptions; - - return ( -
- - - - {filtersError ? ( - - ) : filtersLoading && !filterOptions ? ( - - ) : ( - - )} - - - - {loading ? ( - - ) : error ? ( - - ) : actors.length === 0 ? ( - - ) : ( - - )} -
- ); -} - -export default ThreatActor; diff --git a/frontend/src/routes/AppRoutes.tsx b/frontend/src/routes/AppRoutes.tsx index 597487b..b2f168f 100644 --- a/frontend/src/routes/AppRoutes.tsx +++ b/frontend/src/routes/AppRoutes.tsx @@ -1,24 +1,34 @@ -import { BrowserRouter, Routes, Route } from "react-router-dom"; +import { BrowserRouter, Routes, Route, useParams } from "react-router-dom"; import Home from "../pages/Home/Home"; -import ThreatActors from "../pages/ThreatActors/ThreatActors"; +import ThreatActors from "../pages/ThreatActor/ThreatActors"; import Software from "../pages/Software/Software"; import SoftwareDetail from "../pages/Software/SoftwareDetail"; import Tactics from "../pages/Tactics/Tactics"; import SavedItems from "../pages/SavedItems/SavedItems"; import Techniques from "../pages/Techniques/Techniques"; -import AppLayout from "../components/layout/AppLayout"; +import AppLayout from "../components/common/Layout/AppLayout"; import TacticDetailed from "../pages/Tactics/TacticDetailed"; -import ThreatActorDetailed from "../pages/ThreatActors/ThreatActorDetailed"; +import ThreatActorDetailed from "../pages/ThreatActor/ThreatActorDetailed"; import TechniqueDetailed from "../pages/Techniques/TechniqueDetailed"; +/** + * Oppretter en ny ThreatActorDetailed-instans når MITRE-ID endres. + * Dette nullstiller lokal state, inkludert paginering. + */ +function ThreatActorDetailedRoute() { + const { mitreId } = useParams(); + + return ; +} + /** * Håndterer navigasjon mellom de ulike pages på nettsiden - * + * * Hver Route kobler sammen en URL til en page-component. * Componenten AppLayot er delt mellom alle pages, og allerede inneholder en header, * sidebar og en Outlet som er der innholdet til Home,ThreatActors, Software, - * Tactics, SavedItems og Techniques vises. - * + * Tactics, SavedItems og Techniques vises. + * * Med dette sparer vi duplisering av og
*/ function AppRoutes() { @@ -28,18 +38,24 @@ function AppRoutes() { }> } /> } /> - }/> + } + /> } /> } /> } /> - }/> + } /> } /> } /> - } /> + } + /> ); } -export default AppRoutes; \ No newline at end of file +export default AppRoutes; diff --git a/frontend/src/styles/dark.css b/frontend/src/styles/dark.css deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/src/styles/light.css b/frontend/src/styles/light.css deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/src/types/Example.tsx b/frontend/src/types/Example.tsx deleted file mode 100644 index 5943cdc..0000000 --- a/frontend/src/types/Example.tsx +++ /dev/null @@ -1 +0,0 @@ -// Just a file to make the folder appear in git. \ No newline at end of file diff --git a/frontend/src/types/ThreatActorTypes.ts b/frontend/src/types/ThreatActorTypes.ts index 888e183..2b2cbec 100644 --- a/frontend/src/types/ThreatActorTypes.ts +++ b/frontend/src/types/ThreatActorTypes.ts @@ -1,5 +1,11 @@ +/* ============================================================ + 1. THREAT ACTOR CATALOG + ============================================================ */ -/** Representerer én Threat Actor i gridet. */ +/** + * Representerer én Threat Actor i katalogens grid. + * Inneholder kun informasjonen som brukes av ThreatActorCard. + */ export type ThreatActorGridItem = { name: string; mitreId: string; @@ -9,14 +15,18 @@ export type ThreatActorGridItem = { motivation: string; }; -/** Responsen fra GraphQL-queryen som henter ThreatActorGrid. */ -export type ThreatActorsGridData = { - allThreatActorsGrid: { - totalCount: number; - items: ThreatActorGridItem[]; - }; -}; +/* ============================================================ + 2. FILTERING & SORTING + ============================================================ */ +/** + * Filterverdier som sendes til GraphQL-backend. + * + * Alle egenskaper er valgfrie, slik at brukeren kan filtrere + * på én eller flere egenskaper samtidig. + * + * Sortering håndteres separat gjennom sortBy. + */ export type ThreatActorFilterInput = { country?: string; region?: string; @@ -26,33 +36,83 @@ export type ThreatActorFilterInput = { dangerLevel?: string; }; -/** Variablene som brukes til server-side pagination i gridet. */ -export type ThreatActorsGridVariables = { - limit: number; - offset: number; - search?: string; - filters?: ThreatActorFilterInput; - sortBy?: string; +/** + * Representerer den samlede filter- og sorteringstilstanden + * i ThreatActor-katalogen. + * + * Brukes både som aktive filtre og som filterutkast. + * Tom streng betyr at et filter ikke er valgt. + */ +export type ThreatActorFilterValues = { + country: string; + region: string; + motivation: string; + status: string; + dangerLevel: string; + sector: string; + sortBy: string; }; /** - * Representerer én tilknyttet Technique eller Software. - * Begge bruker samme struktur: MITRE-ID og navn. + * Standardverdier for filtrering og sortering. + * + * Brukes ved initialisering av katalogen, nullstilling av + * filtre og fjerning av individuelle filtervalg. */ -export type AssociatedItem = { - mitreId: string; - name: string; +export const DEFAULT_FILTERS: ThreatActorFilterValues = { + country: "", + region: "", + motivation: "", + sector: "", + status: "", + dangerLevel: "", + sortBy: "name-asc", }; +/* ============================================================ + 3. FILTER OPTIONS + ============================================================ */ + /** - * Representerer én Threat Actor med all informasjon - * som trengs på detaljsiden. + * Representerer ett tilgjengelig filteralternativ. * - * Inneholder både attributter fra ThreatActor-tabellen - * og relaterte data fra andre databasetabeller. + * value: Verdien som brukes ved filtrering. + * label: Teksten som presenteres i brukergrensesnittet. + * count: Antall registrerte treff for alternativet. + */ +export type ThreatActorFilterOption = { + value: string; + label: string; + count: number; +}; + +/** + * GraphQL-respons med tilgjengelige filteralternativer. * - * Relasjoner med 0..* kardinalitet representeres som arrays, - * der en tom array [] betyr at ingen relasjoner finnes. + * Alternativene grupperes etter filterkategori og brukes + * til å fylle filtermenyene i ThreatActorFilter. + */ +export type ThreatActorFilterOptionsData = { + threatActorFilterOptions: { + countries: ThreatActorFilterOption[]; + regions: ThreatActorFilterOption[]; + motivations: ThreatActorFilterOption[]; + sectors: ThreatActorFilterOption[]; + statuses: ThreatActorFilterOption[]; + danger_levels: ThreatActorFilterOption[]; + }; +}; + +/* ============================================================ + 4. THREAT ACTOR DETAILS + ============================================================ */ + +/** + * Representerer en Threat Actor med informasjonen + * som presenteres på detaljsiden. + * + * aliases og sectors inneholder flere verdier og + * representeres derfor som arrays. */ export type ThreatActorDetailedItem = { threatId: string; @@ -69,61 +129,103 @@ export type ThreatActorDetailedItem = { dangerLevel: string; xMitreDeprecated: boolean; - /** Alle aliaser og sektorer knyttet til aktøren. */ aliases: string[]; sectors: string[]; }; +/* ============================================================ + 5. ASSOCIATED ENTITIES + ============================================================ */ + /** - * Responsen fra GraphQL-queryen getThreatActorByMitreId. - * Returnerer null dersom MITRE-ID-en ikke finnes i databasen. + * Representerer én tilknyttet Technique eller Software. + * Begge entitetene identifiseres med MITRE-ID og navn. */ -export type ThreatActorDetailedData = { - getThreatActorByMitreId: ThreatActorDetailedItem | null; +export type AssociatedItem = { + mitreId: string; + name: string; }; /** - * Variablene som sendes til getThreatActorByMitreId. - * MITRE-ID identifiserer hvilken Threat Actor som skal hentes. + * En paginert samling av tilknyttede entiteter. + * + * items: Elementene på den aktuelle siden. + * totalCount: Totalt antall tilknyttede elementer. */ -export type ThreatActorDetailedVariables = { - mitreId: string; +export type AssociatedItemsPage = { + items: AssociatedItem[]; + totalCount: number; }; -export type AssociatedItemsPage = { - items: AssociatedItem[]; +/* ============================================================ + 6. GRAPHQL QUERY TYPES + ============================================================ */ + +/* ---------- Catalog Query ---------- */ + +/** Respons fra GraphQL-queryen allThreatActorsGrid. */ +export type ThreatActorsGridData = { + allThreatActorsGrid: { totalCount: number; + items: ThreatActorGridItem[]; + }; }; -export type ThreatActorTechniquesData = { - getThreatActorTechniques: AssociatedItemsPage; +/** + * Variabler for katalogqueryen. + * + * limit og offset styrer server-side paginering. + * search, filters og sortBy bestemmer hvilke resultater + * backend skal returnere, og i hvilken rekkefølge. + */ +export type ThreatActorsGridVariables = { + limit: number; + offset: number; + search?: string; + filters?: ThreatActorFilterInput; + sortBy?: string; }; -export type ThreatActorSoftwaresData = { - getThreatActorSoftwares: AssociatedItemsPage; +/* ---------- Detailed Query ---------- */ + +/** + * Respons fra getThreatActorByMitreId. + * Returnerer null dersom aktøren ikke finnes. + */ +export type ThreatActorDetailedData = { + getThreatActorByMitreId: ThreatActorDetailedItem | null; }; -export type ThreatActorAssociatedVariables = { - mitreId: string; - limit: number; - offset: number; +/** MITRE-ID-en til aktøren som skal hentes. */ +export type ThreatActorDetailedVariables = { + mitreId: string; }; -/** Representerer ett filteralternativ. */ -export type ThreatActorFilterOption = { - value: string; - label: string; - count: number; +/* ---------- Associated Techniques Query ---------- */ + +/** Respons med paginerte teknikker tilknyttet aktøren. */ +export type ThreatActorTechniquesData = { + getThreatActorTechniques: AssociatedItemsPage; }; -/** Responsen fra GraphQL-queryen som henter filteralternativene. */ -export type ThreatActorFilterOptionsData = { - threatActorFilterOptions: { - countries: ThreatActorFilterOption[]; - regions: ThreatActorFilterOption[]; - motivations: ThreatActorFilterOption[]; - sectors: ThreatActorFilterOption[]; - statuses: ThreatActorFilterOption[]; - danger_levels: ThreatActorFilterOption[]; - }; -}; \ No newline at end of file +/* ---------- Associated Software Query ---------- */ + +/** Respons med paginert programvare tilknyttet aktøren. */ +export type ThreatActorSoftwaresData = { + getThreatActorSoftwares: AssociatedItemsPage; +}; + +/* ---------- Associated Entities Variables ---------- */ + +/** + * Felles variabler for uthenting av tilknyttede entiteter. + * + * mitreId: Identifiserer aktøren. + * limit: Maksimalt antall elementer per side. + * offset: Antall elementer som hoppes over. + */ +export type ThreatActorAssociatedVariables = { + mitreId: string; + limit: number; + offset: number; +}; diff --git a/frontend/src/utils/getCountryCode.ts b/frontend/src/utils/getCountryCode.ts index 8e7ee3c..3306699 100644 --- a/frontend/src/utils/getCountryCode.ts +++ b/frontend/src/utils/getCountryCode.ts @@ -7,21 +7,21 @@ */ const countryCodes: Record = { "North Korea": "KP", - "Russia": "RU", - "China": "CN", - "Pakistan": "PK", - "India": "IN", - "Vietnam": "VN", - "Iran": "IR", + 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", + 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 +}