diff --git a/backend/src/index.ts b/backend/src/index.ts index 98b3c8b..de77e9d 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -16,6 +16,7 @@ const server = new ApolloServer({ const { url } = await startStandaloneServer(server, { listen: { + host: process.env.HOST ?? "0.0.0.0", port: Number(process.env.PORT ?? 3001), }, }); diff --git a/frontend/src/components/tactic/TacticDetailed/RelatedTechniques/RelatedTechniques.css b/frontend/src/components/tactic/TacticDetailed/RelatedTechniques/RelatedTechniques.css index 19b3751..6cffe66 100644 --- a/frontend/src/components/tactic/TacticDetailed/RelatedTechniques/RelatedTechniques.css +++ b/frontend/src/components/tactic/TacticDetailed/RelatedTechniques/RelatedTechniques.css @@ -119,9 +119,58 @@ /* ===== List ===== */ .related-techniques-list { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.related-techniques-pagination { display: flex; - flex-direction: column; - gap: 6px; + align-items: center; + justify-content: center; + gap: 16px; + + margin-top: 14px; +} + +.related-techniques-pagination span { + color: var(--text); + font-size: 13px; + white-space: nowrap; +} + +.related-techniques-pagination button { + width: 34px; + height: 34px; + + display: flex; + align-items: center; + justify-content: center; + + padding: 0; + + background: var(--bg); + color: var(--accent); + + border: 1px solid var(--border); + border-radius: 6px; + + cursor: pointer; +} + +.related-techniques-pagination button:hover:not(:disabled) { + background: var(--accent-soft); + border-color: var(--accent); +} + +.related-techniques-pagination button:disabled { + opacity: 0.35; + cursor: not-allowed; +} + +.related-techniques-pagination button:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; } @@ -131,13 +180,14 @@ min-width: 0; display: grid; - grid-template-columns: 38px 70px minmax(0, 1fr) 18px; + grid-template-columns: 50px minmax(0, 1fr) 18px; + grid-template-rows: auto 1fr; align-items: center; gap: 10px; - min-height: 48px; + min-height: 82px; - padding: 6px 10px; + padding: 12px; background: var(--bg); @@ -153,8 +203,8 @@ /* ===== Icon ===== */ .related-technique-icon { - width: 34px; - height: 34px; + width: 50px; + height: 50px; display: flex; align-items: center; @@ -164,6 +214,9 @@ color: var(--accent); border-radius: 6px; + + grid-column: 1; + grid-row: 1 / 3; } .related-technique-icon svg { @@ -175,10 +228,22 @@ /* ===== MITRE ID ===== */ .related-technique-id { - color: var(--text); + min-width: 64px; + + padding: 6px 8px; + + background: var(--accent-soft); + color: var(--accent); + + border-radius: 6px; font-size: 13px; font-weight: 700; + text-align: center; + white-space: nowrap; + + grid-column: 2; + grid-row: 1; } @@ -186,20 +251,15 @@ .related-technique-content { min-width: 0; + grid-column: 2; + grid-row: 2; } .related-technique-content h3 { - margin-bottom: 2px; - + overflow-wrap: anywhere; font-size: 13px; - font-weight: 600; -} - -.related-technique-content p { - color: var(--text-muted); - - font-size: 11px; - line-height: 1.3; + font-weight: 500; + text-align: center; } @@ -210,6 +270,9 @@ height: 16px; color: var(--text-muted); + + grid-column: 3; + grid-row: 1 / 3; } .related-technique-row:hover .related-technique-arrow { @@ -220,21 +283,12 @@ /* ===== Tablet / mobile ===== */ @media (max-width: 700px) { - .related-technique-row { - grid-template-columns: 36px minmax(0, 1fr) 18px; + .related-techniques-list { + grid-template-columns: minmax(0, 1fr); } - .related-technique-id { - grid-column: 2; - } - - .related-technique-content { - grid-column: 2; - } - - .related-technique-arrow { - grid-column: 3; - grid-row: 1 / 3; + .related-technique-row { + min-height: 76px; } } @@ -251,7 +305,9 @@ padding: 10px; } - .related-technique-content p { - font-size: 10px; + .related-technique-id { + min-width: 64px; + 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 b24464b..9464fe0 100644 --- a/frontend/src/components/tactic/TacticDetailed/RelatedTechniques/RelatedTechniques.tsx +++ b/frontend/src/components/tactic/TacticDetailed/RelatedTechniques/RelatedTechniques.tsx @@ -1,4 +1,5 @@ import { + ChevronLeft, ChevronRight, Search, Shield, @@ -16,19 +17,29 @@ type Technique = { type RelatedTechniquesProps = { tacticName: string; techniques: Technique[]; + totalCount: number; + page: number; + pageSize: number; + onPageChange: (page: number) => void; }; function RelatedTechniques({ tacticName, techniques, + totalCount, + page, + pageSize, + onPageChange, }: RelatedTechniquesProps) { + const totalPages = Math.ceil(totalCount / pageSize); + return (

- Related Techniques ({techniques.length}) + Related Techniques ({totalCount})

@@ -68,13 +79,38 @@ function RelatedTechniques({

{technique.name}

-

{technique.description}

))} + + {totalPages > 1 && ( +
+ + + + Page {page + 1} of {totalPages} + + + +
+ )}
); } diff --git a/frontend/src/components/tactic/TacticMain/TacticCard/TacticCard.tsx b/frontend/src/components/tactic/TacticMain/TacticCard/TacticCard.tsx index 108de1f..03687b9 100644 --- a/frontend/src/components/tactic/TacticMain/TacticCard/TacticCard.tsx +++ b/frontend/src/components/tactic/TacticMain/TacticCard/TacticCard.tsx @@ -42,7 +42,7 @@ function TacticCard({ {techniqueCount} techniques diff --git a/frontend/src/graphql/queries/tacticQueries.ts b/frontend/src/graphql/queries/tacticQueries.ts new file mode 100644 index 0000000..c4ad981 --- /dev/null +++ b/frontend/src/graphql/queries/tacticQueries.ts @@ -0,0 +1,67 @@ +import { gql, type TypedDocumentNode } from "@apollo/client"; + +type Tactic = { + mitreId: string; + name: string; + description: string; + techniqueCount: number; +}; + +type TacticTechnique = { + mitreId: string; + name: string; + description: string; +}; + +type TacticDetails = { + mitreId: string; + name: string; + description: string; +}; + +type TacticsGridData = { + allTactics: Tactic[]; +}; + +type TacticDetailsData = { + getTacticByMitreId: TacticDetails | null; + getTacticTechniques: { + items: TacticTechnique[]; + totalCount: number; + }; +}; + +type TacticDetailsVariables = { + mitreId: string; + limit: number; + offset: number; +}; + +export const GET_ALL_TACTICS: TypedDocumentNode = gql` + query GetAllTactics { + allTactics { + mitreId + name + description + techniqueCount + } + } +`; + +export const GET_TACTIC_DETAILS: TypedDocumentNode = gql` + query GetTacticDetails($mitreId: String!, $limit: Int!, $offset: Int!) { + getTacticByMitreId(mitreId: $mitreId) { + mitreId + name + description + } + getTacticTechniques(mitreId: $mitreId, limit: $limit, offset: $offset) { + totalCount + items { + mitreId + name + description + } + } + } +`; \ No newline at end of file diff --git a/frontend/src/pages/Tactics/TacticDetailed.tsx b/frontend/src/pages/Tactics/TacticDetailed.tsx index 911f691..2d94392 100644 --- a/frontend/src/pages/Tactics/TacticDetailed.tsx +++ b/frontend/src/pages/Tactics/TacticDetailed.tsx @@ -1,60 +1,61 @@ import RelatedTechniques from "../../components/tactic/TacticDetailed/RelatedTechniques/RelatedTechniques"; import TacticDescription from "../../components/tactic/TacticDetailed/TacticDescription/TacticDescription"; import TacticDetailedHeader from "../../components/tactic/TacticDetailed/TacticDetailedHeader/TacticDetailedHeader"; +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 "./TacticDetailed.css"; -//Midlertidig statisk. -const techniques = [ - { - mitreId: "T1566", - name: "Phishing", - description: - "Adversaries may send phishing messages to deceive victims into revealing information or opening malicious content.", - }, - { - mitreId: "T1190", - name: "Exploit Public-Facing Application", - description: - "Adversaries may exploit vulnerabilities in internet-facing applications to gain initial access.", - }, - { - mitreId: "T1078", - name: "Valid Accounts", - description: - "Adversaries may obtain and use valid accounts to gain access to systems and networks.", - }, - { - mitreId: "T1133", - name: "External Remote Services", - description: - "Adversaries may use externally accessible remote services to gain initial access.", - }, - { - mitreId: "T1189", - name: "Drive-by Compromise", - description: - "Adversaries may gain access through a user visiting a compromised website.", - }, -]; - function TacticDetailed() { + const { mitreId } = useParams(); + const pageSize = 20; + const [techniquesPage, setTechniquesPage] = useState(0); + const { data, loading, error } = useQuery(GET_TACTIC_DETAILS, { + variables: { + mitreId: mitreId ?? "", + limit: pageSize, + offset: techniquesPage * pageSize, + }, + skip: !mitreId, + }); + + if (!mitreId) { + return

Missing Tactic ID.

; + } + + if (loading) { + return

Loading Tactic...

; + } + + if (error) { + return

Error loading Tactic: {error.message}

; + } + + const tactic = data?.getTacticByMitreId; + + if (!tactic) { + return

Tactic not found.

; + } return (
- + - +
); } diff --git a/frontend/src/pages/Tactics/Tactics.tsx b/frontend/src/pages/Tactics/Tactics.tsx index 06491ef..482f3ef 100644 --- a/frontend/src/pages/Tactics/Tactics.tsx +++ b/frontend/src/pages/Tactics/Tactics.tsx @@ -1,105 +1,18 @@ 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 "./Tactics.css"; function Tactics() { - const tactics = [ - { - mitreId: "TA0001", - name: "Initial Access", - description: "Gain an initial foothold within a target environment.", - techniqueCount: 0, - }, - { - mitreId: "TA0002", - name: "Execution", - description: "Run adversary-controlled code on a target system.", - techniqueCount: 0, - }, - { - mitreId: "TA0003", - name: "Persistence", - description: "Maintain access to systems across interruptions.", - techniqueCount: 0, - }, - { - mitreId: "TA0004", - name: "Privilege Escalation", - description: "Gain higher-level permissions within a target system.", - techniqueCount: 0, - }, - { - mitreId: "TA0005", - name: "Stealth", - description: "Hide malicious activity and avoid detection.", - techniqueCount: 0, - }, - { - mitreId: "TA0006", - name: "Credential Access", - description: "Steal account names and authentication credentials.", - techniqueCount: 0, - }, - { - mitreId: "TA0007", - name: "Discovery", - description: "Gather information about systems and environments.", - techniqueCount: 0, - }, - { - mitreId: "TA0008", - name: "Lateral Movement", - description: "Move through systems and accounts inside a network.", - techniqueCount: 0, - }, - { - mitreId: "TA0009", - name: "Collection", - description: "Gather data relevant to the adversary's objectives.", - techniqueCount: 0, - }, - { - mitreId: "TA0010", - name: "Exfiltration", - description: "Remove collected data from the target environment.", - techniqueCount: 0, - }, - { - mitreId: "TA0011", - name: "Command and Control", - description: "Communicate with compromised systems under adversary control.", - techniqueCount: 0, - }, - { - mitreId: "TA0040", - name: "Impact", - description: "Manipulate, interrupt, or destroy systems and data.", - techniqueCount: 0, - }, - { - mitreId: "TA0042", - name: "Resource Development", - description: "Establish resources that support future operations.", - techniqueCount: 0, - }, - { - mitreId: "TA0043", - name: "Reconnaissance", - description: "Gather information to plan future operations.", - techniqueCount: 0, - }, - { - mitreId: "TA0112", - name: "Defense Impairment", - description: "Reduce or disable defensive capabilities.", - techniqueCount: 0, - }, - ]; + const { data, loading, error } = useQuery(GET_ALL_TACTICS); return (
- + {loading &&

Loading tactics...

} + {error &&

Error loading tactics: {error.message}

} + {!loading && !error && }
); } diff --git a/frontend/src/routes/AppRoutes.tsx b/frontend/src/routes/AppRoutes.tsx index 5bbbe51..d340da8 100644 --- a/frontend/src/routes/AppRoutes.tsx +++ b/frontend/src/routes/AppRoutes.tsx @@ -32,7 +32,7 @@ function AppRoutes() { } /> } /> } /> - }/> + }/> } /> } /> } />