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,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,60 @@
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 { LocationQueryValue } from "vue-router";
6
+
7
+ import { getCountryByCode } from "@/countries";
8
+ import { 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:
44
+ | undefined
45
+ | null
46
+ | string
47
+ | string[]
48
+ | LocationQueryValue
49
+ | LocationQueryValue[]
50
+ ): string | undefined {
51
+ if (param === undefined || param === null) {
52
+ return undefined;
53
+ }
54
+
55
+ if (typeof param === "string") {
56
+ return param;
57
+ }
58
+
59
+ return param.toString();
60
+ }
@@ -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,7 @@
1
+ import { getHumanizedRelativeTime } from "@/utils";
2
+
3
+ describe("getHumanizedRelativeTime", () => {
4
+ it("returns a relative time in humanized format", () => {
5
+ expect(getHumanizedRelativeTime("1970-01-01 00:00:00")).toContain("years");
6
+ });
7
+ });
@@ -0,0 +1,40 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "esnext",
4
+ "module": "esnext",
5
+ "strict": true,
6
+ "jsx": "preserve",
7
+ "importHelpers": true,
8
+ "moduleResolution": "node",
9
+ "skipLibCheck": true,
10
+ "esModuleInterop": true,
11
+ "allowSyntheticDefaultImports": true,
12
+ "sourceMap": true,
13
+ "baseUrl": ".",
14
+ "types": [
15
+ "webpack-env",
16
+ "jest"
17
+ ],
18
+ "paths": {
19
+ "@/*": [
20
+ "src/*"
21
+ ]
22
+ },
23
+ "lib": [
24
+ "esnext",
25
+ "dom",
26
+ "dom.iterable",
27
+ "scripthost"
28
+ ]
29
+ },
30
+ "include": [
31
+ "src/**/*.ts",
32
+ "src/**/*.tsx",
33
+ "src/**/*.vue",
34
+ "tests/**/*.ts",
35
+ "tests/**/*.tsx"
36
+ ],
37
+ "exclude": [
38
+ "node_modules"
39
+ ]
40
+ }
@@ -0,0 +1,24 @@
1
+ import vue from "@vitejs/plugin-vue";
2
+ import path from "path";
3
+ import { defineConfig, loadEnv } from "vite";
4
+
5
+ export default defineConfig(({ _, mode }) => {
6
+ const env = loadEnv(mode, process.cwd(), "");
7
+ const target = env.BACKEND_URL || "http://localhost:9292/";
8
+ const port = env.port || 8080;
9
+
10
+ return {
11
+ plugins: [vue()],
12
+ server: {
13
+ port,
14
+ proxy: {
15
+ "/api": target,
16
+ },
17
+ },
18
+ resolve: {
19
+ alias: {
20
+ "@": path.resolve(__dirname, "./src"),
21
+ },
22
+ },
23
+ };
24
+ });
data/lefthook.yml ADDED
@@ -0,0 +1,10 @@
1
+ pre-commit:
2
+ parallel: true
3
+ commands:
4
+ standard:
5
+ glob: "*.rb"
6
+ run: bundle exec standardrb --fix {staged_files} && git add {staged_files}
7
+ eslint:
8
+ root: "frontend/"
9
+ glob: "*.{js,ts,vue}"
10
+ run: npx eslint --fix {staged_files} && git add {staged_files}
@@ -35,17 +35,34 @@ module Mihari
35
35
  end
36
36
  end
37
37
 
38
- # @return [String]
39
- def source
40
- self.class.to_s.split("::").last.to_s
41
- end
42
-
43
38
  # @return [String]
44
39
  def class_name
45
40
  self.class.to_s.split("::").last
46
41
  end
47
42
 
43
+ alias_method :source, :class_name
44
+
48
45
  class << self
46
+ #
47
+ # Initialize an analyzer by query params
48
+ #
49
+ # @param [Hash] params
50
+ #
51
+ # @return [Mihari::Analyzers::Base]
52
+ #
53
+ def from_query(params)
54
+ # get options and set default value as an empty hash
55
+ options = params[:options] || {}
56
+
57
+ # set interval in the top level
58
+ interval = options[:interval]
59
+ params[:interval] = interval if interval
60
+
61
+ query = params[:query]
62
+
63
+ new(query, **params)
64
+ end
65
+
49
66
  def inherited(child)
50
67
  super
51
68
  Mihari.analyzers << child
@@ -46,7 +46,7 @@ module Mihari
46
46
  #
47
47
  # @param [Mihari::Structs::Rule] rule
48
48
  #
49
- def initialize(rule:)
49
+ def initialize(rule)
50
50
  @rule = rule
51
51
  @base_time = Time.now.utc
52
52
 
@@ -153,15 +153,6 @@ module Mihari
153
153
  end
154
154
  end
155
155
 
156
- #
157
- # Deep copied queries
158
- #
159
- # @return [Array<Hash>]
160
- #
161
- def queries
162
- rule.queries.map(&:deep_dup)
163
- end
164
-
165
156
  #
166
157
  # Get analyzer class
167
158
  #
@@ -177,26 +168,13 @@ module Mihari
177
168
  end
178
169
 
179
170
  #
180
- # @return [Array<Mihari::Analyzers::Base>] <description>
171
+ # @return [Array<Mihari::Analyzers::Base>]
181
172
  #
182
173
  def analyzers
183
- @analyzers ||= queries.map do |params|
184
- analyzer_name = params[:analyzer]
174
+ @analyzers ||= rule.queries.map do |query_params|
175
+ analyzer_name = query_params[:analyzer]
185
176
  klass = get_analyzer_class(analyzer_name)
186
-
187
- # set interval in the top level
188
- options = params[:options] || {}
189
- interval = options[:interval]
190
- params[:interval] = interval if interval
191
-
192
- # set rule
193
- params[:rule] = rule
194
- query = params[:query]
195
-
196
- analyzer = klass.new(query, **params)
197
- raise ConfigurationError, "#{analyzer.source} is not configured correctly" unless analyzer.configured?
198
-
199
- analyzer
177
+ klass.from_query(query_params)
200
178
  end
201
179
  end
202
180
 
@@ -240,8 +218,9 @@ module Mihari
240
218
  # Validate configuration of analyzers
241
219
  #
242
220
  def validate_analyzer_configurations
243
- # memoize analyzers & raise ConfigurationError if there is an analyzer which is not configured
244
- analyzers
221
+ analyzers.map do |analyzer|
222
+ raise ConfigurationError, "#{analyzer.source} is not configured correctly" unless analyzer.configured?
223
+ end
245
224
  end
246
225
  end
247
226
  end