mihari 5.2.3 → 5.2.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/build_frontend.sh +1 -9
  3. data/frontend/.browserslistrc +3 -0
  4. data/frontend/.eslintrc.js +33 -0
  5. data/frontend/.gitignore +25 -0
  6. data/frontend/README.md +3 -0
  7. data/frontend/babel.config.js +3 -0
  8. data/frontend/index.html +21 -0
  9. data/frontend/jest.config.js +9 -0
  10. data/frontend/package-lock.json +13216 -0
  11. data/frontend/package.json +54 -0
  12. data/frontend/public/favicon.ico +0 -0
  13. data/frontend/scripts/swagger_doc_to_yaml.rb +23 -0
  14. data/frontend/src/App.vue +27 -0
  15. data/frontend/src/api-helper.ts +113 -0
  16. data/frontend/src/api.ts +105 -0
  17. data/frontend/src/components/ErrorMessage.vue +32 -0
  18. data/frontend/src/components/Loading.vue +15 -0
  19. data/frontend/src/components/Navbar.vue +59 -0
  20. data/frontend/src/components/Pagination.vue +126 -0
  21. data/frontend/src/components/alert/Alert.vue +92 -0
  22. data/frontend/src/components/alert/Alerts.vue +66 -0
  23. data/frontend/src/components/alert/AlertsWithPagination.vue +91 -0
  24. data/frontend/src/components/alert/AlertsWrapper.vue +141 -0
  25. data/frontend/src/components/alert/Form.vue +185 -0
  26. data/frontend/src/components/artifact/AS.vue +29 -0
  27. data/frontend/src/components/artifact/Artifact.vue +321 -0
  28. data/frontend/src/components/artifact/ArtifactTag.vue +70 -0
  29. data/frontend/src/components/artifact/ArtifactTags.vue +29 -0
  30. data/frontend/src/components/artifact/ArtifactWrapper.vue +62 -0
  31. data/frontend/src/components/artifact/CPEs.vue +23 -0
  32. data/frontend/src/components/artifact/DnsRecords.vue +38 -0
  33. data/frontend/src/components/artifact/Ports.vue +23 -0
  34. data/frontend/src/components/artifact/ReverseDnsNames.vue +31 -0
  35. data/frontend/src/components/artifact/Tags.vue +29 -0
  36. data/frontend/src/components/artifact/WhoisRecord.vue +49 -0
  37. data/frontend/src/components/config/Configs.vue +68 -0
  38. data/frontend/src/components/config/ConfigsWrapper.vue +40 -0
  39. data/frontend/src/components/link/Link.vue +32 -0
  40. data/frontend/src/components/link/Links.vue +47 -0
  41. data/frontend/src/components/rule/EditRule.vue +74 -0
  42. data/frontend/src/components/rule/EditRuleWrapper.vue +56 -0
  43. data/frontend/src/components/rule/Form.vue +160 -0
  44. data/frontend/src/components/rule/InputForm.vue +80 -0
  45. data/frontend/src/components/rule/NewRule.vue +60 -0
  46. data/frontend/src/components/rule/Rule.vue +108 -0
  47. data/frontend/src/components/rule/RuleWrapper.vue +62 -0
  48. data/frontend/src/components/rule/Rules.vue +88 -0
  49. data/frontend/src/components/rule/RulesWrapper.vue +130 -0
  50. data/frontend/src/components/rule/YAML.vue +47 -0
  51. data/frontend/src/components/tag/Tag.vue +73 -0
  52. data/frontend/src/components/tag/Tags.vue +37 -0
  53. data/frontend/src/countries.ts +350 -0
  54. data/frontend/src/index.ts +23 -0
  55. data/frontend/src/links/anyrun.ts +19 -0
  56. data/frontend/src/links/base.ts +14 -0
  57. data/frontend/src/links/censys.ts +20 -0
  58. data/frontend/src/links/crtsh.ts +20 -0
  59. data/frontend/src/links/dnslytics.ts +38 -0
  60. data/frontend/src/links/greynoise.ts +20 -0
  61. data/frontend/src/links/index.ts +40 -0
  62. data/frontend/src/links/intezer.ts +20 -0
  63. data/frontend/src/links/otx.ts +33 -0
  64. data/frontend/src/links/securitytrails.ts +38 -0
  65. data/frontend/src/links/shodan.ts +20 -0
  66. data/frontend/src/links/urlscan.ts +50 -0
  67. data/frontend/src/links/virustotal.ts +72 -0
  68. data/frontend/src/main.ts +11 -0
  69. data/frontend/src/router/index.ts +57 -0
  70. data/frontend/src/rule.ts +14 -0
  71. data/frontend/src/shims-vue.d.ts +6 -0
  72. data/frontend/src/swagger.yaml +737 -0
  73. data/frontend/src/types.ts +188 -0
  74. data/frontend/src/utils.ts +60 -0
  75. data/frontend/src/views/Alerts.vue +20 -0
  76. data/frontend/src/views/Artifact.vue +44 -0
  77. data/frontend/src/views/Configs.vue +20 -0
  78. data/frontend/src/views/EditRule.vue +44 -0
  79. data/frontend/src/views/NewRule.vue +26 -0
  80. data/frontend/src/views/Rule.vue +44 -0
  81. data/frontend/src/views/Rules.vue +20 -0
  82. data/frontend/tests/unit/utils.spec.ts +7 -0
  83. data/frontend/tsconfig.json +40 -0
  84. data/frontend/vite.config.js +24 -0
  85. data/lefthook.yml +10 -0
  86. data/lib/mihari/analyzers/base.rb +22 -5
  87. data/lib/mihari/analyzers/rule.rb +8 -29
  88. data/lib/mihari/commands/search.rb +16 -7
  89. data/lib/mihari/entities/rule.rb +1 -1
  90. data/lib/mihari/entities/tag.rb +1 -1
  91. data/lib/mihari/schemas/analyzer.rb +2 -7
  92. data/lib/mihari/schemas/rule.rb +1 -1
  93. data/lib/mihari/structs/config.rb +39 -16
  94. data/lib/mihari/structs/rule.rb +1 -1
  95. data/lib/mihari/version.rb +1 -1
  96. data/lib/mihari/web/public/assets/index-ac4e5ffa.js +50 -0
  97. data/lib/mihari/web/public/index.html +1 -1
  98. data/mihari.gemspec +5 -5
  99. metadata +97 -16
  100. data/.gitmodules +0 -0
  101. data/.overcommit.yml +0 -12
  102. data/lib/mihari/web/public/assets/index-cbe1734c.js +0 -50
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "mihari-frontend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "serve": "vite",
7
+ "build": "npx redoc-cli build src/swagger.yaml -o public/redoc-static.html && vite build",
8
+ "test:unit": "jest",
9
+ "lint": "eslint --ext .js,.vue,.ts --ignore-path .gitignore --fix src --fix tests"
10
+ },
11
+ "dependencies": {
12
+ "@fortawesome/fontawesome-free": "^6.4.0",
13
+ "@vueuse/core": "^10.2.0",
14
+ "@vueuse/router": "^10.2.0",
15
+ "axios": "^1.4.0",
16
+ "bulma": "^0.9.4",
17
+ "bulma-helpers": "^0.4.3",
18
+ "dayjs": "^1.11.8",
19
+ "js-sha256": "^0.9.0",
20
+ "truncate": "^3.0.0",
21
+ "ts-dedent": "^2.2.0",
22
+ "url-parse": "^1.5.10",
23
+ "uuidv4": "^6.2.13",
24
+ "vue": "^3.3.4",
25
+ "vue-concurrency": "4.0.1",
26
+ "vue-json-pretty": "^2.2.4",
27
+ "vue-prism-editor": "^2.0.0-alpha.2",
28
+ "vue-router": "^4.2.2"
29
+ },
30
+ "devDependencies": {
31
+ "@types/jest": "29.5.2",
32
+ "@types/prismjs": "^1.26.0",
33
+ "@types/url-parse": "^1.4.8",
34
+ "@typescript-eslint/eslint-plugin": "^5.59.11",
35
+ "@typescript-eslint/parser": "^5.59.11",
36
+ "@vitejs/plugin-vue": "^4.2.3",
37
+ "@vue/eslint-config-typescript": "^11.0.3",
38
+ "@vue/test-utils": "2.3.2",
39
+ "@vue/vue3-jest": "^29.2.4",
40
+ "eslint": "^8.43.0",
41
+ "eslint-config-prettier": "^8.8.0",
42
+ "eslint-plugin-prettier": "^4.2.1",
43
+ "eslint-plugin-simple-import-sort": "^10.0.0",
44
+ "eslint-plugin-vue": "^9.14.1",
45
+ "handlebars": "^4.7.7",
46
+ "husky": "^8.0.3",
47
+ "prettier": "^2.8.8",
48
+ "redoc": "2.0.0",
49
+ "redoc-cli": "^0.13.21",
50
+ "ts-jest": "^29.1.0",
51
+ "typescript": "~5.1.3",
52
+ "vite": "^4.3.9"
53
+ }
54
+ }
Binary file
@@ -0,0 +1,23 @@
1
+ require "http"
2
+ require "json"
3
+ require "yaml"
4
+
5
+ def recursive_delete(hash, to_remove)
6
+ hash.delete(to_remove)
7
+ hash.each_value do |value|
8
+ recursive_delete(value, to_remove) if value.is_a? Hash
9
+ end
10
+ end
11
+
12
+ res = HTTP.get("http://localhost:9292/api/swagger_doc")
13
+ json = JSON.parse(res.body.to_s)
14
+
15
+ # remove host and operationId because
16
+ # - host: can be varied
17
+ # - operationId: is useless (to me)
18
+ keys_to_remove = ["host", "operationId"]
19
+ keys_to_remove.each do |key|
20
+ recursive_delete json, key
21
+ end
22
+
23
+ puts json.to_yaml
@@ -0,0 +1,27 @@
1
+ <template>
2
+ <Navbar></Navbar>
3
+ <section class="section is-medium">
4
+ <div class="container">
5
+ <router-view />
6
+ </div>
7
+ </section>
8
+ </template>
9
+
10
+ <script lang="ts">
11
+ import { defineComponent } from "vue";
12
+
13
+ import Navbar from "@/components/Navbar.vue";
14
+
15
+ export default defineComponent({
16
+ name: "App",
17
+ components: {
18
+ Navbar,
19
+ },
20
+ });
21
+ </script>
22
+
23
+ <style>
24
+ table.is-fullwidth th {
25
+ width: 120px;
26
+ }
27
+ </style>
@@ -0,0 +1,113 @@
1
+ import { Task, useAsyncTask } from "vue-concurrency";
2
+
3
+ import { API } from "@/api";
4
+ import {
5
+ Alerts,
6
+ AlertSearchParams,
7
+ ArtifactWithTags,
8
+ Config,
9
+ CreateRule,
10
+ IPInfo,
11
+ Rule,
12
+ Rules,
13
+ RuleSearchParams,
14
+ UpdateRule,
15
+ } from "@/types";
16
+
17
+ export function generateGetAlertsTask(): Task<Alerts, [AlertSearchParams]> {
18
+ return useAsyncTask<Alerts, [AlertSearchParams]>(async (_signal, params) => {
19
+ return await API.getAlerts(params);
20
+ });
21
+ }
22
+
23
+ export function generateDeleteAlertTask(): Task<void, [string]> {
24
+ return useAsyncTask<void, [string]>(async (_signal, id) => {
25
+ return await API.deleteAlert(id);
26
+ });
27
+ }
28
+
29
+ export function generateGetTagsTask(): Task<string[], []> {
30
+ return useAsyncTask<string[], []>(async () => {
31
+ return await API.getTags();
32
+ });
33
+ }
34
+
35
+ export function generateDeleteTagTask(): Task<void, [string]> {
36
+ return useAsyncTask<void, [string]>(async (_signal, tag) => {
37
+ return await API.deleteTag(tag);
38
+ });
39
+ }
40
+
41
+ export function generateGetRuleSetTask(): Task<string[], []> {
42
+ return useAsyncTask<string[], []>(async () => {
43
+ return await API.getRuleSet();
44
+ });
45
+ }
46
+
47
+ export function generateGetArtifactTask(): Task<ArtifactWithTags, [string]> {
48
+ return useAsyncTask<ArtifactWithTags, [string]>(async (_signal, id) => {
49
+ return await API.getArtifact(id);
50
+ });
51
+ }
52
+
53
+ export function generateDeleteArtifactTask(): Task<void, [string]> {
54
+ return useAsyncTask<void, [string]>(async (_signal, id) => {
55
+ return await API.deleteArtifact(id);
56
+ });
57
+ }
58
+
59
+ export function generateEnrichArtifactTask(): Task<void, [string]> {
60
+ return useAsyncTask<void, [string]>(async (_signal, id) => {
61
+ return await API.enrichArtifact(id);
62
+ });
63
+ }
64
+
65
+ export function generateGetConfigsTask(): Task<Config[], []> {
66
+ return useAsyncTask<Config[], []>(async () => {
67
+ return await API.getConfigs();
68
+ });
69
+ }
70
+
71
+ export function generateGetIPTask(): Task<IPInfo, [string]> {
72
+ return useAsyncTask<IPInfo, [string]>(async (_signal, ipAddress: string) => {
73
+ return await API.getIPInfo(ipAddress);
74
+ });
75
+ }
76
+
77
+ export function generateGetRulesTask(): Task<Rules, [RuleSearchParams]> {
78
+ return useAsyncTask<Rules, [RuleSearchParams]>(
79
+ async (_signal, params: RuleSearchParams) => {
80
+ return await API.getRules(params);
81
+ }
82
+ );
83
+ }
84
+
85
+ export function generateGetRuleTask(): Task<Rule, [string]> {
86
+ return useAsyncTask<Rule, [string]>(async (_signal, id: string) => {
87
+ return await API.getRule(id);
88
+ });
89
+ }
90
+
91
+ export function generateDeleteRuleTask(): Task<void, [string]> {
92
+ return useAsyncTask<void, [string]>(async (_signal, id: string) => {
93
+ return await API.deleteRule(id);
94
+ });
95
+ }
96
+
97
+ export function generateRunRuleTask(): Task<void, [string]> {
98
+ return useAsyncTask<void, [string]>(async (_signal, id) => {
99
+ return await API.runRule(id);
100
+ });
101
+ }
102
+
103
+ export function generateCreateRuleTask(): Task<Rule, [CreateRule]> {
104
+ return useAsyncTask<Rule, [CreateRule]>(async (_signal, payload) => {
105
+ return await API.createRule(payload);
106
+ });
107
+ }
108
+
109
+ export function generateUpdateRuleTask(): Task<Rule, [UpdateRule]> {
110
+ return useAsyncTask<Rule, [UpdateRule]>(async (_signal, payload) => {
111
+ return await API.updateRule(payload);
112
+ });
113
+ }
@@ -0,0 +1,105 @@
1
+ import axios from "axios";
2
+
3
+ import {
4
+ Alerts,
5
+ AlertSearchParams,
6
+ ArtifactWithTags,
7
+ Config,
8
+ CreateRule,
9
+ IPInfo,
10
+ Rule,
11
+ Rules,
12
+ RuleSearchParams,
13
+ RuleSet,
14
+ Tags,
15
+ UpdateRule,
16
+ } from "@/types";
17
+
18
+ const client = axios.create({
19
+ headers: {
20
+ Accept: "application/json",
21
+ },
22
+ });
23
+
24
+ export const API = {
25
+ async getConfigs(): Promise<Config[]> {
26
+ const res = await client.get<Config[]>("/api/configs");
27
+ return res.data;
28
+ },
29
+
30
+ async getAlerts(params: AlertSearchParams): Promise<Alerts> {
31
+ params.page = params.page || 1;
32
+ const res = await client.get<Alerts>("/api/alerts", {
33
+ params: params,
34
+ });
35
+ return res.data;
36
+ },
37
+
38
+ async getTags(): Promise<string[]> {
39
+ const res = await client.get<Tags>("/api/tags");
40
+ return res.data.tags;
41
+ },
42
+
43
+ async getRuleSet(): Promise<string[]> {
44
+ const res = await client.get<RuleSet>("/api/rules/ids");
45
+ return res.data.ruleIds;
46
+ },
47
+
48
+ async deleteAlert(id: string): Promise<void> {
49
+ await client.delete(`/api/alerts/${id}`);
50
+ },
51
+
52
+ async getArtifact(id: string): Promise<ArtifactWithTags> {
53
+ const res = await client.get(`/api/artifacts/${id}`);
54
+ return res.data;
55
+ },
56
+
57
+ async enrichArtifact(id: string): Promise<void> {
58
+ await client.get(`/api/artifacts/${id}/enrich`);
59
+ return;
60
+ },
61
+
62
+ async deleteArtifact(id: string): Promise<void> {
63
+ await client.delete(`/api/artifacts/${id}`);
64
+ },
65
+
66
+ async getRules(params: RuleSearchParams): Promise<Rules> {
67
+ params.page = params.page || 1;
68
+ const res = await client.get<Rules>("/api/rules", {
69
+ params: params,
70
+ });
71
+ return res.data;
72
+ },
73
+
74
+ async getRule(id: string): Promise<Rule> {
75
+ const res = await client.get<Rule>(`/api/rules/${id}`);
76
+ return res.data;
77
+ },
78
+
79
+ async runRule(id: string): Promise<void> {
80
+ await client.get<void>(`/api/rules/${id}/run`);
81
+ },
82
+
83
+ async createRule(payload: CreateRule): Promise<Rule> {
84
+ const res = await client.post<Rule>("/api/rules/", payload);
85
+ return res.data;
86
+ },
87
+
88
+ async updateRule(payload: UpdateRule): Promise<Rule> {
89
+ const res = await client.put<Rule>("/api/rules/", payload);
90
+ return res.data;
91
+ },
92
+
93
+ async deleteRule(id: string): Promise<void> {
94
+ await client.delete<void>(`/api/rules/${id}`);
95
+ },
96
+
97
+ async deleteTag(name: string): Promise<void> {
98
+ await client.delete(`/api/tags/${name}`);
99
+ },
100
+
101
+ async getIPInfo(ipAddress: string): Promise<IPInfo> {
102
+ const res = await client.get<IPInfo>(`/api/ip_addresses/${ipAddress}`);
103
+ return res.data;
104
+ },
105
+ };
@@ -0,0 +1,32 @@
1
+ <template>
2
+ <div class="notification is-danger is-light">
3
+ <p v-if="error.response.data?.message">{{ error.response.data.message }}</p>
4
+ <p v-else>{{ error }}</p>
5
+ </div>
6
+
7
+ <article class="message" v-if="error.response.data?.details">
8
+ <div class="message-body">
9
+ <VueJsonPretty :data="error.response.data.details"></VueJsonPretty>
10
+ </div>
11
+ </article>
12
+ </template>
13
+
14
+ <script lang="ts">
15
+ import "vue-json-pretty/lib/styles.css";
16
+
17
+ import { defineComponent } from "vue";
18
+ import VueJsonPretty from "vue-json-pretty";
19
+
20
+ export default defineComponent({
21
+ name: "ErrorItem",
22
+ props: {
23
+ error: {
24
+ type: Object,
25
+ required: true,
26
+ },
27
+ },
28
+ components: {
29
+ VueJsonPretty,
30
+ },
31
+ });
32
+ </script>
@@ -0,0 +1,15 @@
1
+ <template>
2
+ <div class="has-text-centered">
3
+ <div class="fa-3x">
4
+ <i class="fas fa-spinner fa-spin"></i>
5
+ </div>
6
+ </div>
7
+ </template>
8
+
9
+ <script lang="ts">
10
+ import { defineComponent } from "vue";
11
+
12
+ export default defineComponent({
13
+ name: "LoadingItem",
14
+ });
15
+ </script>
@@ -0,0 +1,59 @@
1
+ <template>
2
+ <nav
3
+ role="navigation"
4
+ aria-label="main navigation"
5
+ class="navbar is-fixed-top"
6
+ >
7
+ <div class="navbar-brand">
8
+ <a class="navbar-item"><h1 class="title">Mihari</h1></a
9
+ ><a role="button" aria-label="menu" class="navbar-burger burger"
10
+ ><span aria-hidden="true"></span><span aria-hidden="true"></span
11
+ ><span aria-hidden="true"></span
12
+ ></a>
13
+ </div>
14
+ <div class="navbar-menu">
15
+ <div class="navbar-start"></div>
16
+ <div class="navbar-end">
17
+ <router-link class="navbar-item" :to="{ name: 'Alerts' }"
18
+ >Alerts</router-link
19
+ >
20
+ <router-link class="navbar-item" :to="{ name: 'NewRule' }"
21
+ >New rule</router-link
22
+ >
23
+ <router-link class="navbar-item" :to="{ name: 'Rules' }"
24
+ >Rules</router-link
25
+ >
26
+ <router-link class="navbar-item" :to="{ name: 'Configs' }"
27
+ >Configs</router-link
28
+ >
29
+ <a class="navbar-item"
30
+ ><a href="/redoc-static.html" target="_blank" class="navbar-item"
31
+ >API</a
32
+ ></a
33
+ >
34
+ <a class="navbar-item"
35
+ ><a
36
+ href="https://github.com/ninoseki/mihari"
37
+ target="_blank"
38
+ class="navbar-item"
39
+ >GitHub</a
40
+ ></a
41
+ >
42
+ </div>
43
+ </div>
44
+ </nav>
45
+ </template>
46
+
47
+ <script lang="ts">
48
+ import { defineComponent } from "vue";
49
+
50
+ export default defineComponent({
51
+ name: "NavbarItem",
52
+ });
53
+ </script>
54
+
55
+ <style scoped>
56
+ .navbar {
57
+ border-bottom: 1px solid lightgray;
58
+ }
59
+ </style>
@@ -0,0 +1,126 @@
1
+ <template>
2
+ <article class="message is-warning" v-if="total === 0">
3
+ <div class="message-body">There is no result to show.</div>
4
+ </article>
5
+ <nav class="pagination" role="navigation" aria-label="pagination" v-else>
6
+ <ul class="pagination-list" v-if="hasOnlyOnePage">
7
+ <li>
8
+ <a class="pagination-link mt-2 is-current" @click="updatePage(1)">1</a>
9
+ </li>
10
+ </ul>
11
+ <ul class="pagination-list" v-else>
12
+ <li v-if="hasPreviousPage && isPreviousPageNotFirst">
13
+ <a class="pagination-link mt-2" @click="updatePage(1)"> 1</a>
14
+ </li>
15
+ <li v-if="hasPreviousPage && isPreviousPageNotFirst">
16
+ <span class="pagination-ellipsis">&hellip;</span>
17
+ </li>
18
+ <li v-if="hasPreviousPage">
19
+ <a class="pagination-link mt-2" @click="updatePage(currentPage - 1)">
20
+ {{ currentPage - 1 }}</a
21
+ >
22
+ </li>
23
+ <li>
24
+ <a
25
+ class="pagination-link mt-2 is-current"
26
+ @click="updatePage(currentPage)"
27
+ >
28
+ {{ currentPage }}</a
29
+ >
30
+ </li>
31
+ <li v-if="hasNextPage">
32
+ <a class="pagination-link mt-2" @click="updatePage(currentPage + 1)">
33
+ {{ currentPage + 1 }}</a
34
+ >
35
+ </li>
36
+ <li v-if="hasNextPage && isNextPageNotLast">
37
+ <span class="pagination-ellipsis">&hellip;</span>
38
+ </li>
39
+ <li v-if="hasNextPage && isNextPageNotLast">
40
+ <a class="pagination-link mt-2" @click="updatePage(totalPageCount)">{{
41
+ totalPageCount
42
+ }}</a>
43
+ </li>
44
+ </ul>
45
+ </nav>
46
+ </template>
47
+
48
+ <script lang="ts">
49
+ import { useRouteQuery } from "@vueuse/router";
50
+ import { computed, defineComponent, onMounted, Ref } from "vue";
51
+ import { useRoute, useRouter } from "vue-router";
52
+
53
+ export default defineComponent({
54
+ name: "AlertsPagination",
55
+ props: {
56
+ currentPage: {
57
+ type: Number,
58
+ required: true,
59
+ },
60
+ pageSize: {
61
+ type: Number,
62
+ required: true,
63
+ },
64
+ total: {
65
+ type: Number,
66
+ required: true,
67
+ },
68
+ },
69
+ emits: ["update-page"],
70
+ setup(props, context) {
71
+ const route = useRoute();
72
+ const router = useRouter();
73
+ const options = { route, router };
74
+
75
+ const totalPageCount = computed(() => {
76
+ return Math.ceil(props.total / props.pageSize);
77
+ });
78
+
79
+ const hasOnlyOnePage = computed(() => {
80
+ return totalPageCount.value === 1;
81
+ });
82
+
83
+ const hasPreviousPage = computed(() => {
84
+ return props.currentPage > 1;
85
+ });
86
+
87
+ const isPreviousPageNotFirst = computed(() => {
88
+ return props.currentPage - 1 !== 1;
89
+ });
90
+
91
+ const hasNextPage = computed(() => {
92
+ return props.currentPage < totalPageCount.value;
93
+ });
94
+
95
+ const isNextPageNotLast = computed(() => {
96
+ return props.currentPage + 1 !== totalPageCount.value;
97
+ });
98
+
99
+ const updatePage = (page: number) => {
100
+ const pageQuery = useRouteQuery("page", page.toString(), options);
101
+ pageQuery.value = page.toString();
102
+
103
+ context.emit("update-page", page);
104
+ };
105
+
106
+ onMounted(() => {
107
+ const pageQuery = useRouteQuery("page", null, options) as Ref<
108
+ string | null
109
+ >;
110
+ if (pageQuery.value && parseInt(pageQuery.value) !== props.currentPage) {
111
+ updatePage(parseInt(pageQuery.value));
112
+ }
113
+ });
114
+
115
+ return {
116
+ updatePage,
117
+ hasNextPage,
118
+ hasOnlyOnePage,
119
+ hasPreviousPage,
120
+ isNextPageNotLast,
121
+ isPreviousPageNotFirst,
122
+ totalPageCount,
123
+ };
124
+ },
125
+ });
126
+ </script>
@@ -0,0 +1,92 @@
1
+ <template>
2
+ <div class="box">
3
+ <table class="table is-fullwidth is-completely-borderless">
4
+ <tr>
5
+ <th>ID</th>
6
+ <td>
7
+ {{ alert.id }}
8
+ <button
9
+ class="button is-light is-small is-pulled-right"
10
+ @click="deleteAlert"
11
+ >
12
+ <span>Delete</span>
13
+ <span class="icon is-small">
14
+ <i class="fas fa-times"></i>
15
+ </span>
16
+ </button>
17
+ </td>
18
+ </tr>
19
+ <tr>
20
+ <th>Rule</th>
21
+ <td>
22
+ <router-link :to="{ name: 'Rule', params: { id: alert.ruleId } }">{{
23
+ alert.ruleId
24
+ }}</router-link>
25
+ </td>
26
+ </tr>
27
+ <tr>
28
+ <th>Artifacts</th>
29
+ <td>
30
+ <Artifacts :artifacts="alert.artifacts"></Artifacts>
31
+ </td>
32
+ </tr>
33
+ <tr v-if="alert.tags.length > 0">
34
+ <th>Tags</th>
35
+ <td>
36
+ <Tags :tags="alert.tags" @update-tag="updateTag"></Tags>
37
+ </td>
38
+ </tr>
39
+ </table>
40
+ <p class="help">Created at: {{ alert.createdAt }}</p>
41
+ </div>
42
+ </template>
43
+
44
+ <script lang="ts">
45
+ import { defineComponent, PropType } from "vue";
46
+
47
+ import { generateDeleteAlertTask } from "@/api-helper";
48
+ import Artifacts from "@/components/artifact/ArtifactTags.vue";
49
+ import Tags from "@/components/tag/Tags.vue";
50
+ import { Alert } from "@/types";
51
+ import { getHumanizedRelativeTime, getLocalDatetime } from "@/utils";
52
+
53
+ export default defineComponent({
54
+ name: "AlertItem",
55
+ components: {
56
+ Artifacts,
57
+ Tags,
58
+ },
59
+ props: {
60
+ alert: {
61
+ type: Object as PropType<Alert>,
62
+ required: true,
63
+ },
64
+ },
65
+ setup(props, context) {
66
+ const updateTag = (tag: string) => {
67
+ context.emit("update-tag", tag);
68
+ };
69
+
70
+ const deleteAlertTask = generateDeleteAlertTask();
71
+
72
+ const deleteAlert = async () => {
73
+ const result = window.confirm(
74
+ `Are you sure you want to delete ${props.alert.id}?`
75
+ );
76
+
77
+ if (result) {
78
+ await deleteAlertTask.perform(props.alert.id);
79
+ // refresh the page
80
+ context.emit("refresh-page");
81
+ }
82
+ };
83
+
84
+ return {
85
+ updateTag,
86
+ deleteAlert,
87
+ getLocalDatetime,
88
+ getHumanizedRelativeTime,
89
+ };
90
+ },
91
+ });
92
+ </script>