mihari 5.2.2 → 5.2.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (130) 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/binaryedge.rb +0 -1
  88. data/lib/mihari/analyzers/censys.rb +7 -2
  89. data/lib/mihari/analyzers/circl.rb +1 -1
  90. data/lib/mihari/analyzers/passivetotal.rb +1 -1
  91. data/lib/mihari/analyzers/rule.rb +43 -73
  92. data/lib/mihari/analyzers/virustotal_intelligence.rb +1 -2
  93. data/lib/mihari/clients/base.rb +1 -1
  94. data/lib/mihari/commands/database.rb +12 -11
  95. data/lib/mihari/commands/rule.rb +47 -45
  96. data/lib/mihari/commands/search.rb +73 -45
  97. data/lib/mihari/commands/version.rb +8 -6
  98. data/lib/mihari/commands/web.rb +26 -23
  99. data/lib/mihari/emitters/base.rb +14 -1
  100. data/lib/mihari/emitters/database.rb +3 -10
  101. data/lib/mihari/emitters/misp.rb +16 -5
  102. data/lib/mihari/emitters/slack.rb +13 -15
  103. data/lib/mihari/emitters/the_hive.rb +17 -19
  104. data/lib/mihari/emitters/webhook.rb +23 -23
  105. data/lib/mihari/enrichers/whois.rb +1 -0
  106. data/lib/mihari/entities/rule.rb +1 -1
  107. data/lib/mihari/entities/tag.rb +1 -1
  108. data/lib/mihari/feed/parser.rb +1 -0
  109. data/lib/mihari/feed/reader.rb +29 -14
  110. data/lib/mihari/mixins/configurable.rb +13 -4
  111. data/lib/mihari/schemas/analyzer.rb +2 -7
  112. data/lib/mihari/schemas/rule.rb +1 -1
  113. data/lib/mihari/structs/censys.rb +96 -82
  114. data/lib/mihari/structs/config.rb +46 -21
  115. data/lib/mihari/structs/google_public_dns.rb +27 -23
  116. data/lib/mihari/structs/greynoise.rb +44 -38
  117. data/lib/mihari/structs/onyphe.rb +34 -30
  118. data/lib/mihari/structs/rule.rb +1 -1
  119. data/lib/mihari/structs/shodan.rb +77 -69
  120. data/lib/mihari/structs/urlscan.rb +42 -36
  121. data/lib/mihari/structs/virustotal_intelligence.rb +57 -49
  122. data/lib/mihari/type_checker.rb +10 -8
  123. data/lib/mihari/version.rb +1 -1
  124. data/lib/mihari/web/public/assets/index-ac4e5ffa.js +50 -0
  125. data/lib/mihari/web/public/index.html +1 -1
  126. data/mihari.gemspec +8 -8
  127. metadata +103 -22
  128. data/.gitmodules +0 -0
  129. data/.overcommit.yml +0 -12
  130. data/lib/mihari/web/public/assets/index-cbe1734c.js +0 -50
