mihari 5.2.3 → 5.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (135) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -1
  3. data/README.md +0 -10
  4. data/Rakefile +7 -1
  5. data/build_frontend.sh +2 -10
  6. data/frontend/.eslintrc.cjs +22 -0
  7. data/frontend/.gitignore +31 -0
  8. data/frontend/.prettierrc.json +8 -0
  9. data/frontend/README.md +3 -0
  10. data/frontend/env.d.ts +5 -0
  11. data/frontend/index.html +21 -0
  12. data/frontend/package-lock.json +8650 -0
  13. data/frontend/package.json +64 -0
  14. data/frontend/public/favicon.ico +0 -0
  15. data/frontend/scripts/swagger_doc_to_yaml.rb +23 -0
  16. data/frontend/src/App.vue +27 -0
  17. data/frontend/src/api-helper.ts +111 -0
  18. data/frontend/src/api.ts +105 -0
  19. data/frontend/src/components/ErrorMessage.vue +32 -0
  20. data/frontend/src/components/Loading.vue +15 -0
  21. data/frontend/src/components/Navbar.vue +42 -0
  22. data/frontend/src/components/Pagination.vue +119 -0
  23. data/frontend/src/components/alert/Alert.vue +87 -0
  24. data/frontend/src/components/alert/Alerts.vue +64 -0
  25. data/frontend/src/components/alert/AlertsWithPagination.vue +91 -0
  26. data/frontend/src/components/alert/AlertsWrapper.vue +134 -0
  27. data/frontend/src/components/alert/Form.vue +184 -0
  28. data/frontend/src/components/artifact/AS.vue +29 -0
  29. data/frontend/src/components/artifact/Artifact.vue +304 -0
  30. data/frontend/src/components/artifact/ArtifactTag.vue +64 -0
  31. data/frontend/src/components/artifact/ArtifactTags.vue +29 -0
  32. data/frontend/src/components/artifact/ArtifactWrapper.vue +59 -0
  33. data/frontend/src/components/artifact/CPEs.vue +23 -0
  34. data/frontend/src/components/artifact/DnsRecords.vue +38 -0
  35. data/frontend/src/components/artifact/Ports.vue +23 -0
  36. data/frontend/src/components/artifact/ReverseDnsNames.vue +31 -0
  37. data/frontend/src/components/artifact/Tags.vue +29 -0
  38. data/frontend/src/components/artifact/WhoisRecord.vue +47 -0
  39. data/frontend/src/components/config/Configs.vue +65 -0
  40. data/frontend/src/components/config/ConfigsWrapper.vue +34 -0
  41. data/frontend/src/components/link/Link.vue +32 -0
  42. data/frontend/src/components/link/Links.vue +42 -0
  43. data/frontend/src/components/rule/EditRule.vue +74 -0
  44. data/frontend/src/components/rule/EditRuleWrapper.vue +50 -0
  45. data/frontend/src/components/rule/Form.vue +160 -0
  46. data/frontend/src/components/rule/InputForm.vue +86 -0
  47. data/frontend/src/components/rule/NewRule.vue +60 -0
  48. data/frontend/src/components/rule/Rule.vue +106 -0
  49. data/frontend/src/components/rule/RuleWrapper.vue +55 -0
  50. data/frontend/src/components/rule/Rules.vue +84 -0
  51. data/frontend/src/components/rule/RulesWrapper.vue +127 -0
  52. data/frontend/src/components/rule/YAML.vue +44 -0
  53. data/frontend/src/components/tag/Tag.vue +65 -0
  54. data/frontend/src/components/tag/Tags.vue +37 -0
  55. data/frontend/src/countries.ts +350 -0
  56. data/frontend/src/index.ts +20 -0
  57. data/frontend/src/links/anyrun.ts +19 -0
  58. data/frontend/src/links/base.ts +14 -0
  59. data/frontend/src/links/censys.ts +20 -0
  60. data/frontend/src/links/crtsh.ts +20 -0
  61. data/frontend/src/links/dnslytics.ts +38 -0
  62. data/frontend/src/links/greynoise.ts +20 -0
  63. data/frontend/src/links/index.ts +40 -0
  64. data/frontend/src/links/intezer.ts +20 -0
  65. data/frontend/src/links/otx.ts +33 -0
  66. data/frontend/src/links/securitytrails.ts +38 -0
  67. data/frontend/src/links/shodan.ts +20 -0
  68. data/frontend/src/links/urlscan.ts +50 -0
  69. data/frontend/src/links/virustotal.ts +72 -0
  70. data/frontend/src/main.ts +11 -0
  71. data/frontend/src/router/index.ts +57 -0
  72. data/frontend/src/rule.ts +14 -0
  73. data/frontend/src/shims-vue.d.ts +6 -0
  74. data/frontend/src/swagger.yaml +737 -0
  75. data/frontend/src/types.ts +188 -0
  76. data/frontend/src/utils.ts +54 -0
  77. data/frontend/src/views/Alerts.vue +20 -0
  78. data/frontend/src/views/Artifact.vue +44 -0
  79. data/frontend/src/views/Configs.vue +20 -0
  80. data/frontend/src/views/EditRule.vue +44 -0
  81. data/frontend/src/views/NewRule.vue +26 -0
  82. data/frontend/src/views/Rule.vue +44 -0
  83. data/frontend/src/views/Rules.vue +20 -0
  84. data/frontend/tests/utils.spec.ts +9 -0
  85. data/frontend/tsconfig.app.json +21 -0
  86. data/frontend/tsconfig.json +14 -0
  87. data/frontend/tsconfig.node.json +13 -0
  88. data/frontend/tsconfig.vitest.json +12 -0
  89. data/frontend/vite.config.ts +24 -0
  90. data/frontend/vitest.config.ts +21 -0
  91. data/lefthook.yml +12 -0
  92. data/lib/mihari/analyzers/base.rb +63 -12
  93. data/lib/mihari/analyzers/binaryedge.rb +10 -15
  94. data/lib/mihari/analyzers/censys.rb +12 -15
  95. data/lib/mihari/analyzers/circl.rb +10 -10
  96. data/lib/mihari/analyzers/crtsh.rb +10 -6
  97. data/lib/mihari/analyzers/dnstwister.rb +6 -8
  98. data/lib/mihari/analyzers/feed.rb +21 -10
  99. data/lib/mihari/analyzers/greynoise.rb +10 -20
  100. data/lib/mihari/analyzers/onyphe.rb +9 -14
  101. data/lib/mihari/analyzers/otx.rb +8 -9
  102. data/lib/mihari/analyzers/passivetotal.rb +10 -10
  103. data/lib/mihari/analyzers/pulsedive.rb +21 -31
  104. data/lib/mihari/analyzers/rule.rb +8 -29
  105. data/lib/mihari/analyzers/securitytrails.rb +8 -6
  106. data/lib/mihari/analyzers/shodan.rb +8 -13
  107. data/lib/mihari/analyzers/urlscan.rb +15 -20
  108. data/lib/mihari/analyzers/virustotal.rb +16 -26
  109. data/lib/mihari/analyzers/virustotal_intelligence.rb +11 -17
  110. data/lib/mihari/analyzers/zoomeye.rb +12 -17
  111. data/lib/mihari/commands/search.rb +16 -7
  112. data/lib/mihari/config.rb +133 -0
  113. data/lib/mihari/constants.rb +3 -0
  114. data/lib/mihari/emitters/slack.rb +13 -3
  115. data/lib/mihari/entities/rule.rb +1 -1
  116. data/lib/mihari/entities/tag.rb +1 -1
  117. data/lib/mihari/errors.rb +1 -1
  118. data/lib/mihari/http.rb +2 -3
  119. data/lib/mihari/schemas/analyzer.rb +4 -7
  120. data/lib/mihari/schemas/rule.rb +1 -1
  121. data/lib/mihari/structs/config.rb +39 -16
  122. data/lib/mihari/structs/rule.rb +1 -1
  123. data/lib/mihari/type_checker.rb +6 -6
  124. data/lib/mihari/version.rb +1 -1
  125. data/lib/mihari/web/endpoints/configs.rb +5 -1
  126. data/lib/mihari/web/public/assets/{index-eed1bcd8.css → index-2ba8f0a6.css} +1 -1
  127. data/lib/mihari/web/public/assets/index-71285b15.js +50 -0
  128. data/lib/mihari/web/public/index.html +2 -2
  129. data/lib/mihari/web/public/redoc-static.html +388 -2193
  130. data/lib/mihari.rb +9 -59
  131. data/mihari.gemspec +13 -13
  132. metadata +112 -69
  133. data/.gitmodules +0 -0
  134. data/.overcommit.yml +0 -12
  135. data/lib/mihari/web/public/assets/index-cbe1734c.js +0 -50
