Skip to content

Commit

Permalink
add protection index, update attack models and backend logic
Browse files Browse the repository at this point in the history
  • Loading branch information
NikhilShahi committed Sep 28, 2022
1 parent 2a4de4b commit 0220746
Show file tree
Hide file tree
Showing 13 changed files with 645 additions and 19 deletions.
1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"dev-collector": "nodemon -r tsconfig-paths/register src/collector.ts",
"start-collector": "TS_NODE_BASEURL=./dist/ node -r tsconfig-paths/register dist/collector.js",
"sync-endpoints": "ts-node -r tsconfig-paths/register src/scripts/generate-endpoints.ts",
"generate-attacks": "ts-node -r tsconfig-paths/register src/scripts/generate-attacks.ts",
"generate-alerts": "ts-node -r tsconfig-paths/register src/scripts/generate-alerts.ts",
"format": "prettier --write './src/**/*.{ts,tsx}'"
},
Expand Down
3 changes: 3 additions & 0 deletions backend/src/models/attack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ export class Attack extends BaseEntity {
@Column({ type: "text", nullable: true })
uniqueSessionKey: string

@Column()
host: string

@Column({ nullable: true })
sourceIP: string

Expand Down
62 changes: 62 additions & 0 deletions backend/src/scripts/generate-attacks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import yargs from "yargs"
import { randomBytes } from "crypto"
import { AppDataSource } from "data-source"
import { ApiEndpoint, Attack } from "models"
import { AttackType } from "@common/enums"
import { ATTACK_TYPE_TO_RISK_SCORE } from "@common/maps"
import { DateTime } from "luxon"

const randomDate = (start?: boolean) => {
const startTime = start
? DateTime.now().minus({ hours: 5 }).toJSDate().getTime()
: DateTime.now().minus({ minutes: 50 }).toJSDate().getTime()
const endTime = start
? DateTime.now().minus({ hours: 1 }).toJSDate().getTime()
: DateTime.now().toJSDate().getTime()
return new Date(startTime + Math.random() * (endTime - startTime))
}

const generateAttacks = async (numAttacks: number) => {
const queryRunner = AppDataSource.createQueryRunner()
await queryRunner.connect()
try {
const endpoints = await queryRunner.manager.find(ApiEndpoint, {
select: { uuid: true, host: true },
})
const attackTypes = Object.keys(AttackType)
const insertAttacks: Attack[] = []
for (let i = 0; i < numAttacks; i++) {
const newAttack = new Attack()
const randTypeNum = Math.floor(Math.random() * attackTypes.length)
const randEndpointNum = Math.floor(Math.random() * endpoints.length)
newAttack.attackType = AttackType[attackTypes[randTypeNum]]
newAttack.riskScore = ATTACK_TYPE_TO_RISK_SCORE[newAttack.attackType]
newAttack.description = `${newAttack.attackType} detected.`
newAttack.startTime = randomDate(true)
newAttack.endTime = randomDate()
newAttack.uniqueSessionKey = randomBytes(16).toString("hex")
newAttack.apiEndpointUuid = endpoints[randEndpointNum].uuid
newAttack.host = endpoints[randEndpointNum].host
insertAttacks.push(newAttack)
}
await queryRunner.manager.insert(Attack, insertAttacks)
} catch (err) {
console.error(`Encountered error while generating sample attacks: ${err}`)
} finally {
await queryRunner.release()
}
}

const main = async () => {
const datasource = await AppDataSource.initialize()
if (!datasource.isInitialized) {
console.error("Couldn't initialize datasource...")
return
}
console.log("AppDataSource Initialized...")
const args = yargs.argv
const numAttacks = args["numAttacks"] ?? 20
await generateAttacks(numAttacks)
}

main()
53 changes: 49 additions & 4 deletions backend/src/services/attacks/index.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,80 @@
import { In } from "typeorm"
import { In, FindOptionsWhere } from "typeorm"
import { AttackResponse, GetAttackParams } from "@common/types"
import { AppDataSource } from "data-source"
import Error500InternalServer from "errors/error-500-internal-server"
import { Attack } from "models/attack"
import { FindOptionsWhere } from "typeorm"
import { hasValidLicense } from "utils/license"
import { AttackType } from "@common/enums"

export const getAttacks = async (
getAttackParams: GetAttackParams,
): Promise<AttackResponse> => {
const queryRunner = AppDataSource.createQueryRunner()
await queryRunner.connect()
try {
const validLicense = await hasValidLicense()
if (!validLicense) {
return {
attackTypeCount: {} as Record<AttackType, number>,
attacks: [],
totalAttacks: 0,
totalEndpoints: 0,
validLicense: false,
}
}

const attackRepository = AppDataSource.getRepository(Attack)
const totalEndpointsQb = queryRunner.manager
.createQueryBuilder()
.select([
'CAST(COUNT(DISTINCT("apiEndpointUuid")) AS INTEGER) as "totalEndpoints"',
])
.from(Attack, "attacks")
const attackTypeCountQb = queryRunner.manager
.createQueryBuilder()
.select(['"attackType"', "CAST(COUNT(*) AS INTEGER) as count"])
.from(Attack, "attacks")

let whereConditions: FindOptionsWhere<Attack> = {}

if (getAttackParams?.riskScores) {
whereConditions = {
...whereConditions,
riskScore: In(getAttackParams.riskScores),
}
totalEndpointsQb.where('"riskScore" IN(:...scores)', {
scores: getAttackParams.riskScores,
})
attackTypeCountQb.where('"riskScore" IN(:...scores)', {
scores: getAttackParams.riskScores,
})
}
if (getAttackParams?.hosts) {
whereConditions = {
...whereConditions,
host: In(getAttackParams.hosts),
}
totalEndpointsQb.andWhere("host IN(:...hosts)", {
hosts: getAttackParams.hosts,
})
attackTypeCountQb.andWhere("host IN(:...hosts)", {
hosts: getAttackParams.hosts,
})
}

const resp = await attackRepository.findAndCount({
const totalEndpointsRes = await totalEndpointsQb.getRawOne()
const attackTypeCountRes = await attackTypeCountQb
.groupBy('"attackType"')
.orderBy('"attackType"')
.getRawMany()
const resp = await queryRunner.manager.findAndCount(Attack, {
where: whereConditions,
relations: {
apiEndpoint: true,
},
order: {
riskScore: "DESC",
resolved: "DESC",
startTime: "DESC",
},
skip: getAttackParams?.offset ?? 0,
take: getAttackParams?.limit ?? 10,
Expand All @@ -44,10 +83,16 @@ export const getAttacks = async (
return {
attacks: resp[0],
totalAttacks: resp[1],
totalEndpoints: totalEndpointsRes?.totalEndpoints,
attackTypeCount: Object.fromEntries(
attackTypeCountRes.map(e => [e.attackType, e.count]),
) as any,
validLicense: true,
}
} catch (err) {
console.error(`Error Getting Attacks: ${err}`)
throw new Error500InternalServer(err)
} finally {
await queryRunner.release()
}
}
9 changes: 9 additions & 0 deletions common/src/maps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
DataClass,
RiskScore,
AlertType,
AttackType,
} from "./enums"

export const AWS_NEXT_STEP: Record<AWS_STEPS, AWS_STEPS | null> = {
Expand Down Expand Up @@ -86,3 +87,11 @@ export const ALERT_TYPE_TO_RISK_SCORE: Record<AlertType, RiskScore> = {
[AlertType.BASIC_AUTHENTICATION_DETECTED]: RiskScore.MEDIUM,
[AlertType.UNSECURED_ENDPOINT_DETECTED]: RiskScore.HIGH,
}

export const ATTACK_TYPE_TO_RISK_SCORE: Record<AttackType, RiskScore> = {
[AttackType.HIGH_ERROR_RATE]: RiskScore.HIGH,
[AttackType.ANOMALOUS_CALL_ORDER]: RiskScore.MEDIUM,
[AttackType.BOLA]: RiskScore.HIGH,
[AttackType.HIGH_USAGE_SENSITIVE_ENDPOINT]: RiskScore.HIGH,
[AttackType.UNAUTHENTICATED_ACCESS]: RiskScore.HIGH,
}
4 changes: 4 additions & 0 deletions common/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export interface GetVulnerabilityAggParams {
}

export interface GetAttackParams {
hosts?: string[]
riskScores?: RiskScore[]
offset?: number
limit?: number
Expand Down Expand Up @@ -310,15 +311,18 @@ export interface Attack {
sourceIP: string
apiEndpointUuid: string
apiEndpoint: ApiEndpoint
host: string

resolved: boolean
snoozed: boolean
snoozeHours: number
}

export interface AttackResponse {
attackTypeCount: Record<AttackType, number>
attacks: Attack[]
totalAttacks: number
totalEndpoints: number
validLicense: boolean
}

Expand Down
11 changes: 3 additions & 8 deletions frontend/src/api/attacks/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import axios from "axios"
import { GetAttackParams } from "@common/types"
import { AttackResponse, GetAttackParams } from "@common/types"
import { getAPIURL } from "~/constants"

interface GetAttacksResp {
validLicense: boolean
attacks: any[]
}

export const getAttacks = async (
params: GetAttackParams,
): Promise<GetAttacksResp> => {
const resp = await axios.get<GetAttacksResp>(`${getAPIURL()}/attacks`, {
): Promise<AttackResponse> => {
const resp = await axios.get<AttackResponse>(`${getAPIURL()}/attacks`, {
params,
})
return resp.data
Expand Down
129 changes: 129 additions & 0 deletions frontend/src/components/Protection/AggAttackChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import React from "react"
import {
Chart as ChartJS,
ArcElement,
Tooltip,
Legend,
ChartOptions,
} from "chart.js"
import { Doughnut } from "react-chartjs-2"
import {
HStack,
StackProps,
Grid,
GridItem,
Text,
VStack,
Box,
StackDivider,
} from "@chakra-ui/react"
import { AttackType } from "@common/enums"
import { PIE_BACKGROUND_COLORS, PIE_BORDER_COLORS } from "~/constants"

ChartJS.register(ArcElement, Tooltip, Legend)

interface AggAttackChartProps extends StackProps {
attackTypeCount: Record<AttackType, number>
totalAttacks: number
totalEndpoints: number
}

export const AggAttackChart: React.FC<AggAttackChartProps> = React.memo(
({ totalAttacks, totalEndpoints, attackTypeCount, ...props }) => {
const data = Object.values(attackTypeCount)
const labels = Object.keys(attackTypeCount)
const chartData = {
labels,
datasets: [
{
data,
backgroundColor: PIE_BACKGROUND_COLORS,
borderColor: PIE_BORDER_COLORS,
borderWidth: 1,
},
],
}
const options = {
responsive: true,
cutout: "60%",
plugins: {
legend: {
display: false,
},
tooltip: {
caretSize: 0,
bodyFont: {
size: 11,
},
},
},
} as ChartOptions
return (
<HStack
overflow="hidden"
alignItems="flex-start"
bg="cellBG"
spacing="0"
divider={<StackDivider />}
w="full"
h="60"
>
<VStack divider={<StackDivider />} spacing="0" h="full">
<VStack
bg="cellBG"
py="4"
spacing="1"
px="20"
h="50%"
justifyContent="center"
>
<Text fontSize="xl" fontWeight="semibold" rounded="md">
{totalAttacks}
</Text>
<Text fontSize="sm" fontWeight="medium">
Attacks
</Text>
</VStack>
<VStack
bg="cellBG"
py="4"
spacing="1"
h="50%"
justifyContent="center"
>
<Text fontSize="xl" fontWeight="semibold" rounded="md">
{totalEndpoints}
</Text>
<Text fontSize="sm" fontWeight="medium">
Endpoints
</Text>
</VStack>
</VStack>
<HStack spacing="12" flexGrow="1" alignItems="center" p="4" h="full">
<Box w="220px">
<Doughnut options={options} data={chartData} />
</Box>
<VStack
display={{ base: "none", md: "inherit" }}
flexGrow="1"
alignItems="flex-start"
spacing="4"
h="full"
py="8"
>
<Grid templateColumns="repeat(2, 1fr)" gap="4">
{labels.map((e, i) => (
<GridItem key={i}>
<HStack>
<Box bg={PIE_BACKGROUND_COLORS[i]} px="2" py="1" />
<Text fontSize="sm">{e}</Text>
</HStack>
</GridItem>
))}
</Grid>
</VStack>
</HStack>
</HStack>
)
},
)
Loading

0 comments on commit 0220746

Please sign in to comment.