@@ -0,0 +1,108 @@
1
+ <template>
2
+ <div class="column">
3
+ <div v-if="runRuleTask.isRunning">
4
+ <Loading></Loading>
5
+ <hr />
6
+ </div>
7
+
8
+ <div v-if="runRuleTask.last?.error">
9
+ <ErrorMessage :error="runRuleTask.last.error"></ErrorMessage>
10
+ <hr />
11
+ </div>
12
+
13
+ <h2 class="is-size-2 mb-4">Rule</h2>
14
+
15
+ <p class="is-clearfix">
16
+ <span class="buttons is-pulled-right">
17
+ <button class="button is-primary is-light is-small" @click="runRule">
18
+ <span>Run</span>
19
+ <span class="icon is-small">
20
+ <i class="fas fa-arrow-right"></i>
21
+ </span>
22
+ </button>
23
+ <router-link
24
+ class="button is-info is-light is-small"
25
+ :to="{ name: 'EditRule', params: { id: rule.id } }"
26
+ >
27
+ <span>Edit</span>
28
+ <span class="icon is-small">
29
+ <i class="fas fa-edit"></i>
30
+ </span>
31
+ </router-link>
32
+ <button class="button is-light is-small" @click="deleteRule">
33
+ <span>Delete</span>
34
+ <span class="icon is-small">
35
+ <i class="fas fa-times"></i>
36
+ </span>
37
+ </button>
38
+ </span>
39
+ </p>
40
+
41
+ <YAML :yaml="rule.yaml"></YAML>
42
+ </div>
43
+
44
+ <hr />
45
+
46
+ <div class="column">
47
+ <h2 class="is-size-2 mb-4">Related alerts</h2>
48
+
49
+ <Alerts :ruleId="rule.id"></Alerts>
50
+ </div>
51
+ </template>
52
+
53
+ <script lang="ts">
54
+ import { defineComponent, PropType } from "vue";
55
+ import { useRouter } from "vue-router";
56
+
57
+ import { generateDeleteRuleTask, generateRunRuleTask } from "@/api-helper";
58
+ import Alerts from "@/components/alert/AlertsWithPagination.vue";
59
+ import ErrorMessage from "@/components/ErrorMessage.vue";
60
+ import Loading from "@/components/Loading.vue";
61
+ import YAML from "@/components/rule/YAML.vue";
62
+ import { Rule } from "@/types";
63
+
64
+ export default defineComponent({
65
+ name: "RuleItem",
66
+ props: {
67
+ rule: {
68
+ type: Object as PropType<Rule>,
69
+ required: true,
70
+ },
71
+ },
72
+ components: {
73
+ YAML,
74
+ Alerts,
75
+ Loading,
76
+ ErrorMessage,
77
+ },
78
+ emits: ["refresh"],
79
+ setup(props, context) {
80
+ const router = useRouter();
81
+
82
+ const deleteRuleTask = generateDeleteRuleTask();
83
+ const runRuleTask = generateRunRuleTask();
84
+
85
+ const deleteRule = async () => {
86
+ const result = window.confirm(
87
+ `Are you sure you want to delete ${props.rule.id}?`
88
+ );
89
+
90
+ if (result) {
91
+ await deleteRuleTask.perform(props.rule.id);
92
+ router.push("/");
93
+ }
94
+ };
95
+
96
+ const runRule = async () => {
97
+ await runRuleTask.perform(props.rule.id);
98
+ context.emit("refresh");
99
+ };
100
+
101
+ return {
102
+ deleteRule,
103
+ runRule,
104
+ runRuleTask,
105
+ };
106
+ },
107
+ });
108
+ </script>
@@ -0,0 +1,62 @@
1
+ <template>
2
+ <Loading v-if="getRuleTask.isRunning"></Loading>
3
+
4
+ <ErrorMessage
5
+ v-if="getRuleTask.isError"
6
+ :error="getRuleTask.last?.error"
7
+ ></ErrorMessage>
8
+
9
+ <Rule
10
+ :rule="getRuleTask.last.value"
11
+ @refresh="refresh"
12
+ v-if="getRuleTask.last?.value"
13
+ ></Rule>
14
+ </template>
15
+
16
+ <script lang="ts">
17
+ import { defineComponent, onMounted, watch } from "vue";
18
+
19
+ import { generateGetRuleTask } from "@/api-helper";
20
+ import ErrorMessage from "@/components/ErrorMessage.vue";
21
+ import Loading from "@/components/Loading.vue";
22
+ import Rule from "@/components/rule/Rule.vue";
23
+
24
+ export default defineComponent({
25
+ name: "RuleWrapper",
26
+ components: {
27
+ Rule,
28
+ Loading,
29
+ ErrorMessage,
30
+ },
31
+ props: {
32
+ id: {
33
+ type: String,
34
+ required: true,
35
+ },
36
+ },
37
+ setup(props) {
38
+ const getRuleTask = generateGetRuleTask();
39
+
40
+ const getRule = async () => {
41
+ await getRuleTask.perform(props.id);
42
+ };
43
+
44
+ const refresh = async () => {
45
+ await getRule();
46
+ };
47
+
48
+ onMounted(async () => {
49
+ await getRule();
50
+ });
51
+
52
+ watch(props, async () => {
53
+ await getRule();
54
+ });
55
+
56
+ return {
57
+ getRuleTask,
58
+ refresh,
59
+ };
60
+ },
61
+ });
62
+ </script>
@@ -0,0 +1,88 @@
1
+ <template>
2
+ <div v-if="hasRules">
3
+ <table class="table is-fullwidth">
4
+ <tr>
5
+ <th>ID</th>
6
+ <th>Title</th>
7
+ <th>Description</th>
8
+ <th>Tags</th>
9
+ </tr>
10
+ <tr v-for="rule in rules.rules" :key="rule.id">
11
+ <td>
12
+ <router-link :to="{ name: 'Rule', params: { id: rule.id } }">{{
13
+ rule.id
14
+ }}</router-link>
15
+ </td>
16
+ <td>
17
+ {{ rule.title }}
18
+ </td>
19
+ <td>
20
+ {{ rule.description }}
21
+ </td>
22
+ <td>
23
+ <Tags :tags="rule.tags" @update-tag="updateTag"></Tags>
24
+ </td>
25
+ </tr>
26
+ </table>
27
+ </div>
28
+ <Pagination
29
+ :currentPage="rules.currentPage"
30
+ :total="rules.total"
31
+ :pageSize="rules.pageSize"
32
+ @update-page="updatePage"
33
+ ></Pagination>
34
+ <p class="help">
35
+ ({{ rules.total }} results in total, {{ rules.rules.length }} shown)
36
+ </p>
37
+ </template>
38
+
39
+ <script lang="ts">
40
+ import { computed, defineComponent, PropType } from "vue";
41
+
42
+ import Pagination from "@/components/Pagination.vue";
43
+ import Tags from "@/components/tag/Tags.vue";
44
+ import { Rules } from "@/types";
45
+
46
+ export default defineComponent({
47
+ name: "RulesItem",
48
+ props: {
49
+ rules: {
50
+ type: Object as PropType<Rules>,
51
+ required: true,
52
+ },
53
+ },
54
+ components: {
55
+ Pagination,
56
+ Tags,
57
+ },
58
+ emits: ["update-page", "refresh-page", "update-tag"],
59
+ setup(props, context) {
60
+ const scrollToTop = () => {
61
+ window.scrollTo({
62
+ top: 0,
63
+ });
64
+ };
65
+
66
+ const updatePage = (page: number) => {
67
+ scrollToTop();
68
+ context.emit("update-page", page);
69
+ };
70
+
71
+ const refreshPage = () => {
72
+ scrollToTop();
73
+ context.emit("refresh-page");
74
+ };
75
+
76
+ const updateTag = (tag: string) => {
77
+ scrollToTop();
78
+ context.emit("update-tag", tag);
79
+ };
80
+
81
+ const hasRules = computed(() => {
82
+ return props.rules.rules.length > 0;
83
+ });
84
+
85
+ return { updatePage, refreshPage, updateTag, hasRules };
86
+ },
87
+ });
88
+ </script>
@@ -0,0 +1,130 @@
1
+ <template>
2
+ <div class="box mb-6">
3
+ <FormComponent
4
+ ref="form"
5
+ :tags="getTagsTask.last?.value || []"
6
+ :page="page"
7
+ :tag="tag"
8
+ ></FormComponent>
9
+
10
+ <hr />
11
+
12
+ <div class="column">
13
+ <div class="field is-grouped is-grouped-centered">
14
+ <p class="control">
15
+ <a class="button is-primary" @click="search">
16
+ <span class="icon is-small">
17
+ <i class="fas fa-search"></i>
18
+ </span>
19
+ <span>Search</span>
20
+ </a>
21
+ </p>
22
+ </div>
23
+ </div>
24
+ </div>
25
+
26
+ <div v-if="getRulesTask.performCount > 0">
27
+ <hr />
28
+
29
+ <Loading v-if="getRulesTask.isRunning"></Loading>
30
+
31
+ <ErrorMessage
32
+ v-if="getRulesTask.isError"
33
+ :error="getRulesTask.last?.error"
34
+ ></ErrorMessage>
35
+
36
+ <Rules
37
+ :rules="getRulesTask.last.value"
38
+ v-if="getRulesTask.last?.value"
39
+ @refresh-page="refreshPage"
40
+ @update-page="updatePage"
41
+ @update-tag="updateTag"
42
+ ></Rules>
43
+ </div>
44
+ </template>
45
+
46
+ <script lang="ts">
47
+ import { defineComponent, nextTick, onMounted, ref, watch } from "vue";
48
+
49
+ import { generateGetRulesTask, generateGetTagsTask } from "@/api-helper";
50
+ import ErrorMessage from "@/components/ErrorMessage.vue";
51
+ import Loading from "@/components/Loading.vue";
52
+ import FormComponent from "@/components/rule/Form.vue";
53
+ import Rules from "@/components/rule/Rules.vue";
54
+ import { RuleSearchParams } from "@/types";
55
+
56
+ export default defineComponent({
57
+ name: "RulesWrapper",
58
+ components: {
59
+ Rules,
60
+ Loading,
61
+ FormComponent,
62
+ ErrorMessage,
63
+ },
64
+ setup() {
65
+ const page = ref(1);
66
+ const tag = ref<string | undefined>(undefined);
67
+ const form = ref<InstanceType<typeof FormComponent>>();
68
+
69
+ const getRulesTask = generateGetRulesTask();
70
+ const getTagsTask = generateGetTagsTask();
71
+
72
+ const getRules = async () => {
73
+ const params = form.value?.getSearchParams() as RuleSearchParams;
74
+ return await getRulesTask.perform(params);
75
+ };
76
+
77
+ const updatePage = (newPage: number) => {
78
+ page.value = newPage;
79
+ };
80
+
81
+ const resetPage = () => {
82
+ page.value = 1;
83
+ };
84
+
85
+ const search = async () => {
86
+ // reset page
87
+ resetPage();
88
+
89
+ await getRules();
90
+ };
91
+
92
+ const updateTag = (newTag: string | undefined) => {
93
+ if (tag.value === newTag) {
94
+ tag.value = undefined;
95
+ } else {
96
+ tag.value = newTag;
97
+ }
98
+
99
+ nextTick(async () => await search());
100
+ };
101
+
102
+ const refreshPage = async () => {
103
+ // it is just an alias of search
104
+ // this function will be invoked when a rule is deleted
105
+ await search();
106
+ };
107
+
108
+ onMounted(async () => {
109
+ getTagsTask.perform();
110
+ await getRules();
111
+ });
112
+
113
+ watch([page, tag], async () => {
114
+ nextTick(async () => await getRules());
115
+ });
116
+
117
+ return {
118
+ form,
119
+ getRulesTask,
120
+ getTagsTask,
121
+ page,
122
+ tag,
123
+ refreshPage,
124
+ search,
125
+ updatePage,
126
+ updateTag,
127
+ };
128
+ },
129
+ });
130
+ </script>
@@ -0,0 +1,47 @@
1
+ <template>
2
+ <pre
3
+ ref="pre"
4
+ class="line-numbers"
5
+ ><code class="language-yaml">{{ yaml }}</code></pre>
6
+ </template>
7
+
8
+ <script lang="ts">
9
+ // eslint-disable-next-line simple-import-sort/imports
10
+ import { defineComponent, onMounted, ref } from "vue";
11
+
12
+ import Prism from "prismjs";
13
+
14
+ import "prismjs/components/prism-yaml";
15
+ import "prismjs/plugins/custom-class/prism-custom-class";
16
+ import "prismjs/plugins/line-numbers/prism-line-numbers.css";
17
+ import "prismjs/plugins/line-numbers/prism-line-numbers";
18
+ import "prismjs/themes/prism-twilight.css";
19
+
20
+ export default defineComponent({
21
+ name: "YAML",
22
+ props: {
23
+ yaml: {
24
+ type: String,
25
+ required: true,
26
+ },
27
+ },
28
+ setup() {
29
+ const pre = ref<HTMLElement | undefined>(undefined);
30
+
31
+ Prism.plugins.customClass.map({
32
+ number: "prism-number",
33
+ tag: "prism-tag",
34
+ });
35
+
36
+ onMounted(() => {
37
+ if (pre.value) {
38
+ pre.value.querySelectorAll("code").forEach((elem) => {
39
+ Prism.highlightElement(elem);
40
+ });
41
+ }
42
+ });
43
+
44
+ return { pre };
45
+ },
46
+ });
47
+ </script>
@@ -0,0 +1,73 @@
1
+ <template>
2
+ <div class="control" v-if="!isDeleted">
3
+ <div
4
+ class="tags has-addons are-medium"
5
+ v-on:mouseover="showDeleteButton"
6
+ v-on:mouseleave="hideDeleteButton"
7
+ >
8
+ <span class="tag is-info is-light" @click="updateTag">{{
9
+ tag.name
10
+ }}</span>
11
+ <a
12
+ class="tag is-delete"
13
+ v-if="isDeleteButtonEnabled"
14
+ @click="deleteTag"
15
+ ></a>
16
+ </div>
17
+ </div>
18
+ </template>
19
+
20
+ <script lang="ts">
21
+ import { defineComponent, PropType, ref } from "vue";
22
+
23
+ import { generateDeleteTagTask } from "@/api-helper";
24
+ import { Tag } from "@/types";
25
+
26
+ export default defineComponent({
27
+ name: "TagItem",
28
+ props: {
29
+ tag: {
30
+ type: Object as PropType<Tag>,
31
+ required: true,
32
+ },
33
+ },
34
+ setup(props, context) {
35
+ const isDeleted = ref(false);
36
+ const isDeleteButtonEnabled = ref(false);
37
+
38
+ const deleteTagTask = generateDeleteTagTask();
39
+
40
+ const deleteTag = async () => {
41
+ const result = window.confirm(
42
+ `Are you sure you want to delete ${props.tag.name}?`
43
+ );
44
+
45
+ if (result) {
46
+ await deleteTagTask.perform(props.tag.name);
47
+ isDeleted.value = true;
48
+ }
49
+ };
50
+
51
+ const showDeleteButton = () => {
52
+ isDeleteButtonEnabled.value = true;
53
+ };
54
+
55
+ const hideDeleteButton = () => {
56
+ isDeleteButtonEnabled.value = false;
57
+ };
58
+
59
+ const updateTag = () => {
60
+ context.emit("update-tag", props.tag.name);
61
+ };
62
+
63
+ return {
64
+ updateTag,
65
+ isDeleted,
66
+ deleteTag,
67
+ showDeleteButton,
68
+ hideDeleteButton,
69
+ isDeleteButtonEnabled,
70
+ };
71
+ },
72
+ });
73
+ </script>
@@ -0,0 +1,37 @@
1
+ <template>
2
+ <div class="field is-grouped is-grouped-multiline">
3
+ <TagComponent
4
+ v-for="tag in tags"
5
+ :tag="tag"
6
+ :key="tag.name"
7
+ @update-tag="updateTag"
8
+ ></TagComponent>
9
+ </div>
10
+ </template>
11
+
12
+ <script lang="ts">
13
+ import { defineComponent, PropType } from "vue";
14
+
15
+ import TagComponent from "@/components/tag/Tag.vue";
16
+ import { Tag } from "@/types";
17
+
18
+ export default defineComponent({
19
+ name: "TagsItem",
20
+ components: {
21
+ TagComponent,
22
+ },
23
+ props: {
24
+ tags: {
25
+ type: Array as PropType<Tag[]>,
26
+ required: true,
27
+ },
28
+ },
29
+ setup(_, context) {
30
+ const updateTag = (tag: string) => {
31
+ context.emit("update-tag", tag);
32
+ };
33
+
34
+ return { updateTag };
35
+ },
36
+ });
37
+ </script>