@@ -0,0 +1,106 @@
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, type 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 type { 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(`Are you sure you want to delete ${props.rule.id}?`)
87
+
88
+ if (result) {
89
+ await deleteRuleTask.perform(props.rule.id)
90
+ router.push("/")
91
+ }
92
+ }
93
+
94
+ const runRule = async () => {
95
+ await runRuleTask.perform(props.rule.id)
96
+ context.emit("refresh")
97
+ }
98
+
99
+ return {
100
+ deleteRule,
101
+ runRule,
102
+ runRuleTask
103
+ }
104
+ }
105
+ })
106
+ </script>
@@ -0,0 +1,55 @@
1
+ <template>
2
+ <Loading v-if="getRuleTask.isRunning"></Loading>
3
+
4
+ <ErrorMessage v-if="getRuleTask.isError" :error="getRuleTask.last?.error"></ErrorMessage>
5
+
6
+ <Rule :rule="getRuleTask.last.value" @refresh="refresh" v-if="getRuleTask.last?.value"></Rule>
7
+ </template>
8
+
9
+ <script lang="ts">
10
+ import { defineComponent, onMounted, watch } from "vue"
11
+
12
+ import { generateGetRuleTask } from "@/api-helper"
13
+ import ErrorMessage from "@/components/ErrorMessage.vue"
14
+ import Loading from "@/components/Loading.vue"
15
+ import Rule from "@/components/rule/Rule.vue"
16
+
17
+ export default defineComponent({
18
+ name: "RuleWrapper",
19
+ components: {
20
+ Rule,
21
+ Loading,
22
+ ErrorMessage
23
+ },
24
+ props: {
25
+ id: {
26
+ type: String,
27
+ required: true
28
+ }
29
+ },
30
+ setup(props) {
31
+ const getRuleTask = generateGetRuleTask()
32
+
33
+ const getRule = async () => {
34
+ await getRuleTask.perform(props.id)
35
+ }
36
+
37
+ const refresh = async () => {
38
+ await getRule()
39
+ }
40
+
41
+ onMounted(async () => {
42
+ await getRule()
43
+ })
44
+
45
+ watch(props, async () => {
46
+ await getRule()
47
+ })
48
+
49
+ return {
50
+ getRuleTask,
51
+ refresh
52
+ }
53
+ }
54
+ })
55
+ </script>
@@ -0,0 +1,84 @@
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 } }">{{ rule.id }}</router-link>
13
+ </td>
14
+ <td>
15
+ {{ rule.title }}
16
+ </td>
17
+ <td>
18
+ {{ rule.description }}
19
+ </td>
20
+ <td>
21
+ <Tags :tags="rule.tags" @update-tag="updateTag"></Tags>
22
+ </td>
23
+ </tr>
24
+ </table>
25
+ </div>
26
+ <Pagination
27
+ :currentPage="rules.currentPage"
28
+ :total="rules.total"
29
+ :pageSize="rules.pageSize"
30
+ @update-page="updatePage"
31
+ ></Pagination>
32
+ <p class="help">({{ rules.total }} results in total, {{ rules.rules.length }} shown)</p>
33
+ </template>
34
+
35
+ <script lang="ts">
36
+ import { computed, defineComponent, type PropType } from "vue"
37
+
38
+ import Pagination from "@/components/Pagination.vue"
39
+ import Tags from "@/components/tag/Tags.vue"
40
+ import type { Rules } from "@/types"
41
+
42
+ export default defineComponent({
43
+ name: "RulesItem",
44
+ props: {
45
+ rules: {
46
+ type: Object as PropType<Rules>,
47
+ required: true
48
+ }
49
+ },
50
+ components: {
51
+ Pagination,
52
+ Tags
53
+ },
54
+ emits: ["update-page", "refresh-page", "update-tag"],
55
+ setup(props, context) {
56
+ const scrollToTop = () => {
57
+ window.scrollTo({
58
+ top: 0
59
+ })
60
+ }
61
+
62
+ const updatePage = (page: number) => {
63
+ scrollToTop()
64
+ context.emit("update-page", page)
65
+ }
66
+
67
+ const refreshPage = () => {
68
+ scrollToTop()
69
+ context.emit("refresh-page")
70
+ }
71
+
72
+ const updateTag = (tag: string) => {
73
+ scrollToTop()
74
+ context.emit("update-tag", tag)
75
+ }
76
+
77
+ const hasRules = computed(() => {
78
+ return props.rules.rules.length > 0
79
+ })
80
+
81
+ return { updatePage, refreshPage, updateTag, hasRules }
82
+ }
83
+ })
84
+ </script>
@@ -0,0 +1,127 @@
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 v-if="getRulesTask.isError" :error="getRulesTask.last?.error"></ErrorMessage>
32
+
33
+ <Rules
34
+ :rules="getRulesTask.last.value"
35
+ v-if="getRulesTask.last?.value"
36
+ @refresh-page="refreshPage"
37
+ @update-page="updatePage"
38
+ @update-tag="updateTag"
39
+ ></Rules>
40
+ </div>
41
+ </template>
42
+
43
+ <script lang="ts">
44
+ import { defineComponent, nextTick, onMounted, ref, watch } from "vue"
45
+
46
+ import { generateGetRulesTask, generateGetTagsTask } from "@/api-helper"
47
+ import ErrorMessage from "@/components/ErrorMessage.vue"
48
+ import Loading from "@/components/Loading.vue"
49
+ import FormComponent from "@/components/rule/Form.vue"
50
+ import Rules from "@/components/rule/Rules.vue"
51
+ import type { RuleSearchParams } from "@/types"
52
+
53
+ export default defineComponent({
54
+ name: "RulesWrapper",
55
+ components: {
56
+ Rules,
57
+ Loading,
58
+ FormComponent,
59
+ ErrorMessage
60
+ },
61
+ setup() {
62
+ const page = ref(1)
63
+ const tag = ref<string | undefined>(undefined)
64
+ const form = ref<InstanceType<typeof FormComponent>>()
65
+
66
+ const getRulesTask = generateGetRulesTask()
67
+ const getTagsTask = generateGetTagsTask()
68
+
69
+ const getRules = async () => {
70
+ const params = form.value?.getSearchParams() as RuleSearchParams
71
+ return await getRulesTask.perform(params)
72
+ }
73
+
74
+ const updatePage = (newPage: number) => {
75
+ page.value = newPage
76
+ }
77
+
78
+ const resetPage = () => {
79
+ page.value = 1
80
+ }
81
+
82
+ const search = async () => {
83
+ // reset page
84
+ resetPage()
85
+
86
+ await getRules()
87
+ }
88
+
89
+ const updateTag = (newTag: string | undefined) => {
90
+ if (tag.value === newTag) {
91
+ tag.value = undefined
92
+ } else {
93
+ tag.value = newTag
94
+ }
95
+
96
+ nextTick(async () => await search())
97
+ }
98
+
99
+ const refreshPage = async () => {
100
+ // it is just an alias of search
101
+ // this function will be invoked when a rule is deleted
102
+ await search()
103
+ }
104
+
105
+ onMounted(async () => {
106
+ getTagsTask.perform()
107
+ await getRules()
108
+ })
109
+
110
+ watch([page, tag], async () => {
111
+ nextTick(async () => await getRules())
112
+ })
113
+
114
+ return {
115
+ form,
116
+ getRulesTask,
117
+ getTagsTask,
118
+ page,
119
+ tag,
120
+ refreshPage,
121
+ search,
122
+ updatePage,
123
+ updateTag
124
+ }
125
+ }
126
+ })
127
+ </script>
@@ -0,0 +1,44 @@
1
+ <template>
2
+ <pre ref="pre" class="line-numbers"><code class="language-yaml">{{ yaml }}</code></pre>
3
+ </template>
4
+
5
+ <script lang="ts">
6
+ // eslint-disable-next-line simple-import-sort/imports
7
+ import { defineComponent, onMounted, ref } from "vue"
8
+
9
+ import Prism from "prismjs"
10
+
11
+ import "prismjs/components/prism-yaml"
12
+ import "prismjs/plugins/custom-class/prism-custom-class"
13
+ import "prismjs/plugins/line-numbers/prism-line-numbers.css"
14
+ import "prismjs/plugins/line-numbers/prism-line-numbers"
15
+ import "prismjs/themes/prism-twilight.css"
16
+
17
+ export default defineComponent({
18
+ name: "YAML",
19
+ props: {
20
+ yaml: {
21
+ type: String,
22
+ required: true
23
+ }
24
+ },
25
+ setup() {
26
+ const pre = ref<HTMLElement | undefined>(undefined)
27
+
28
+ Prism.plugins.customClass.map({
29
+ number: "prism-number",
30
+ tag: "prism-tag"
31
+ })
32
+
33
+ onMounted(() => {
34
+ if (pre.value) {
35
+ pre.value.querySelectorAll("code").forEach((elem) => {
36
+ Prism.highlightElement(elem)
37
+ })
38
+ }
39
+ })
40
+
41
+ return { pre }
42
+ }
43
+ })
44
+ </script>
@@ -0,0 +1,65 @@
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">{{ tag.name }}</span>
9
+ <a class="tag is-delete" v-if="isDeleteButtonEnabled" @click="deleteTag"></a>
10
+ </div>
11
+ </div>
12
+ </template>
13
+
14
+ <script lang="ts">
15
+ import { defineComponent, type PropType, ref } from "vue"
16
+
17
+ import { generateDeleteTagTask } from "@/api-helper"
18
+ import type { Tag } from "@/types"
19
+
20
+ export default defineComponent({
21
+ name: "TagItem",
22
+ props: {
23
+ tag: {
24
+ type: Object as PropType<Tag>,
25
+ required: true
26
+ }
27
+ },
28
+ setup(props, context) {
29
+ const isDeleted = ref(false)
30
+ const isDeleteButtonEnabled = ref(false)
31
+
32
+ const deleteTagTask = generateDeleteTagTask()
33
+
34
+ const deleteTag = async () => {
35
+ const result = window.confirm(`Are you sure you want to delete ${props.tag.name}?`)
36
+
37
+ if (result) {
38
+ await deleteTagTask.perform(props.tag.name)
39
+ isDeleted.value = true
40
+ }
41
+ }
42
+
43
+ const showDeleteButton = () => {
44
+ isDeleteButtonEnabled.value = true
45
+ }
46
+
47
+ const hideDeleteButton = () => {
48
+ isDeleteButtonEnabled.value = false
49
+ }
50
+
51
+ const updateTag = () => {
52
+ context.emit("update-tag", props.tag.name)
53
+ }
54
+
55
+ return {
56
+ updateTag,
57
+ isDeleted,
58
+ deleteTag,
59
+ showDeleteButton,
60
+ hideDeleteButton,
61
+ isDeleteButtonEnabled
62
+ }
63
+ }
64
+ })
65
+ </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, type PropType } from "vue"
14
+
15
+ import TagComponent from "@/components/tag/Tag.vue"
16
+ import type { 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>