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
3 changes: 2 additions & 1 deletion backend/src/graphql/resolvers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@

import { threatActorResolvers } from "./threatActorResolver.js";
import { techniqueResolvers } from "./techniqueResolver.js";

import { softwareResolvers } from "./softwareResolver.js";


export const resolvers = {
Query: {
...threatActorResolvers.Query,
...techniqueResolvers.Query,
...softwareResolvers.Query,
},
};
24 changes: 12 additions & 12 deletions backend/src/graphql/resolvers/softwareResolver.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/**
* Resolver-laget håndterer GraphQL-spørringer fra klienten.
*
* Resolverne kobler GraphQL-feltene til riktig funksjon i
* repository-laget og sender resultatet tilbake til klienten.
*
* Database-logikk skal ikke ligge direkte i resolverne.
* Den håndteres av repositories.
*
*
* GraphQL -> Resolver -> Repository -> Drizzle -> MySQL
*/
import {
getAllSoftware,
getSoftwareByMitreId,
} from "../../repositories/softwareRepository.js";

export const softwareResolvers = {
Query: {
software: async () => getAllSoftware(),
softwareByMitreId: async (_parent: unknown, args: { mitreId: string }) =>
getSoftwareByMitreId(args.mitreId),
},
};
62 changes: 25 additions & 37 deletions backend/src/graphql/typeDefs.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
/**
* typeDefs beskriver hvilke operasjoner frontend
* har lov til å spørre GraphQL-API-et om.
*
* Det kan sammenlignes med et interface/kontrakt:
* vi definerer hvilke typer og operasjoner som finnes,
* mens selve implementasjonen ligger i resolvers/.
* GraphQL schema: defines the types and operations available to the frontend.
*/

