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,188 @@
1
+ export interface Pagination {
2
+ total: number
3
+ currentPage: number
4
+ pageSize: number
5
+ }
6
+
7
+ export interface ConfigValue {
8
+ key: string
9
+ value: string | null
10
+ }
11
+
12
+ export interface Config {
13
+ name: string
14
+ isConfigured: boolean
15
+ values: ConfigValue[]
16
+ type: string
17
+ }
18
+
19
+ export interface Tag {
20
+ name: string
21
+ }
22
+
23
+ export interface Tags {
24
+ tags: string[]
25
+ }
26
+
27
+ export interface RuleSet {
28
+ ruleIds: string[]
29
+ }
30
+
31
+ export interface DnsRecord {
32
+ resource: string
33
+ value: string
34
+ }
35
+
36
+ export interface Contact {
37
+ name: string | null
38
+ organization: string | null
39
+ }
40
+
41
+ export interface Registrar {
42
+ name: string | null
43
+ organization: string | null
44
+ }
45
+
46
+ export interface WhoisRecord {
47
+ createdOn: Date | null
48
+ updatedOn: Date | null
49
+ expiresOn: Date | null
50
+ registrar: Registrar | null
51
+ contacts: Contact[]
52
+ }
53
+
54
+ export interface AutonomousSystem {
55
+ asn: number
56
+ }
57
+
58
+ export interface Geolocation {
59
+ country: string
60
+ countryCode: string
61
+ }
62
+
63
+ export interface ReverseDnsName {
64
+ name: string
65
+ }
66
+
67
+ export interface CPE {
68
+ cpe: string
69
+ }
70
+
71
+ export interface Port {
72
+ port: string
73
+ }
74
+
75
+ export interface Artifact {
76
+ id: string
77
+ data: string
78
+ dataType: string
79
+ source: string
80
+ metadata: unknown | null
81
+ createdAt: string
82
+
83
+ autonomousSystem: AutonomousSystem | null
84
+ whoisRecord: WhoisRecord | null
85
+ geolocation: Geolocation | null
86
+
87
+ dnsRecords: DnsRecord[] | null
88
+ reverseDnsNames: ReverseDnsName[] | null
89
+ cpes: CPE[] | null
90
+ ports: Port[] | null
91
+ }
92
+
93
+ export interface ArtifactWithTags extends Artifact {
94
+ tags: string[]
95
+ }
96
+
97
+ export interface Alert {
98
+ id: string
99
+ ruleId: string
100
+ createdAt: string
101
+
102
+ tags: Tag[]
103
+ artifacts: Artifact[]
104
+ }
105
+
106
+ export interface Alerts extends Pagination {
107
+ alerts: Alert[]
108
+ }
109
+
110
+ export interface PaginationParams {
111
+ page: number | undefined
112
+ }
113
+
114
+ export interface AlertSearchParams extends PaginationParams {
115
+ artifact: string | undefined
116
+ ruleId: string | undefined
117
+ tag: string | undefined
118
+ fromAt: string | undefined
119
+ toAt: string | undefined
120
+ }
121
+
122
+ export interface IPInfo {
123
+ ip: string
124
+ hostname: string | null
125
+ loc: string
126
+ countryCode: string
127
+ asn: string
128
+ }
129
+
130
+ export interface GCS {
131
+ lat: number
132
+ long: number
133
+ }
134
+
135
+ export interface Country {
136
+ name: string
137
+ code: string
138
+ lat: number
139
+ long: number
140
+ }
141
+
142
+ export type LinkType = "ip" | "domain" | "url" | "hash"
143
+
144
+ export interface Link {
145
+ name: string
146
+ type: string
147
+ baseURL: string
148
+ // eslint-disable-next-line no-unused-vars
149
+ href(data: string): string
150
+ favicon(): string
151
+ }
152
+
153
+ export interface Rule {
154
+ id: string
155
+ title: string
156
+ description: string
157
+ yaml: string
158
+ createdAt: string
159
+ updatedAt: string
160
+ tags: Tag[]
161
+ }
162
+
163
+ export interface CreateRule {
164
+ yaml: string
165
+ }
166
+
167
+ export interface UpdateRule {
168
+ id: string
169
+ yaml: string
170
+ }
171
+
172
+ export interface Query {
173
+ analyzer: string
174
+ query: string
175
+ interval: null
176
+ }
177
+
178
+ export interface Rules extends Pagination {
179
+ rules: Rule[]
180
+ }
181
+
182
+ export interface RuleSearchParams extends PaginationParams {
183
+ description: string | undefined
184
+ tag: string | undefined
185
+ title: string | undefined
186
+ fromAt: string | undefined
187
+ toAt: string | undefined
188
+ }
@@ -0,0 +1,54 @@
1
+ import dayjs from "dayjs"
2
+ import relativeTime from "dayjs/plugin/relativeTime"
3
+ import timezone from "dayjs/plugin/timezone"
4
+ import utc from "dayjs/plugin/utc"
5
+ import type { LocationQueryValue } from "vue-router"
6
+
7
+ import { getCountryByCode } from "@/countries"
8
+ import type { GCS, IPInfo } from "@/types"
9
+
10
+ dayjs.extend(relativeTime)
11
+ dayjs.extend(timezone)
12
+ dayjs.extend(utc)
13
+
14
+ export function getLocalDatetime(datetime: string): string {
15
+ return dayjs(datetime).local().format("YYYY-MM-DD HH:mm:ss")
16
+ }
17
+
18
+ export function getHumanizedRelativeTime(datetime: string): string {
19
+ return dayjs(datetime).local().fromNow()
20
+ }
21
+
22
+ export function getGCSByCountryCode(countryCode: string): GCS | undefined {
23
+ const country = getCountryByCode(countryCode)
24
+ if (country !== undefined) {
25
+ return { lat: country.lat, long: country.long }
26
+ }
27
+ }
28
+
29
+ export function getGCSByIPInfo(ipinfo: IPInfo): GCS | undefined {
30
+ if (ipinfo.loc !== undefined) {
31
+ const numbers = ipinfo.loc.split(",")
32
+ if (numbers.length === 2) {
33
+ const lat = numbers[0]
34
+ const long = numbers[1]
35
+
36
+ return { lat: parseFloat(lat), long: parseFloat(long) }
37
+ }
38
+ }
39
+ return getGCSByCountryCode(ipinfo.countryCode)
40
+ }
41
+
42
+ export function normalizeQueryParam(
43
+ param: undefined | null | string | string[] | LocationQueryValue | LocationQueryValue[]
44
+ ): string | undefined {
45
+ if (param === undefined || param === null) {
46
+ return undefined
47
+ }
48
+
49
+ if (typeof param === "string") {
50
+ return param
51
+ }
52
+
53
+ return param.toString()
54
+ }
@@ -0,0 +1,20 @@
1
+ <template>
2
+ <Alerts></Alerts>
3
+ </template>
4
+
5
+ <script lang="ts">
6
+ import { useTitle } from "@vueuse/core"
7
+ import { defineComponent } from "vue"
8
+
9
+ import Alerts from "@/components/alert/AlertsWrapper.vue"
10
+
11
+ export default defineComponent({
12
+ name: "AlertsView",
13
+ components: {
14
+ Alerts
15
+ },
16
+ setup() {
17
+ useTitle("Alerts - Mihari")
18
+ }
19
+ })
20
+ </script>
@@ -0,0 +1,44 @@
1
+ <template>
2
+ <Artifact :id="artifactId"></Artifact>
3
+ </template>
4
+
5
+ <script lang="ts">
6
+ import { useTitle } from "@vueuse/core"
7
+ import { defineComponent, onMounted, ref, watch } from "vue"
8
+
9
+ import Artifact from "@/components/artifact/ArtifactWrapper.vue"
10
+
11
+ export default defineComponent({
12
+ name: "ArtifactView",
13
+ components: {
14
+ Artifact
15
+ },
16
+ props: {
17
+ id: {
18
+ type: String,
19
+ required: true
20
+ }
21
+ },
22
+ setup(props) {
23
+ const artifactId = ref<string>(props.id)
24
+
25
+ const updateTitle = () => {
26
+ useTitle(`Artifact:${artifactId.value} - Mihari`)
27
+ }
28
+
29
+ onMounted(() => {
30
+ updateTitle()
31
+ })
32
+
33
+ watch(
34
+ () => props.id,
35
+ () => {
36
+ artifactId.value = props.id
37
+ updateTitle()
38
+ }
39
+ )
40
+
41
+ return { artifactId }
42
+ }
43
+ })
44
+ </script>
@@ -0,0 +1,20 @@
1
+ <template>
2
+ <Configs></Configs>
3
+ </template>
4
+
5
+ <script lang="ts">
6
+ import { useTitle } from "@vueuse/core"
7
+ import { defineComponent } from "vue"
8
+
9
+ import Configs from "@/components/config/ConfigsWrapper.vue"
10
+
11
+ export default defineComponent({
12
+ name: "ConfigView",
13
+ components: {
14
+ Configs
15
+ },
16
+ setup() {
17
+ useTitle("Config - Mihari")
18
+ }
19
+ })
20
+ </script>
@@ -0,0 +1,44 @@
1
+ <template>
2
+ <EditRule :id="id"></EditRule>
3
+ </template>
4
+
5
+ <script lang="ts">
6
+ import { useTitle } from "@vueuse/core"
7
+ import { defineComponent, onMounted, ref, watch } from "vue"
8
+
9
+ import EditRule from "@/components/rule/EditRuleWrapper.vue"
10
+
11
+ export default defineComponent({
12
+ name: "EditRuleView",
13
+ components: {
14
+ EditRule
15
+ },
16
+ props: {
17
+ id: {
18
+ type: String,
19
+ required: true
20
+ }
21
+ },
22
+ setup(props) {
23
+ const ruleId = ref<string>(props.id)
24
+
25
+ const updateTitle = () => {
26
+ useTitle(`Edit rule:${ruleId.value} - Mihari`)
27
+ }
28
+
29
+ onMounted(() => {
30
+ updateTitle()
31
+ })
32
+
33
+ watch(
34
+ () => props.id,
35
+ () => {
36
+ ruleId.value = props.id
37
+ updateTitle()
38
+ }
39
+ )
40
+
41
+ return { ruleId }
42
+ }
43
+ })
44
+ </script>
@@ -0,0 +1,26 @@
1
+ <template>
2
+ <NewRule></NewRule>
3
+ </template>
4
+
5
+ <script lang="ts">
6
+ import { useTitle } from "@vueuse/core"
7
+ import { defineComponent, onMounted } from "vue"
8
+
9
+ import NewRule from "@/components/rule/NewRule.vue"
10
+
11
+ export default defineComponent({
12
+ name: "NewRuleView",
13
+ components: {
14
+ NewRule
15
+ },
16
+ setup() {
17
+ const updateTitle = () => {
18
+ useTitle(`New rule - Mihari`)
19
+ }
20
+
21
+ onMounted(() => {
22
+ updateTitle()
23
+ })
24
+ }
25
+ })
26
+ </script>
@@ -0,0 +1,44 @@
1
+ <template>
2
+ <Rule :id="ruleId"></Rule>
3
+ </template>
4
+
5
+ <script lang="ts">
6
+ import { useTitle } from "@vueuse/core"
7
+ import { defineComponent, onMounted, ref, watch } from "vue"
8
+
9
+ import Rule from "@/components/rule/RuleWrapper.vue"
10
+
11
+ export default defineComponent({
12
+ name: "RuleView",
13
+ components: {
14
+ Rule
15
+ },
16
+ props: {
17
+ id: {
18
+ type: String,
19
+ required: true
20
+ }
21
+ },
22
+ setup(props) {
23
+ const ruleId = ref<string>(props.id)
24
+
25
+ const updateTitle = () => {
26
+ useTitle(`Rule:${ruleId.value} - Mihari`)
27
+ }
28
+
29
+ onMounted(() => {
30
+ updateTitle()
31
+ })
32
+
33
+ watch(
34
+ () => props.id,
35
+ () => {
36
+ ruleId.value = props.id
37
+ updateTitle()
38
+ }
39
+ )
40
+
41
+ return { ruleId }
42
+ }
43
+ })
44
+ </script>
@@ -0,0 +1,20 @@
1
+ <template>
2
+ <Rules></Rules>
3
+ </template>
4
+
5
+ <script lang="ts">
6
+ import { useTitle } from "@vueuse/core"
7
+ import { defineComponent } from "vue"
8
+
9
+ import Rules from "@/components/rule/RulesWrapper.vue"
10
+
11
+ export default defineComponent({
12
+ name: "RulesView",
13
+ components: {
14
+ Rules
15
+ },
16
+ setup() {
17
+ useTitle("Rules - Mihari")
18
+ }
19
+ })
20
+ </script>
@@ -0,0 +1,9 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { getHumanizedRelativeTime } from '@/utils'
4
+
5
+ describe('getHumanizedRelativeTime', () => {
6
+ it('returns a relative time in humanized format', () => {
7
+ expect(getHumanizedRelativeTime('1970-01-01 00:00:00')).toContain('years')
8
+ })
9
+ })
@@ -0,0 +1,21 @@
1
+ {
2
+ "extends": "@vue/tsconfig/tsconfig.dom.json",
3
+ "include": [
4
+ "env.d.ts",
5
+ "src/**/*",
6
+ "src/**/*.vue",
7
+ "tests/**/*"
8
+ ],
9
+ "exclude": [
10
+ "src/**/__tests__/*"
11
+ ],
12
+ "compilerOptions": {
13
+ "composite": true,
14
+ "baseUrl": ".",
15
+ "paths": {
16
+ "@/*": [
17
+ "./src/*"
18
+ ]
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ {
5
+ "path": "./tsconfig.node.json"
6
+ },
7
+ {
8
+ "path": "./tsconfig.app.json"
9
+ },
10
+ {
11
+ "path": "./tsconfig.vitest.json"
12
+ }
13
+ ]
14
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "@tsconfig/node20/tsconfig.json",
3
+ "include": [
4
+ "vite.config.*",
5
+ ],
6
+ "compilerOptions": {
7
+ "composite": true,
8
+ "module": "ESNext",
9
+ "types": [
10
+ "node"
11
+ ]
12
+ }
13
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "./tsconfig.app.json",
3
+ "exclude": [],
4
+ "compilerOptions": {
5
+ "composite": true,
6
+ "lib": [],
7
+ "types": [
8
+ "node",
9
+ "jsdom"
10
+ ]
11
+ }
12
+ }
@@ -0,0 +1,24 @@
1
+ import { fileURLToPath, URL } from "node:url"
2
+
3
+ import vue from "@vitejs/plugin-vue"
4
+ import { defineConfig } from "vite"
5
+
6
+ const env = process.env
7
+ const target = env.BACKEND_URL || "http://localhost:9292/"
8
+ const port = parseInt(env.port || "8080")
9
+
10
+ // https://vitejs.dev/config/
11
+ export default defineConfig({
12
+ plugins: [vue()],
13
+ server: {
14
+ port,
15
+ proxy: {
16
+ "/api": target
17
+ }
18
+ },
19
+ resolve: {
20
+ alias: {
21
+ "@": fileURLToPath(new URL("./src", import.meta.url))
22
+ }
23
+ }
24
+ })
@@ -0,0 +1,21 @@
1
+ import { fileURLToPath } from "node:url"
2
+
3
+ import { mergeConfig } from "vite"
4
+ import { configDefaults, defineConfig } from "vitest/config"
5
+
6
+ import viteConfig from "./vite.config"
7
+
8
+ export default mergeConfig(
9
+ viteConfig,
10
+ defineConfig({
11
+ test: {
12
+ environment: "jsdom",
13
+ exclude: [...configDefaults.exclude, "e2e/*"],
14
+ root: fileURLToPath(new URL("./", import.meta.url)),
15
+ transformMode: {
16
+ web: [/\.[jt]sx$/]
17
+ },
18
+ include: ["**/__tests__/**/*.?(c|m)[jt]s?(x)", "**/?(*.){test,spec}.?(c|m)[jt]s?(x)"]
19
+ }
20
+ })
21
+ )
data/lefthook.yml ADDED
@@ -0,0 +1,12 @@
1
+ pre-commit:
2
+ parallel: true
3
+ commands:
4
+ standard:
5
+ glob: "*.rb"
6
+ run: bundle exec standardrb --fix {staged_files}
7
+ stage_fixed: true
8
+ eslint:
9
+ root: "frontend/"
10
+ glob: "*.{js,ts,vue}"
11
+ run: npx eslint --fix {staged_files}
12
+ stage_fixed: true