mihari 5.2.3 → 5.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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>