archsight 0.1.1 → 0.1.3
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.
- checksums.yaml +4 -4
- data/Dockerfile +21 -22
- data/README.md +38 -12
- data/exe/archsight +3 -1
- data/lib/archsight/analysis/executor.rb +112 -0
- data/lib/archsight/analysis/result.rb +174 -0
- data/lib/archsight/analysis/sandbox.rb +319 -0
- data/lib/archsight/analysis.rb +11 -0
- data/lib/archsight/annotations/architecture_annotations.rb +2 -2
- data/lib/archsight/cli.rb +164 -0
- data/lib/archsight/database.rb +6 -2
- data/lib/archsight/helpers/analysis_renderer.rb +83 -0
- data/lib/archsight/helpers/formatting.rb +95 -0
- data/lib/archsight/helpers.rb +20 -4
- data/lib/archsight/import/concurrent_progress.rb +341 -0
- data/lib/archsight/import/executor.rb +466 -0
- data/lib/archsight/import/git_analytics.rb +626 -0
- data/lib/archsight/import/handler.rb +263 -0
- data/lib/archsight/import/handlers/github.rb +161 -0
- data/lib/archsight/import/handlers/gitlab.rb +202 -0
- data/lib/archsight/import/handlers/jira_base.rb +189 -0
- data/lib/archsight/import/handlers/jira_discover.rb +161 -0
- data/lib/archsight/import/handlers/jira_metrics.rb +179 -0
- data/lib/archsight/import/handlers/openapi_schema_parser.rb +279 -0
- data/lib/archsight/import/handlers/repository.rb +439 -0
- data/lib/archsight/import/handlers/rest_api.rb +293 -0
- data/lib/archsight/import/handlers/rest_api_index.rb +183 -0
- data/lib/archsight/import/progress.rb +91 -0
- data/lib/archsight/import/registry.rb +54 -0
- data/lib/archsight/import/shared_file_writer.rb +67 -0
- data/lib/archsight/import/team_matcher.rb +195 -0
- data/lib/archsight/import.rb +14 -0
- data/lib/archsight/resources/analysis.rb +91 -0
- data/lib/archsight/resources/application_component.rb +2 -2
- data/lib/archsight/resources/application_service.rb +12 -12
- data/lib/archsight/resources/business_product.rb +12 -12
- data/lib/archsight/resources/data_object.rb +1 -1
- data/lib/archsight/resources/import.rb +79 -0
- data/lib/archsight/resources/technology_artifact.rb +23 -2
- data/lib/archsight/version.rb +1 -1
- data/lib/archsight/web/api/docs.rb +17 -0
- data/lib/archsight/web/api/json_helpers.rb +164 -0
- data/lib/archsight/web/api/openapi/spec.yaml +500 -0
- data/lib/archsight/web/api/routes.rb +101 -0
- data/lib/archsight/web/application.rb +66 -43
- data/lib/archsight/web/doc/import.md +458 -0
- data/lib/archsight/web/doc/index.md.erb +2 -1
- data/lib/archsight/web/public/css/artifact.css +10 -0
- data/lib/archsight/web/public/css/graph.css +14 -0
- data/lib/archsight/web/public/css/instance.css +489 -0
- data/lib/archsight/web/views/api_docs.erb +19 -0
- data/lib/archsight/web/views/partials/artifact/_project_estimate.haml +14 -8
- data/lib/archsight/web/views/partials/instance/_analysis_detail.haml +74 -0
- data/lib/archsight/web/views/partials/instance/_analysis_result.haml +64 -0
- data/lib/archsight/web/views/partials/instance/_detail.haml +7 -3
- data/lib/archsight/web/views/partials/instance/_import_detail.haml +87 -0
- data/lib/archsight/web/views/partials/instance/_relations.haml +4 -4
- data/lib/archsight/web/views/partials/layout/_content.haml +4 -0
- data/lib/archsight/web/views/partials/layout/_navigation.haml +6 -5
- metadata +78 -1
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../annotations/relation_resolver"
|
|
4
|
+
|
|
5
|
+
module Archsight
|
|
6
|
+
module Analysis
|
|
7
|
+
# Sandbox provides a safe execution context for Analysis scripts.
|
|
8
|
+
# Scripts are evaluated via instance_eval with access only to
|
|
9
|
+
# explicitly defined methods.
|
|
10
|
+
class Sandbox
|
|
11
|
+
# Output sections collected during script execution
|
|
12
|
+
attr_reader :sections
|
|
13
|
+
|
|
14
|
+
def initialize(database)
|
|
15
|
+
@database = database
|
|
16
|
+
@sections = []
|
|
17
|
+
@resolver_cache = {}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Instance iteration methods
|
|
21
|
+
|
|
22
|
+
# Iterate over all instances of a kind
|
|
23
|
+
# @param kind [Symbol, String] Resource kind (e.g., :ApplicationService)
|
|
24
|
+
# @yield [instance] Block called for each instance
|
|
25
|
+
def each_instance(kind, &)
|
|
26
|
+
instances(kind).each(&)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Get all instances of a kind
|
|
30
|
+
# @param kind [Symbol, String] Resource kind
|
|
31
|
+
# @return [Array] Array of instances
|
|
32
|
+
def instances(kind)
|
|
33
|
+
@database.instances_by_kind(kind.to_s).values
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Get a specific instance by kind and name
|
|
37
|
+
# @param kind [Symbol, String] Resource kind
|
|
38
|
+
# @param name [String] Instance name
|
|
39
|
+
# @return [Object, nil] Instance or nil if not found
|
|
40
|
+
def instance(kind, name)
|
|
41
|
+
@database.instance_by_kind(kind.to_s, name)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Relation traversal methods (reuse ComputedRelationResolver)
|
|
45
|
+
|
|
46
|
+
# Get direct outgoing relations
|
|
47
|
+
# @param inst [Object] Source instance
|
|
48
|
+
# @param kind [Symbol, nil] Optional kind filter
|
|
49
|
+
# @return [Array] Related instances
|
|
50
|
+
def outgoing(inst, kind = nil)
|
|
51
|
+
resolver_for(inst).outgoing(kind)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Get transitive outgoing relations
|
|
55
|
+
# @param inst [Object] Source instance
|
|
56
|
+
# @param kind [Symbol, nil] Optional kind filter
|
|
57
|
+
# @param max_depth [Integer] Maximum traversal depth
|
|
58
|
+
# @return [Array] Transitively related instances
|
|
59
|
+
def outgoing_transitive(inst, kind = nil, max_depth: 10)
|
|
60
|
+
resolver_for(inst).outgoing_transitive(kind, max_depth: max_depth)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Get direct incoming relations (instances that reference this one)
|
|
64
|
+
# @param inst [Object] Target instance
|
|
65
|
+
# @param kind [Symbol, nil] Optional kind filter
|
|
66
|
+
# @return [Array] Instances referencing this one
|
|
67
|
+
def incoming(inst, kind = nil)
|
|
68
|
+
resolver_for(inst).incoming(kind)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Get transitive incoming relations
|
|
72
|
+
# @param inst [Object] Target instance
|
|
73
|
+
# @param kind [Symbol, nil] Optional kind filter
|
|
74
|
+
# @param max_depth [Integer] Maximum traversal depth
|
|
75
|
+
# @return [Array] Instances transitively referencing this one
|
|
76
|
+
def incoming_transitive(inst, kind = nil, max_depth: 10)
|
|
77
|
+
resolver_for(inst).incoming_transitive(kind, max_depth: max_depth)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Data access methods
|
|
81
|
+
|
|
82
|
+
# Get an annotation value from an instance
|
|
83
|
+
# @param inst [Object] Instance
|
|
84
|
+
# @param key [String] Annotation key
|
|
85
|
+
# @return [Object, nil] Annotation value
|
|
86
|
+
def annotation(inst, key)
|
|
87
|
+
inst.annotations[key]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Get all annotations from an instance (frozen copy)
|
|
91
|
+
# @param inst [Object] Instance
|
|
92
|
+
# @return [Hash] Frozen hash of annotations
|
|
93
|
+
def annotations(inst)
|
|
94
|
+
inst.annotations.dup.freeze
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Get the name of an instance
|
|
98
|
+
# @param inst [Object] Instance
|
|
99
|
+
# @return [String] Instance name
|
|
100
|
+
def name(inst)
|
|
101
|
+
inst.name
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Get the kind of an instance
|
|
105
|
+
# @param inst [Object] Instance
|
|
106
|
+
# @return [String] Instance kind
|
|
107
|
+
def kind(inst)
|
|
108
|
+
inst.klass
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Query method
|
|
112
|
+
|
|
113
|
+
# Execute a query string
|
|
114
|
+
# @param query_string [String] Query string
|
|
115
|
+
# @return [Array] Matching instances
|
|
116
|
+
def query(query_string)
|
|
117
|
+
@database.query(query_string)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Aggregation methods
|
|
121
|
+
|
|
122
|
+
# Sum numeric values
|
|
123
|
+
# @param values [Array<Numeric>] Values to sum
|
|
124
|
+
# @return [Numeric] Sum
|
|
125
|
+
def sum(values)
|
|
126
|
+
values.compact.sum
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Count items
|
|
130
|
+
# @param items [Array] Items to count
|
|
131
|
+
# @return [Integer] Count
|
|
132
|
+
def count(items)
|
|
133
|
+
items.compact.count
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Calculate average
|
|
137
|
+
# @param values [Array<Numeric>] Values to average
|
|
138
|
+
# @return [Float, nil] Average or nil if empty
|
|
139
|
+
def avg(values)
|
|
140
|
+
compact = values.compact
|
|
141
|
+
return nil if compact.empty?
|
|
142
|
+
|
|
143
|
+
compact.sum.to_f / compact.size
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Find minimum value
|
|
147
|
+
# @param values [Array<Numeric>] Values
|
|
148
|
+
# @return [Numeric, nil] Minimum
|
|
149
|
+
def min(values)
|
|
150
|
+
values.compact.min
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Find maximum value
|
|
154
|
+
# @param values [Array<Numeric>] Values
|
|
155
|
+
# @return [Numeric, nil] Maximum
|
|
156
|
+
def max(values)
|
|
157
|
+
values.compact.max
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Collect values into array
|
|
161
|
+
# @param items [Array] Items
|
|
162
|
+
# @param key [String, Symbol, nil] Optional key to extract
|
|
163
|
+
# @yield [item] Optional block to transform items
|
|
164
|
+
# @return [Array] Collected values
|
|
165
|
+
def collect(items, key = nil, &block)
|
|
166
|
+
if block
|
|
167
|
+
items.map(&block).compact
|
|
168
|
+
elsif key
|
|
169
|
+
items.map { |item| item.respond_to?(key) ? item.send(key) : item.annotations[key.to_s] }.compact
|
|
170
|
+
else
|
|
171
|
+
items.compact
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Group items by a key
|
|
176
|
+
# @param items [Array] Items to group
|
|
177
|
+
# @yield [item] Block that returns the grouping key
|
|
178
|
+
# @return [Hash] Grouped items
|
|
179
|
+
def group_by(items, &)
|
|
180
|
+
items.group_by(&)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Output methods - Structured content building
|
|
184
|
+
|
|
185
|
+
# Add a heading
|
|
186
|
+
# @param text [String] Heading text
|
|
187
|
+
# @param level [Integer] Heading level (0-6, default 0)
|
|
188
|
+
# Level 0 creates accordion sections in the web UI
|
|
189
|
+
# Levels 1-6 map to HTML h2-h6 within sections
|
|
190
|
+
def heading(text, level: 0)
|
|
191
|
+
@sections << { type: :heading, text: text, level: level.clamp(0, 6) }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Add a text paragraph
|
|
195
|
+
# @param content [String] Text content (supports markdown)
|
|
196
|
+
def text(content)
|
|
197
|
+
@sections << { type: :text, content: content }
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Add a table
|
|
201
|
+
# @param headers [Array<String>] Column headers
|
|
202
|
+
# @param rows [Array<Array>] Table rows (each row is an array of values)
|
|
203
|
+
def table(headers:, rows:)
|
|
204
|
+
return if rows.empty?
|
|
205
|
+
|
|
206
|
+
@sections << { type: :table, headers: headers, rows: rows }
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Add a bullet list
|
|
210
|
+
# @param items [Array<String, Hash>] List items (strings or hashes with :text key)
|
|
211
|
+
def list(items)
|
|
212
|
+
return if items.empty?
|
|
213
|
+
|
|
214
|
+
@sections << { type: :list, items: items }
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Add a code block
|
|
218
|
+
# @param content [String] Code content
|
|
219
|
+
# @param lang [String] Language for syntax highlighting (default: plain)
|
|
220
|
+
def code(content, lang: "")
|
|
221
|
+
@sections << { type: :code, content: content, lang: lang }
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Legacy output methods - still supported for backward compatibility
|
|
225
|
+
|
|
226
|
+
# Report findings (legacy method - creates appropriate section based on data type)
|
|
227
|
+
# @param data [Object] Report data (usually Array or Hash)
|
|
228
|
+
# @param title [String, nil] Optional report title
|
|
229
|
+
def report(data, title: nil)
|
|
230
|
+
heading(title, level: 1) if title
|
|
231
|
+
|
|
232
|
+
case data
|
|
233
|
+
when Array
|
|
234
|
+
report_array(data)
|
|
235
|
+
when Hash
|
|
236
|
+
report_hash(data)
|
|
237
|
+
else
|
|
238
|
+
text(data.to_s)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Log a warning message
|
|
243
|
+
# @param message [String] Warning message
|
|
244
|
+
def warning(message)
|
|
245
|
+
@sections << { type: :message, level: :warning, message: message }
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Log an error message
|
|
249
|
+
# @param message [String] Error message
|
|
250
|
+
def error(message)
|
|
251
|
+
@sections << { type: :message, level: :error, message: message }
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Log an info message
|
|
255
|
+
# @param message [String] Info message
|
|
256
|
+
def info(message)
|
|
257
|
+
@sections << { type: :message, level: :info, message: message }
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Configuration access
|
|
261
|
+
|
|
262
|
+
# Get a configuration value from the analysis
|
|
263
|
+
# @param key [String] Configuration key (without 'analysis/config/' prefix)
|
|
264
|
+
# @return [Object, nil] Configuration value
|
|
265
|
+
def config(key)
|
|
266
|
+
@current_analysis&.annotations&.[]("analysis/config/#{key}")
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Set the current analysis being executed (called by Executor)
|
|
270
|
+
# @param analysis [Object] Analysis instance
|
|
271
|
+
# @api private
|
|
272
|
+
def _set_analysis(analysis)
|
|
273
|
+
@current_analysis = analysis
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
private
|
|
277
|
+
|
|
278
|
+
# Get or create a relation resolver for an instance
|
|
279
|
+
def resolver_for(inst)
|
|
280
|
+
@resolver_cache[inst.name] ||= Archsight::Annotations::ComputedRelationResolver.new(inst, @database)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Report an array - auto-detect if it's a table or list
|
|
284
|
+
def report_array(data)
|
|
285
|
+
return if data.empty?
|
|
286
|
+
|
|
287
|
+
# If array of hashes with consistent keys, render as table
|
|
288
|
+
if data.all? { |item| item.is_a?(Hash) }
|
|
289
|
+
keys = data.first.keys
|
|
290
|
+
if data.all? { |item| item.keys == keys }
|
|
291
|
+
table(headers: keys.map(&:to_s), rows: data.map { |item| keys.map { |k| item[k] } })
|
|
292
|
+
return
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Otherwise render as list
|
|
297
|
+
list(data.map { |item| format_list_item(item) })
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Report a hash - render as definition list or table
|
|
301
|
+
def report_hash(data)
|
|
302
|
+
return if data.empty?
|
|
303
|
+
|
|
304
|
+
# Render as simple key-value list
|
|
305
|
+
list(data.map { |k, v| "**#{k}:** #{v}" })
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Format a list item for display
|
|
309
|
+
def format_list_item(item)
|
|
310
|
+
case item
|
|
311
|
+
when Hash
|
|
312
|
+
item.map { |k, v| "#{k}=#{v}" }.join(", ")
|
|
313
|
+
else
|
|
314
|
+
item.to_s
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Archsight
|
|
4
|
+
# Analysis module provides sandboxed execution of Analysis scripts
|
|
5
|
+
module Analysis
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
require_relative "analysis/result"
|
|
10
|
+
require_relative "analysis/sandbox"
|
|
11
|
+
require_relative "analysis/executor"
|
|
@@ -46,9 +46,9 @@ module Archsight::Annotations::Architecture
|
|
|
46
46
|
filter: :word,
|
|
47
47
|
title: "Status"
|
|
48
48
|
annotation "architecture/visibility",
|
|
49
|
-
description: "API visibility (public, private)",
|
|
49
|
+
description: "API visibility (public, private, internal)",
|
|
50
50
|
filter: :word,
|
|
51
|
-
enum: %w[public private],
|
|
51
|
+
enum: %w[public private internal],
|
|
52
52
|
title: "Visibility"
|
|
53
53
|
annotation "architecture/applicationSets",
|
|
54
54
|
description: "Related ArgoCD ApplicationSets",
|
data/lib/archsight/cli.rb
CHANGED
|
@@ -4,6 +4,10 @@ require "thor"
|
|
|
4
4
|
|
|
5
5
|
module Archsight
|
|
6
6
|
class CLI < Thor
|
|
7
|
+
def self.exit_on_failure?
|
|
8
|
+
true
|
|
9
|
+
end
|
|
10
|
+
|
|
7
11
|
class_option :resources,
|
|
8
12
|
aliases: "-r",
|
|
9
13
|
type: :string,
|
|
@@ -12,10 +16,21 @@ module Archsight
|
|
|
12
16
|
desc "web", "Start the web server"
|
|
13
17
|
option :port, aliases: "-p", type: :numeric, default: 4567, desc: "Port to listen on"
|
|
14
18
|
option :host, aliases: "-H", type: :string, default: "localhost", desc: "Host to bind to"
|
|
19
|
+
option :production, type: :boolean, default: false, desc: "Run in production mode"
|
|
20
|
+
option :disable_reload, type: :boolean, default: false, desc: "Disable the reload button in the UI"
|
|
21
|
+
option :enable_logging, type: :boolean, default: nil, desc: "Enable request logging (default: false in dev, true in prod)"
|
|
15
22
|
def web
|
|
16
23
|
configure_resources
|
|
17
24
|
require "archsight/web/application"
|
|
25
|
+
|
|
26
|
+
env = options[:production] ? :production : :development
|
|
27
|
+
Archsight::Web::Application.configure_environment!(env, logging: options[:enable_logging])
|
|
28
|
+
Archsight::Web::Application.set :reload_enabled, !options[:disable_reload]
|
|
29
|
+
Archsight::Web::Application.setup_mcp!
|
|
18
30
|
Archsight::Web::Application.run!(port: options[:port], bind: options[:host])
|
|
31
|
+
rescue Archsight::ResourceError => e
|
|
32
|
+
display_error_with_context(e.to_s)
|
|
33
|
+
exit 1
|
|
19
34
|
end
|
|
20
35
|
|
|
21
36
|
desc "lint", "Validate architecture resources"
|
|
@@ -70,6 +85,80 @@ module Archsight
|
|
|
70
85
|
binding.irb
|
|
71
86
|
end
|
|
72
87
|
|
|
88
|
+
desc "import", "Execute pending imports"
|
|
89
|
+
option :verbose, aliases: "-v", type: :boolean, default: false, desc: "Verbose output"
|
|
90
|
+
option :dry_run, aliases: "-n", type: :boolean, default: false, desc: "Show execution plan without running"
|
|
91
|
+
option :filter, aliases: "-f", type: :string, desc: "Filter imports by name (regex pattern)"
|
|
92
|
+
option :force, aliases: "-F", type: :boolean, default: false, desc: "Ignore cache and re-run all imports"
|
|
93
|
+
def import
|
|
94
|
+
configure_resources
|
|
95
|
+
require "archsight/database"
|
|
96
|
+
require "archsight/import/executor"
|
|
97
|
+
|
|
98
|
+
resources_dir = Archsight.resources_dir
|
|
99
|
+
|
|
100
|
+
# Load all handlers
|
|
101
|
+
require_import_handlers
|
|
102
|
+
|
|
103
|
+
# Create database that loads from resources directory
|
|
104
|
+
# Only load Import resources to avoid validation errors on incomplete resources
|
|
105
|
+
db = Archsight::Database.new(resources_dir, verbose: options[:verbose], only_kinds: ["Import"], verify: false)
|
|
106
|
+
|
|
107
|
+
if options[:dry_run]
|
|
108
|
+
puts "Execution Plan:"
|
|
109
|
+
executor = Archsight::Import::Executor.new(
|
|
110
|
+
database: db,
|
|
111
|
+
resources_dir: resources_dir,
|
|
112
|
+
verbose: true,
|
|
113
|
+
filter: options[:filter],
|
|
114
|
+
force: options[:force]
|
|
115
|
+
)
|
|
116
|
+
executor.execution_plan
|
|
117
|
+
else
|
|
118
|
+
executor = Archsight::Import::Executor.new(
|
|
119
|
+
database: db,
|
|
120
|
+
resources_dir: resources_dir,
|
|
121
|
+
verbose: options[:verbose],
|
|
122
|
+
filter: options[:filter],
|
|
123
|
+
force: options[:force]
|
|
124
|
+
)
|
|
125
|
+
executor.run!
|
|
126
|
+
puts "All imports completed successfully."
|
|
127
|
+
end
|
|
128
|
+
rescue Archsight::Import::InterruptedError
|
|
129
|
+
# Graceful shutdown already handled by executor
|
|
130
|
+
exit 130
|
|
131
|
+
rescue Archsight::Import::DeadlockError => e
|
|
132
|
+
puts "Error: #{e.message}"
|
|
133
|
+
exit 1
|
|
134
|
+
rescue Archsight::Import::ImportError => e
|
|
135
|
+
puts "Error: #{e.message}"
|
|
136
|
+
exit 1
|
|
137
|
+
rescue Archsight::ResourceError => e
|
|
138
|
+
display_error_with_context(e.to_s)
|
|
139
|
+
exit 1
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
desc "analyze", "Execute analysis scripts"
|
|
143
|
+
option :verbose, aliases: "-v", type: :boolean, default: false, desc: "Verbose output"
|
|
144
|
+
option :dry_run, aliases: "-n", type: :boolean, default: false, desc: "List analyses without running"
|
|
145
|
+
option :filter, aliases: "-f", type: :string, desc: "Filter analyses by name (regex pattern)"
|
|
146
|
+
def analyze
|
|
147
|
+
configure_resources
|
|
148
|
+
require "archsight/database"
|
|
149
|
+
require "archsight/analysis"
|
|
150
|
+
|
|
151
|
+
db = load_database_for_analysis
|
|
152
|
+
analyses = filter_analyses(db)
|
|
153
|
+
|
|
154
|
+
return puts("No analyses found#{" matching '#{options[:filter]}'" if options[:filter]}.") if analyses.empty?
|
|
155
|
+
return print_analysis_dry_run(analyses) if options[:dry_run]
|
|
156
|
+
|
|
157
|
+
results = execute_analyses(db, analyses)
|
|
158
|
+
print_analysis_results(results)
|
|
159
|
+
exit 1 if results.any?(&:failed?)
|
|
160
|
+
end
|
|
161
|
+
|
|
73
162
|
desc "version", "Show version"
|
|
74
163
|
def version
|
|
75
164
|
puts "archsight #{Archsight::VERSION}"
|
|
@@ -83,6 +172,81 @@ module Archsight
|
|
|
83
172
|
Archsight.resources_dir = options[:resources] if options[:resources]
|
|
84
173
|
end
|
|
85
174
|
|
|
175
|
+
def require_import_handlers
|
|
176
|
+
handlers_dir = File.expand_path("import/handlers", __dir__)
|
|
177
|
+
Dir.glob(File.join(handlers_dir, "*.rb")).each do |handler_file|
|
|
178
|
+
require handler_file
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def load_database_for_analysis
|
|
183
|
+
db = Archsight::Database.new(Archsight.resources_dir, verbose: options[:verbose])
|
|
184
|
+
db.reload!
|
|
185
|
+
db
|
|
186
|
+
rescue Archsight::ResourceError => e
|
|
187
|
+
display_error_with_context(e.to_s)
|
|
188
|
+
exit 1
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def filter_analyses(db)
|
|
192
|
+
analyses = db.instances_by_kind("Analysis").values
|
|
193
|
+
analyses = analyses.select { |a| Regexp.new(options[:filter], Regexp::IGNORECASE).match?(a.name) } if options[:filter]
|
|
194
|
+
analyses.reject { |a| a.annotations["analysis/enabled"] == "false" }
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def print_analysis_dry_run(analyses)
|
|
198
|
+
puts "Analyses to run#{" (filter: #{options[:filter]})" if options[:filter]}:"
|
|
199
|
+
analyses.sort_by(&:name).each_with_index do |analysis, idx|
|
|
200
|
+
timeout = analysis.annotations["analysis/timeout"] || "30s"
|
|
201
|
+
desc = analysis.annotations["analysis/description"] || "(no description)"
|
|
202
|
+
puts " #{idx + 1}. #{analysis.name} [#{timeout}]"
|
|
203
|
+
puts " #{desc}"
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def execute_analyses(db, analyses)
|
|
208
|
+
executor = Archsight::Analysis::Executor.new(db)
|
|
209
|
+
analyses.map { |analysis| executor.execute(analysis) }
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def print_analysis_results(results)
|
|
213
|
+
require "tty-markdown"
|
|
214
|
+
|
|
215
|
+
results.each do |result|
|
|
216
|
+
print_single_result(result)
|
|
217
|
+
puts ""
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
summary_md = build_analysis_summary_markdown(results)
|
|
221
|
+
puts TTY::Markdown.parse(summary_md)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def print_single_result(result)
|
|
225
|
+
# Print status header
|
|
226
|
+
header = "# #{result.status_emoji} #{result.name}"
|
|
227
|
+
header += " (#{result.duration_str})" unless result.duration_str.empty?
|
|
228
|
+
puts TTY::Markdown.parse(header)
|
|
229
|
+
|
|
230
|
+
# Print error if failed
|
|
231
|
+
puts TTY::Markdown.parse(result.error_markdown(verbose: options[:verbose])) if result.failed?
|
|
232
|
+
|
|
233
|
+
# Print script output
|
|
234
|
+
output = result.to_s(verbose: options[:verbose])
|
|
235
|
+
puts output unless output.empty?
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def build_analysis_summary_markdown(results)
|
|
239
|
+
passed = results.count(&:success?)
|
|
240
|
+
failed = results.count(&:failed?)
|
|
241
|
+
with_findings = results.count(&:has_findings?)
|
|
242
|
+
|
|
243
|
+
lines = ["---", "", "# Summary", ""]
|
|
244
|
+
lines << "- ✅ **#{passed}** passed"
|
|
245
|
+
lines << "- ❌ **#{failed}** failed" if failed.positive?
|
|
246
|
+
lines << "- ⚠️ **#{with_findings}** with findings" if with_findings.positive?
|
|
247
|
+
lines.join("\n")
|
|
248
|
+
end
|
|
249
|
+
|
|
86
250
|
def list_kinds
|
|
87
251
|
puts "Available resource kinds:\n\n"
|
|
88
252
|
Archsight::Resources.resource_classes.each_key { |kind| puts " - #{kind}" }
|
data/lib/archsight/database.rb
CHANGED
|
@@ -43,13 +43,14 @@ module Archsight
|
|
|
43
43
|
# of the structure. The loading and parsing of files will raise errors
|
|
44
44
|
# if invalid data is passed.
|
|
45
45
|
class Database
|
|
46
|
-
attr_accessor :instances, :verbose, :verify, :compute_annotations
|
|
46
|
+
attr_accessor :instances, :verbose, :verify, :compute_annotations, :only_kinds
|
|
47
47
|
|
|
48
|
-
def initialize(path, verbose: false, verify: true, compute_annotations: true)
|
|
48
|
+
def initialize(path, verbose: false, verify: true, compute_annotations: true, only_kinds: nil)
|
|
49
49
|
@path = path
|
|
50
50
|
@verbose = verbose
|
|
51
51
|
@verify = verify
|
|
52
52
|
@compute_annotations = compute_annotations
|
|
53
|
+
@only_kinds = only_kinds
|
|
53
54
|
@instances = {}
|
|
54
55
|
end
|
|
55
56
|
|
|
@@ -127,6 +128,9 @@ module Archsight
|
|
|
127
128
|
obj = node.to_ruby
|
|
128
129
|
next unless obj # skip empty / unknown documents
|
|
129
130
|
|
|
131
|
+
# Skip resources that don't match only_kinds filter
|
|
132
|
+
next if @only_kinds && !@only_kinds.include?(obj["kind"])
|
|
133
|
+
|
|
130
134
|
self << create_valid_instance(obj)
|
|
131
135
|
end
|
|
132
136
|
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rack/utils"
|
|
4
|
+
|
|
5
|
+
module Archsight
|
|
6
|
+
module Helpers
|
|
7
|
+
# AnalysisRenderer provides HTML rendering for analysis result sections
|
|
8
|
+
module AnalysisRenderer
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# HTML escape helper
|
|
12
|
+
def escape_html(text)
|
|
13
|
+
Rack::Utils.escape_html(text.to_s)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Render a single analysis section to HTML
|
|
17
|
+
# @param section [Hash] Section data with :type and content
|
|
18
|
+
# @param markdown_renderer [Proc, nil] Optional proc to render markdown
|
|
19
|
+
def render_analysis_section(section, markdown_renderer: nil)
|
|
20
|
+
case section[:type]
|
|
21
|
+
when :heading
|
|
22
|
+
render_heading(section)
|
|
23
|
+
when :text
|
|
24
|
+
render_text(section, markdown_renderer)
|
|
25
|
+
when :message
|
|
26
|
+
render_message(section)
|
|
27
|
+
when :table
|
|
28
|
+
render_table(section)
|
|
29
|
+
when :list
|
|
30
|
+
render_list(section)
|
|
31
|
+
when :code
|
|
32
|
+
render_code(section)
|
|
33
|
+
else
|
|
34
|
+
""
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Render analysis table section to HTML
|
|
39
|
+
def render_analysis_table(section)
|
|
40
|
+
render_table(section)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Private rendering methods
|
|
44
|
+
|
|
45
|
+
def render_heading(section)
|
|
46
|
+
%(<div class="analysis-heading level-#{section[:level]}">#{escape_html(section[:text])}</div>)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def render_text(section, markdown_renderer)
|
|
50
|
+
content = markdown_renderer ? markdown_renderer.call(section[:content]) : escape_html(section[:content])
|
|
51
|
+
%(<div class="analysis-text">#{content}</div>)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def render_message(section)
|
|
55
|
+
icon = case section[:level]
|
|
56
|
+
when :error then "xmark-circle"
|
|
57
|
+
when :warning then "warning-triangle"
|
|
58
|
+
else "info-circle"
|
|
59
|
+
end
|
|
60
|
+
%(<div class="analysis-message message-#{section[:level]}"><i class="iconoir-#{icon}"></i> #{escape_html(section[:message])}</div>)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def render_table(section)
|
|
64
|
+
headers = section[:headers].map { |h| "<th>#{escape_html(h)}</th>" }.join
|
|
65
|
+
rows = section[:rows].map do |row|
|
|
66
|
+
cells = row.map { |cell| "<td>#{escape_html(cell.to_s)}</td>" }.join
|
|
67
|
+
"<tr>#{cells}</tr>"
|
|
68
|
+
end.join
|
|
69
|
+
%(<div class="analysis-table-wrapper"><table><thead><tr>#{headers}</tr></thead><tbody>#{rows}</tbody></table></div>)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def render_list(section)
|
|
73
|
+
items = section[:items].map { |item| "<li>#{escape_html(item)}</li>" }.join
|
|
74
|
+
%(<ul class="analysis-list">#{items}</ul>)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def render_code(section)
|
|
78
|
+
lang_class = section[:lang] ? " class=\"language-#{section[:lang]}\"" : ""
|
|
79
|
+
%(<pre class="code"><code#{lang_class}>#{escape_html(section[:content])}</code></pre>)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|