Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 7 additions & 13 deletions backend/src/graphql/resolvers/threatActorResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,14 @@
*
* GraphQL Defs -> Resolver -> Repository -> Drizzle -> MySQL
*/



/**
* Resolvere for GraphQL-operasjoner relatert til ThreatActors.
*
* Resolveren mottar GraphQL-spørringer og sender arbeidet
* videre til threatActorRepository.
*
* GraphQL -> Resolver -> Repository -> Drizzle -> MySQL
*/

import {getAllThreatActorsGrid, getThreatActorByMitreId, getThreatActorSoftwares, getThreatActorTechniques} from "../../repositories/threatActorRepository.js";
import {getAllThreatActorsGrid, getThreatActorByMitreId, getThreatActorFilterOptions, getThreatActorSoftwares, getThreatActorTechniques} from "../../repositories/threatActorRepository.js";

export const threatActorResolvers = {
Query: {
/** Henter ut alle filterOptions */
threatActorFilterOptions: async (_parent: unknown) => {
return getThreatActorFilterOptions();
},
/**
* Henter alle ThreatActors for Grid i front end.
*/
Expand All @@ -48,5 +40,7 @@ export const threatActorResolvers = {
getThreatActorSoftwares: async (_parent: unknown, args: {mitreId: string; limit: number; offset: number}) => {
return getThreatActorSoftwares(args.mitreId, args.limit, args.offset);
},


},
};
14 changes: 14 additions & 0 deletions backend/src/graphql/typeDefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@
* GraphQL schema: defines the types and operations available to the frontend.
*/
export const typeDefs = `#graphql
type ThreatActorFilterOption {
value: String!
label: String!
count: Int!
}
type ThreatActorFilterOptions {
countries: [ThreatActorFilterOption!]!
regions: [ThreatActorFilterOption!]!
motivations: [ThreatActorFilterOption!]!
sectors: [ThreatActorFilterOption!]!
statuses: [ThreatActorFilterOption!]!
danger_levels: [ThreatActorFilterOption!]!
}
type ThreatActorGrid {
name: String!
mitreId: String!
Expand Down Expand Up @@ -114,6 +127,7 @@ export const typeDefs = `#graphql
getThreatActorByMitreId(mitreId: String!): ThreatActorDetailed
getThreatActorTechniques(mitreId: String!, limit: Int!, offset: Int!): AssociatedItemsPage!
getThreatActorSoftwares(mitreId: String!, limit: Int!, offset: Int!): AssociatedItemsPage!
threatActorFilterOptions: ThreatActorFilterOptions!
######################################## Techinques vvvv
Expand Down
79 changes: 78 additions & 1 deletion backend/src/repositories/threatActorRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,6 @@ export async function getThreatActorTechniques(mitreId: string, limit: number, o
};
}


