Threat intel (#106)
* added Threat Intel api * added Threat Intel component * precommit fixes --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>
taylor_socfortress committed
Dec 11, 2023 at 07:57 UTC
f163e8b8d3de3d3f7b12296085d52d52119d5173
6 files changed
+233
-12
package.json
+11
-11
@@ -30,9 +30,9 @@
30
"dependencies": {
31
"@ajoelp/json-to-formdata": "^1.5.0",
32
"@fawmi/vue-google-maps": "^0.9.79",
33
- "@fontsource/jetbrains-mono": "^5.0.17",
34
- "@fontsource/lexend": "^5.0.17",
35
- "@fontsource/public-sans": "^5.0.15",
33
+ "@fontsource/jetbrains-mono": "^5.0.18",
34
+ "@fontsource/lexend": "^5.0.18",
35
+ "@fontsource/public-sans": "^5.0.16",
36
"@fullcalendar/core": "^6.1.10",
37
"@fullcalendar/daygrid": "^6.1.10",
38
"@fullcalendar/interaction": "^6.1.10",
@@ -85,7 +85,7 @@
85
"shepherd.js": "^11.2.0",
86
"v-calendar": "^3.1.2",
87
"validator": "^13.11.0",
88
- "vue": "^3.3.10",
88
+ "vue": "^3.3.11",
89
"vue-advanced-cropper": "^2.8.8",
90
"vue-cal": "^4.8.1",
91
"vue-chartjs": "^5.2.0",
@@ -111,9 +111,9 @@
111
"@types/inquirer": "^9.0.7",
112
"@types/jsdom": "^21.1.6",
113
"@types/lodash": "^4.14.202",
114
- "@types/node": "^20.10.3",
114
+ "@types/node": "^20.10.4",
115
"@types/validator": "^13.11.7",
116
- "@vitejs/plugin-vue": "^4.5.1",
116
+ "@vitejs/plugin-vue": "^4.5.2",
117
"@vitejs/plugin-vue-jsx": "^3.1.0",
118
"@vue-leaflet/vue-leaflet": "^0.10.1",
119
"@vue/eslint-config-prettier": "^8.0.0",
@@ -132,18 +132,18 @@
132
"npm-run-all": "^4.1.5",
133
"picocolors": "^1.0.0",
134
"postcss": "^8.4.32",
135
- "prettier": "^3.1.0",
135
+ "prettier": "^3.1.1",
136
"sass": "^1.69.5",
137
"start-server-and-test": "^2.0.3",
138
"tailwind-config-viewer": "^1.7.3",
139
"tailwindcss": "^3.3.6",
140
"taze": "^0.13.0",
141
- "ts-node": "^10.9.1",
142
- "typescript": "~5.3.2",
141
+ "ts-node": "^10.9.2",
142
+ "typescript": "~5.3.3",
143
"unplugin-vue-components": "^0.26.0",
144
- "vite": "^5.0.6",
144
+ "vite": "^5.0.7",
145
"vite-svg-loader": "^5.1.0",
146
- "vitest": "^1.0.1",
146
+ "vitest": "^1.0.4",
147
"vue-tsc": "^1.8.25"
148
},
149
"engines": {
src/api/index.ts
+3
-1
@@ -7,6 +7,7 @@ import graylog from "./graylog"
7
import indices from "./indices"
8
import soc from "./soc"
9
import healthchecks from "./healthchecks"
10
+import threatIntel from "./threatIntel"
11
12
export default {
13
agents,
@@ -17,5 +18,6 @@ export default {
18
graylog,
19
indices,
20
soc,
20
- healthchecks
21
+ healthchecks,
22
+ threatIntel
23
}
src/api/threatIntel.ts
new
+12
@@ -0,0 +1,12 @@
1
+import { HttpClient } from "./httpClient"
2
+import type { FlaskBaseResponse } from "@/types/flask.d"
3
+import type { ThreatIntelResponse } from "@/types/threatIntel.d"
4
+
5
+export default {
6
+ create(iocValue: string) {
7
+ const body = {
8
+ ioc_value: iocValue
9
+ }
10
+ return HttpClient.post<FlaskBaseResponse & { data: ThreatIntelResponse }>(`/threat_intel/socfortress`, body)
11
+ }
12
+}
src/components/alerts/AlertsList.vue
+21
@@ -37,6 +37,7 @@
37
</template>
38
Filters
39
</n-button>
40
+ <n-button size="small" type="primary" @click="showThreatIntelDrawer = true">Threat Intel</n-button>
41
</div>
42
</div>
43
<n-spin :show="loading">
@@ -57,6 +58,18 @@
58
</div>
59
</n-spin>
60
61
+ <n-drawer
62
+ v-model:show="showThreatIntelDrawer"
63
+ :width="500"
64
+ style="max-width: 90vw"
65
+ :trap-focus="false"
66
+ display-directive="show"
67
+ >
68
+ <n-drawer-content title="SOCFortress Threat Intel" closable :native-scrollbar="false">
69
+ <ThreatIntel @mounted="threatIntelCTX = $event" />
70
+ </n-drawer-content>
71
+ </n-drawer>
72
+
73
<n-drawer
74
v-model:show="showStatsDrawer"
75
:width="700"
@@ -132,6 +145,7 @@
145
import { ref, onBeforeMount, toRefs, computed, nextTick, onMounted } from "vue"
146
import { useMessage, NSpin, NPopover, NButton, NEmpty, NDrawer, NDrawerContent, NSelect } from "naive-ui"
147
import Api from "@/api"
148
+import ThreatIntel from "./ThreatIntel.vue"
149
import AlertsStats, { type AlertsStatsCTX } from "./AlertsStats.vue"
150
import AlertsFilters from "./AlertsFilters.vue"
151
import AlertsSummaryItem, { type AlertsSummaryExt } from "./AlertsSummary.vue"
@@ -143,6 +157,7 @@ import type { IndexStats } from "@/types/indices.d"
157
import axios from "axios"
158
import type { Agent } from "@/types/agents.d"
159
import { onBeforeUnmount } from "vue"
160
+import { watch } from "vue"
161
162
const props = defineProps<{ agentHostname?: string; indexName?: string }>()
163
const { agentHostname, indexName } = toRefs(props)
@@ -157,6 +172,8 @@ const alertsSummaryList = ref<AlertsSummaryExt[]>([])
172
const loadingFilters = ref(true)
173
const showFiltersDrawer = ref(true)
174
const showStatsDrawer = ref(false)
175
+const showThreatIntelDrawer = ref(false)
176
+const threatIntelCTX = ref<{ restore: () => void } | null>(null)
177
let abortController: AbortController | null = null
178
179
const InfoIcon = "carbon:information"
@@ -318,6 +335,10 @@ function cancelSearch() {
335
abortController?.abort()
336
}
337
338
+watch(showThreatIntelDrawer, () => {
339
+ threatIntelCTX.value?.restore()
340
+})
341
+
342
onBeforeMount(() => {
343
if (agentHostname?.value) {
344
filters.value.agentHostname = agentHostname.value
src/components/alerts/ThreatIntel.vue
new
+176
@@ -0,0 +1,176 @@
1
+<template>
2
+ <n-spin :show="loading">
3
+ <div class="flex flex-col gap-3">
4
+ <div class="flex flex-col gap-1">
5
+ <small class="ml-2">IOC Value:</small>
6
+ <n-input v-model:value.trim="iocValue" placeholder="IPv4, domain, or SHA256 hash" clearable />
7
+ </div>
8
+ <div class="flex justify-end">
9
+ <n-button type="primary" :disabled="!isValid" @click="create()">Submit</n-button>
10
+ </div>
11
+ <div class="response" :class="{ error }" v-if="error || !!response">
12
+ <div class="message" v-if="error">
13
+ {{ error }}
14
+ </div>
15
+ <div v-else class="list">
16
+ <div class="item">
17
+ <div class="key">type</div>
18
+ <div class="value">{{ response?.type || "-" }}</div>
19
+ </div>
20
+ <div class="item">
21
+ <div class="key">value</div>
22
+ <div class="value">{{ response?.value || "-" }}</div>
23
+ </div>
24
+ <div class="item">
25
+ <div class="key">ioc_source</div>
26
+ <div class="value">{{ response?.ioc_source || "-" }}</div>
27
+ </div>
28
+ <div class="item">
29
+ <div class="key">comment</div>
30
+ <div class="value">{{ response?.comment || "-" }}</div>
31
+ </div>
32
+ <div class="item">
33
+ <div class="key">score</div>
34
+ <div class="value">{{ response?.score || "-" }}</div>
35
+ </div>
36
+ <div class="item">
37
+ <div class="key">timestamp</div>
38
+ <div class="value">{{ response?.timestamp ? formatDate(response.timestamp) : "-" }}</div>
39
+ </div>
40
+ <div class="item">
41
+ <div class="key">report_url</div>
42
+ <div class="value">
43
+ <a :href="response.report_url" target="_blank" v-if="response?.report_url">
44
+ {{ response.report_url }}
45
+ </a>
46
+ <span v-else>-</span>
47
+ </div>
48
+ </div>
49
+ <div class="item">
50
+ <div class="key">virustotal_url</div>
51
+ <div class="value">
52
+ <a :href="response.virustotal_url" target="_blank" v-if="response?.virustotal_url">
53
+ {{ response.virustotal_url }}
54
+ </a>
55
+ <span v-else>-</span>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ </n-spin>
62
+</template>
63
+
64
+<script setup lang="ts">
65
+import { ref, computed, onMounted } from "vue"
66
+import { useMessage, NSpin, NButton, NInput } from "naive-ui"
67
+import Api from "@/api"
68
+import _trim from "lodash/trim"
69
+import _toNumber from "lodash/toNumber"
70
+import type { ThreatIntelResponse } from "@/types/threatIntel.d"
71
+import { useSettingsStore } from "@/stores/settings"
72
+import dayjs from "@/utils/dayjs"
73
+
74
+const emit = defineEmits<{
75
+ (
76
+ e: "mounted",
77
+ value: {
78
+ restore: () => void
79
+ }
80
+ ): void
81
+}>()
82
+
83
+const message = useMessage()
84
+const dFormats = useSettingsStore().dateFormat
85
+
86
+const loading = ref(false)
87
+const iocValue = ref<string>("")
88
+const response = ref<ThreatIntelResponse | null>(null)
89
+const error = ref<string>("")
90
+
91
+const isValid = computed(() => {
92
+ return !!_trim(iocValue.value)
93
+})
94
+
95
+function clear() {
96
+ iocValue.value = ""
97
+}
98
+
99
+function restore() {
100
+ clear()
101
+ loading.value = false
102
+ response.value = null
103
+ error.value = ""
104
+}
105
+
106
+const formatDate = (date: string) => {
107
+ const datejs = dayjs(_toNumber(date) * 1000)
108
+ if (!datejs.isValid()) return date
109
+
110
+ return datejs.format(dFormats.datetime)
111
+}
112
+
113
+function create() {
114
+ loading.value = true
115
+
116
+ Api.threatIntel
117
+ .create(iocValue.value)
118
+ .then(res => {
119
+ if (res.data.success) {
120
+ error.value = ""
121
+ response.value = res.data.data
122
+ clear()
123
+ message.success(res.data?.message || "SOCFortress Threat Intel submitted.")
124
+ } else {
125
+ error.value = res.data?.message || "An error occurred. Please try again later."
126
+ message.warning(res.data?.message || "An error occurred. Please try again later.")
127
+ }
128
+ })
129
+ .catch(err => {
130
+ error.value = err.response?.data?.message || "An error occurred. Please try again later."
131
+ message.error(err.response?.data?.message || "An error occurred. Please try again later.")
132
+ })
133
+ .finally(() => {
134
+ loading.value = false
135
+ })
136
+}
137
+
138
+onMounted(() => {
139
+ emit("mounted", {
140
+ restore
141
+ })
142
+})
143
+</script>
144
+
145
+<style scoped lang="scss">
146
+.response {
147
+ background-color: var(--bg-secondary-color);
148
+ border-radius: var(--border-radius);
149
+ border: 1px solid var(--success-color);
150
+
151
+ .message {
152
+ padding: 10px 16px;
153
+ }
154
+
155
+ .list {
156
+ .item {
157
+ padding: 10px 16px;
158
+
159
+ .key {
160
+ color: var(--fg-secondary-color);
161
+ font-size: 12px;
162
+ margin-bottom: 2px;
163
+ font-family: var(--font-family-mono);
164
+ }
165
+
166
+ &:not(:last-child) {
167
+ border-bottom: var(--border-small-100);
168
+ }
169
+ }
170
+ }
171
+
172
+ &.error {
173
+ border-color: var(--error-color);
174
+ }
175
+}
176
+</style>
src/types/threatIntel.d.ts
new
+10
@@ -0,0 +1,10 @@
1
+export interface ThreatIntelResponse {
2
+ comment: string | null
3
+ ioc_source: string
4
+ report_url: string | null
5
+ score: string | null
6
+ timestamp: string | null
7
+ type: string | null
8
+ value: string | null
9
+ virustotal_url: string | null
10
+}