export const typeDefs = `#graphql
# Beskriver hvordan et ThreatActor-objekt ser ut
type ThreatActorGrid {
name: String!
mitreId: String!
Expand All @@ -23,10 +15,10 @@ export const typeDefs = `#graphql
items: [ThreatActorGrid!]!
totalCount: Int!
}
type AssociatedItem {
mitreId: String!
name: String!
mitreId: String!
name: String!
}
type AssociatedItemsPage {
Expand All @@ -48,7 +40,6 @@ export const typeDefs = `#graphql
status: String!
dangerLevel: String!
xMitreDeprecated: Boolean!
aliases: [String!]!
sectors: [String!]!
}
Expand All @@ -59,35 +50,32 @@ export const typeDefs = `#graphql
description: String!
}
type Query {
######################################## ThreatActors vvvv
type RelatedSoftwareEntity {
id: String!
mitreId: String!
name: String!
}
# Henter alle threat actors som trengs i grid
allThreatActorsGrid(limit: Int!, offset: Int!): ThreatActorGridInfo!
type Software {
id: String!
mitreId: String!
name: String!
description: String!
type: String!
aliases: [String!]!
platforms: [String!]!
associatedTechniques: [RelatedSoftwareEntity!]!
usedByThreatActors: [RelatedSoftwareEntity!]!
}
# Henter alt nødvending info for ThreatActorDetailed i frontend
type Query {
allThreatActorsGrid(limit: Int!, offset: Int!): ThreatActorGridInfo!
getThreatActorByMitreId(mitreId: String!): ThreatActorDetailed
getThreatActorTechniques(mitreId: String!, limit: Int!, offset: Int!): AssociatedItemsPage!
getThreatActorSoftwares(mitreId: String!, limit: Int!, offset: Int!): AssociatedItemsPage!
######################################## Techinques vvvv
# Henter alle Techniques
allTechniques: [Technique!]!
# Henter én Technique basert på MITRE-ID
techniqueByMitreId(mitreId: String!): Technique
######################################## Software vvvv
######################################## Tactics vvvv
software: [Software!]!
softwareByMitreId(mitreId: String!): Software
}
`;
`;
150 changes: 134 additions & 16 deletions backend/src/repositories/softwareRepository.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,134 @@
/**
* Repository-laget samler databaseoperasjoner relatert til Software.
*
* Hensikten er å skille database-logikken fra resten av backenden:
* resolvers skal ikke skrive Drizzle-spørringer direkte, men heller
* kalle funksjoner fra dette repositoryet.
*
* Dermed får vi en tydelig ansvarsfordeling:
*
* GraphQL resolver -> Repository -> Drizzle -> MySQL
*
* Dette gjør koden lettere å lese, teste og endre senere.
*
*
* !!!ALTSÅ HER SKRIVER VI KODEN SOM FAKTISK HENTER UT DATA FRA DB VIA DRIZZLE!!!!
*/
import { eq, inArray } from "drizzle-orm";

import { db } from "../db/connection.js";
import {
software,
softwareAlias,
softwarePlatform,
technique,
techniqueSoftware,
threatActor,
threatActorSoftware,
} from "../db/schema.js";

async function getSoftwareMetadata(softwareIds: string[]) {
if (softwareIds.length === 0) {
return {
aliases: [],
platforms: [],
associatedTechniques: [],
usedByThreatActors: [],
};
}

const [aliases, platforms, associatedTechniques, usedByThreatActors] =
await Promise.all([
db
.select({
softwareId: softwareAlias.software_id,
alias: softwareAlias.alias,
})
.from(softwareAlias)
.where(inArray(softwareAlias.software_id, softwareIds)),
db
.select({
softwareId: softwarePlatform.software_id,
platform: softwarePlatform.platform,
})
.from(softwarePlatform)
.where(inArray(softwarePlatform.software_id, softwareIds)),
db
.select({
softwareId: techniqueSoftware.software_id,
id: technique.technique_id,
mitreId: technique.mitre_id,
name: technique.name,
})
.from(techniqueSoftware)
.innerJoin(
technique,
eq(techniqueSoftware.technique_id, technique.technique_id),
)
.where(inArray(techniqueSoftware.software_id, softwareIds)),
db
.select({
softwareId: threatActorSoftware.software_id,
id: threatActor.threat_id,
mitreId: threatActor.mitre_id,
name: threatActor.name,
})
.from(threatActorSoftware)
.innerJoin(
threatActor,
eq(threatActorSoftware.threat_id, threatActor.threat_id),
)
.where(inArray(threatActorSoftware.software_id, softwareIds)),
]);

return { aliases, platforms, associatedTechniques, usedByThreatActors };
}

export async function getAllSoftware() {
const entries = await db
.select({
id: software.software_id,
mitreId: software.mitre_id,
name: software.name,
description: software.description,
type: software.software_type,
})
.from(software)
.orderBy(software.name);

const metadata = await getSoftwareMetadata(entries.map((entry) => entry.id));

return entries.map((entry) => ({
...entry,
aliases: metadata.aliases
.filter((alias) => alias.softwareId === entry.id)
.map((alias) => alias.alias),
platforms: metadata.platforms
.filter((platform) => platform.softwareId === entry.id)
.map((platform) => platform.platform),
}));
}

export async function getSoftwareByMitreId(mitreId: string) {
const [entry] = await db
.select({
id: software.software_id,
mitreId: software.mitre_id,
name: software.name,
description: software.description,
type: software.software_type,
})
.from(software)
.where(eq(software.mitre_id, mitreId))
.limit(1);

if (!entry) {
return null;
}

const metadata = await getSoftwareMetadata([entry.id]);

return {
...entry,
aliases: metadata.aliases.map((alias) => alias.alias),
platforms: metadata.platforms.map((platform) => platform.platform),
associatedTechniques: metadata.associatedTechniques.map(
({ id, mitreId: relatedMitreId, name }) => ({
id,
mitreId: relatedMitreId,
name,
}),
),
usedByThreatActors: metadata.usedByThreatActors.map(
({ id, mitreId: relatedMitreId, name }) => ({
id,
mitreId: relatedMitreId,
name,
}),
),
};
}