export async function getThreatActorSoftwares(mitreId: string, limit: number, offset: number) {
const [softwares, countResult] = await Promise.all([

Expand Down Expand Up @@ -184,4 +183,82 @@ export async function getThreatActorSoftwares(mitreId: string, limit: number, of
items: softwares,
totalCount: countResult[0]?.totalCount ?? 0,
};
}

// Henter ut alle filteroptions for filtreringen i ThreatActor
export async function getThreatActorFilterOptions() {
/** Henter ut land options */
const countryFilterOptions = db
.select({country: sql<string>`COALESCE(${threatActor.country}, 'Unknown')`, totalCount: count()})
.from(threatActor)
.groupBy(threatActor.country);
/** Henter ut region options */
const regionFilterOptions = db
.select({region: sql<string>`COALESCE(${threatActor.region}, 'Unknown')`, totalCount: count()})
.from(threatActor)
.groupBy(threatActor.region);
/** Henter ut motivasjon options */
const motivationFilterOptions = db
.select({motivation: sql<string>`COALESCE(${threatActor.motivation}, 'Unknown')`, totalCount: count()})
.from(threatActor)
.groupBy(threatActor.motivation);
/** Henter ut sector options */
const sectorFilterOptions = db
.select({sector: threatActorSector.sector, totalCount: count()})
.from(threatActorSector)
.groupBy(threatActorSector.sector);
/** Henter ut status options*/
const statusFilterOptions = db
.select({status: threatActor.status, totalCount: count()})
.from(threatActor)
.groupBy(threatActor.status);
/** Henter ut trusselnivå options*/
const dangerFilterOptions = db
.select({danger: threatActor.danger_level, totalCount: count()})
.from(threatActor)
.groupBy(threatActor.danger_level);
//**Kjører queires parallelt, og venter til alle queries er fullført */
const [countries, regions, motivations, sectors, statuses, danger_levels] = await Promise.all([
countryFilterOptions,
regionFilterOptions,
motivationFilterOptions,
sectorFilterOptions,
statusFilterOptions,
dangerFilterOptions,
])

//Mapper det til objekter vi enkelt kan bruke i front
return {
countries: countries.map((row) => ({
value: row.country,
label: row.country,
count: row.totalCount
})),
regions: regions.map((row) => ({
value: row.region,
label: row.region,
count: row.totalCount
})),
motivations: motivations.map((row) => ({
value: row.motivation,
label: row.motivation,
count: row.totalCount
})),
sectors: sectors.map((row) => ({
value: row.sector,
label: row.sector,
count: row.totalCount
})),
//Lager en Label for ACTIVE = A + ctive = Active
statuses: statuses.map((row) => ({
value: row.status,
label: row.status.charAt(0).toUpperCase() + row.status.slice(1).toLowerCase(),
count: row.totalCount
})),
danger_levels: danger_levels.map((row) => ({
value: row.danger,
label: row.danger.charAt(0).toUpperCase() + row.danger.slice(1).toLowerCase(),
count: row.totalCount
})),
};
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

.threat-actor-filters {
display: flex;
flex-direction: column;
Expand All @@ -6,86 +7,80 @@

.filters-title {
color: var(--text-heading);

font-size: 14px;
font-weight: 600;
}

/* Fire kolonner på desktop */
.filters-content {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));

gap: 12px;
align-items: stretch;
}

.filter-field {
/* Apply Filters fyller sin grid-celle */
.filters-actions {
display: flex;
width: 100%;
min-width: 0;
min-height: 56px;
}

.apply-filters-button {
display: flex;
align-items: center;
gap: 12px;
justify-content: center;
gap: 8px;

width: 100%;
min-height: 52px;
padding: 10px 12px;

padding: 8px 14px;
background: var(--accent, #00ded4);
color: #001b1d;

background: var(--surface);
border: 1px solid var(--accent, #00ded4);
border-radius: 7px;

border: 1px solid var(--border);
border-radius: 8px;
font-family: inherit;
font-size: 12px;
font-weight: 600;
cursor: pointer;

transition:
border-color 0.2s ease,
background-color 0.2s ease;
background-color 0.2s ease,
border-color 0.2s ease;
}

.filter-field:hover {
border-color: #0b5558;
background: var(--surface-hover);
.apply-filters-button:hover {
background: #00bdb5;
border-color: #00bdb5;
}

.filter-field:focus-within {
border-color: var(--accent);
.apply-filters-button:active {
background: #009f99;
}

.filter-field > svg {
width: 21px;
height: 21px;

flex-shrink: 0;

color: var(--text-muted);
.apply-filters-button:focus-visible {
outline: 2px solid var(--text-heading, #ffffff);
outline-offset: 3px;
}

.filter-field:focus-within > svg {
color: var(--accent);
.apply-filters-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}

.filter-text {
width: 100%;
min-width: 0;

display: flex;
flex-direction: column;
gap: 2px;
/* Nettbrett */
@media (max-width: 900px) {
.filters-content {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}

.filter-text > span {
color: var(--text-heading);

font-size: 12px;
font-weight: 600;
/* Mobil */
@media (max-width: 520px) {
.filters-content {
grid-template-columns: minmax(0, 1fr);
}
}

.filter-text select {
width: 100%;

border: 0;
outline: 0;

background: transparent;
color: var(--text-muted);

font-size: 12px;

cursor: pointer;
}
Loading