lex-cost-scanner 0.1.0
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 +7 -0
- data/CHANGELOG.md +10 -0
- data/README.md +41 -0
- data/lib/legion/extensions/cost_scanner/actors/weekly_scan.rb +39 -0
- data/lib/legion/extensions/cost_scanner/helpers/classifier.rb +76 -0
- data/lib/legion/extensions/cost_scanner/helpers/constants.rb +17 -0
- data/lib/legion/extensions/cost_scanner/helpers/findings_store.rb +71 -0
- data/lib/legion/extensions/cost_scanner/runners/reporter.rb +67 -0
- data/lib/legion/extensions/cost_scanner/runners/scanner.rb +73 -0
- data/lib/legion/extensions/cost_scanner/version.rb +9 -0
- data/lib/legion/extensions/cost_scanner.rb +18 -0
- metadata +150 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c65ca54b3c8ef026aa4fa0d3549159b63b00df80b3abc0f1a0b96b6586e4ac0a
|
|
4
|
+
data.tar.gz: a0b4e81649991adffaf07a5cf1fef039471a91839c8a285e8b1a088b3d98559d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: efadba80fb8343108fc6c231e7d7b801feddaba3080dc12f2fd8221ad23a22a9d2fda8abee23fb9c7c061446af607c73070c69a1b331df408f5de28ae7ba079b
|
|
7
|
+
data.tar.gz: e0befc34946b3ea011a04ffd026be4f3206bb781ce824d7c6c415f78fb46bb53ee4b29ffd9b6638ffeeb79a8e59a4fdc18e18bee0734c0bc38a20bab544aafa8
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2026-03-24
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Resource classifier with LLM + rule-based fallback (idle < 5% CPU, oversized 5-20%)
|
|
7
|
+
- Findings store with deduplication by account + resource + finding type
|
|
8
|
+
- Scanner runner: `scan_account`, `scan_all`, `scan_stats`
|
|
9
|
+
- Reporter runner: `generate_report`, `format_slack_blocks`, `post_report`
|
|
10
|
+
- WeeklyScan interval actor (604800s / 1 week)
|
data/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# lex-cost-scanner
|
|
2
|
+
|
|
3
|
+
Cloud cost optimization scanner for LegionIO. Scans AWS/Azure accounts for idle and oversized resources, classifies findings via LLM (with rule-based fallback), deduplicates across scans, and delivers a weekly Slack Block Kit report.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
- **WeeklyScan actor** — interval actor fires `scan_all` every 604,800 seconds (1 week)
|
|
8
|
+
- **Scanner runner** — iterates configured accounts, fetches resources, classifies each one
|
|
9
|
+
- **Classifier helper** — uses `legion-llm` if available; falls back to CPU utilization rules
|
|
10
|
+
- **FindingsStore** — thread-safe in-memory store with dedup by `account_id:resource_id:finding_type`
|
|
11
|
+
- **Reporter runner** — generates Slack Block Kit weekly summary and posts via `lex-slack`
|
|
12
|
+
|
|
13
|
+
## Settings
|
|
14
|
+
|
|
15
|
+
```yaml
|
|
16
|
+
cost_scanner:
|
|
17
|
+
slack_webhook: "https://hooks.slack.com/services/..."
|
|
18
|
+
accounts:
|
|
19
|
+
- id: "123456789012"
|
|
20
|
+
cloud: "aws"
|
|
21
|
+
- id: "my-gcp-project"
|
|
22
|
+
cloud: "gcp"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Classification Rules (rule-based fallback)
|
|
26
|
+
|
|
27
|
+
| CPU Avg | Finding | Severity |
|
|
28
|
+
|---------|---------|----------|
|
|
29
|
+
| < 5% | idle | high |
|
|
30
|
+
| 5–20% | oversized | medium |
|
|
31
|
+
| >= 20% | none | — |
|
|
32
|
+
|
|
33
|
+
Resources costing less than $50/month are skipped (`MIN_MONTHLY_COST`).
|
|
34
|
+
|
|
35
|
+
## Development
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
bundle install
|
|
39
|
+
bundle exec rspec
|
|
40
|
+
bundle exec rubocop
|
|
41
|
+
```
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module CostScanner
|
|
6
|
+
module Actors
|
|
7
|
+
class WeeklyScan < Legion::Extensions::Actors::Every
|
|
8
|
+
def time
|
|
9
|
+
604_800
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def runner_class
|
|
13
|
+
'Legion::Extensions::CostScanner::Runners::Scanner'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def runner_function
|
|
17
|
+
'scan_all'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def run_now?
|
|
21
|
+
false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def check_subtask?
|
|
25
|
+
false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def generate_task?
|
|
29
|
+
false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def use_runner?
|
|
33
|
+
false
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module CostScanner
|
|
6
|
+
module Helpers
|
|
7
|
+
module Classifier
|
|
8
|
+
extend Constants
|
|
9
|
+
|
|
10
|
+
CLASSIFY_PROMPT = <<~PROMPT
|
|
11
|
+
Analyze this cloud resource and classify it. Reply with ONLY valid JSON:
|
|
12
|
+
{"finding_type":"idle|oversized|unused_reservation|orphaned_storage|rightsizing|none",
|
|
13
|
+
"severity":"critical|high|medium|low|info",
|
|
14
|
+
"estimated_monthly_savings":0.0,
|
|
15
|
+
"recommendation":"one sentence action"}
|
|
16
|
+
|
|
17
|
+
Resource: %<resource_type>s %<resource_id>s
|
|
18
|
+
Monthly cost: $%<monthly_cost>.2f
|
|
19
|
+
Utilization: %<utilization>s
|
|
20
|
+
PROMPT
|
|
21
|
+
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
def classify(resource_id:, resource_type:, monthly_cost:, utilization:)
|
|
25
|
+
if llm_available?
|
|
26
|
+
llm_classify(resource_id: resource_id, resource_type: resource_type,
|
|
27
|
+
monthly_cost: monthly_cost, utilization: utilization)
|
|
28
|
+
else
|
|
29
|
+
result = rule_based_classify(utilization: utilization, monthly_cost: monthly_cost)
|
|
30
|
+
result.merge(resource_id: resource_id, resource_type: resource_type, method: :rule_based)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def rule_based_classify(utilization:, monthly_cost:)
|
|
35
|
+
cpu = utilization[:cpu_avg] || 100.0
|
|
36
|
+
|
|
37
|
+
if cpu < Constants::IDLE_CPU_THRESHOLD
|
|
38
|
+
{ finding_type: :idle, severity: :high,
|
|
39
|
+
estimated_monthly_savings: monthly_cost,
|
|
40
|
+
recommendation: 'Terminate or stop idle resource' }
|
|
41
|
+
elsif cpu < Constants::OVERSIZED_CPU_THRESHOLD
|
|
42
|
+
savings = monthly_cost * 0.4
|
|
43
|
+
{ finding_type: :oversized, severity: :medium,
|
|
44
|
+
estimated_monthly_savings: savings,
|
|
45
|
+
recommendation: 'Rightsize to smaller instance type' }
|
|
46
|
+
else
|
|
47
|
+
{ finding_type: :none, severity: :info,
|
|
48
|
+
estimated_monthly_savings: 0.0,
|
|
49
|
+
recommendation: 'Resource is adequately utilized' }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def llm_classify(resource_id:, resource_type:, monthly_cost:, utilization:)
|
|
54
|
+
prompt = format(CLASSIFY_PROMPT, resource_type: resource_type, resource_id: resource_id,
|
|
55
|
+
monthly_cost: monthly_cost, utilization: utilization.inspect)
|
|
56
|
+
response = Legion::LLM.chat(
|
|
57
|
+
message: prompt,
|
|
58
|
+
caller: { extension: 'lex-cost-scanner', function: 'classify' }
|
|
59
|
+
)
|
|
60
|
+
parsed = Legion::JSON.load(response)
|
|
61
|
+
parsed[:finding_type] = parsed[:finding_type].to_sym
|
|
62
|
+
parsed[:severity] = parsed[:severity].to_sym
|
|
63
|
+
parsed.merge(resource_id: resource_id, resource_type: resource_type, method: :llm)
|
|
64
|
+
rescue StandardError
|
|
65
|
+
result = rule_based_classify(utilization: utilization, monthly_cost: monthly_cost)
|
|
66
|
+
result.merge(resource_id: resource_id, resource_type: resource_type, method: :rule_based_fallback)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def llm_available?
|
|
70
|
+
defined?(Legion::LLM) && Legion::LLM.respond_to?(:started?) && Legion::LLM.started?
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module CostScanner
|
|
6
|
+
module Helpers
|
|
7
|
+
module Constants
|
|
8
|
+
FINDING_TYPES = %i[idle oversized unused_reservation orphaned_storage rightsizing none].freeze
|
|
9
|
+
SEVERITIES = %i[critical high medium low info].freeze
|
|
10
|
+
IDLE_CPU_THRESHOLD = 5.0
|
|
11
|
+
OVERSIZED_CPU_THRESHOLD = 20.0
|
|
12
|
+
MIN_MONTHLY_COST = 50.0
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module CostScanner
|
|
6
|
+
module Helpers
|
|
7
|
+
module FindingsStore
|
|
8
|
+
@mutex = Mutex.new
|
|
9
|
+
@findings = {}
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def dedup_key(account_id:, resource_id:, finding_type:, **)
|
|
14
|
+
"#{account_id}:#{resource_id}:#{finding_type}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def record(account_id:, resource_id:, finding_type:, **attrs)
|
|
18
|
+
key = dedup_key(account_id: account_id, resource_id: resource_id, finding_type: finding_type)
|
|
19
|
+
now = Time.now
|
|
20
|
+
|
|
21
|
+
@mutex.synchronize do
|
|
22
|
+
if @findings.key?(key)
|
|
23
|
+
@findings[key][:last_seen] = now
|
|
24
|
+
@findings[key][:scan_count] += 1
|
|
25
|
+
return { new: false, key: key }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
@findings[key] = {
|
|
29
|
+
account_id: account_id, resource_id: resource_id, finding_type: finding_type,
|
|
30
|
+
first_seen: now, last_seen: now, scan_count: 1, status: :new
|
|
31
|
+
}.merge(attrs)
|
|
32
|
+
{ new: true, key: key }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def all
|
|
37
|
+
@mutex.synchronize { @findings.values.dup }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def new_since(time)
|
|
41
|
+
@mutex.synchronize { @findings.values.select { |f| f[:first_seen] >= time } }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def total_savings
|
|
45
|
+
@mutex.synchronize { @findings.values.sum { |f| f[:estimated_monthly_savings] || 0.0 } }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def top_by_savings(limit: 10)
|
|
49
|
+
@mutex.synchronize do
|
|
50
|
+
@findings.values
|
|
51
|
+
.sort_by { |f| -(f[:estimated_monthly_savings] || 0.0) }
|
|
52
|
+
.first(limit)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def stats
|
|
57
|
+
@mutex.synchronize do
|
|
58
|
+
{ total: @findings.size,
|
|
59
|
+
total_savings: @findings.values.sum { |f| f[:estimated_monthly_savings] || 0.0 },
|
|
60
|
+
by_type: @findings.values.group_by { |f| f[:finding_type] }.transform_values(&:size) }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def reset!
|
|
65
|
+
@mutex.synchronize { @findings.clear }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module CostScanner
|
|
6
|
+
module Runners
|
|
7
|
+
module Reporter
|
|
8
|
+
def generate_report(limit: 10)
|
|
9
|
+
top = Helpers::FindingsStore.top_by_savings(limit: limit)
|
|
10
|
+
stats = Helpers::FindingsStore.stats
|
|
11
|
+
|
|
12
|
+
{ total_savings: stats[:total_savings],
|
|
13
|
+
findings_count: stats[:total],
|
|
14
|
+
by_type: stats[:by_type],
|
|
15
|
+
top_findings: top,
|
|
16
|
+
generated_at: Time.now }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def format_slack_blocks(report:)
|
|
20
|
+
blocks = []
|
|
21
|
+
savings = format('%.2f', report[:total_savings])
|
|
22
|
+
|
|
23
|
+
blocks << { type: 'header', text: { type: 'plain_text', text: 'Weekly Cost Optimization Report' } }
|
|
24
|
+
blocks << { type: 'section', text: { type: 'mrkdwn',
|
|
25
|
+
text: "*Total potential savings:* $#{savings}/month\n" \
|
|
26
|
+
"*Findings:* #{report[:findings_count]}" } }
|
|
27
|
+
blocks << { type: 'divider' }
|
|
28
|
+
|
|
29
|
+
(report[:top_findings] || []).each_with_index do |finding, i|
|
|
30
|
+
blocks << { type: 'section', text: { type: 'mrkdwn',
|
|
31
|
+
text: "*#{i + 1}. #{finding[:resource_id]}* (#{finding[:finding_type]})\n" \
|
|
32
|
+
"Savings: $#{format('%.2f',
|
|
33
|
+
finding[:estimated_monthly_savings] || 0)}/month\n" \
|
|
34
|
+
"#{finding[:recommendation]}" } }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
blocks
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def post_report(limit: 10)
|
|
41
|
+
report = generate_report(limit: limit)
|
|
42
|
+
format_slack_blocks(report: report)
|
|
43
|
+
|
|
44
|
+
webhook = report_webhook
|
|
45
|
+
if webhook && defined?(Legion::Extensions::Slack::Client)
|
|
46
|
+
message = "Cost Optimization: $#{format('%.2f', report[:total_savings])}/month in savings identified"
|
|
47
|
+
Legion::Extensions::Slack::Client.new.send_webhook(message: message, webhook: webhook)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
{ success: true, report: report }
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
{ success: false, error: e.message }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def report_webhook
|
|
58
|
+
return nil unless defined?(Legion::Settings)
|
|
59
|
+
|
|
60
|
+
config = Legion::Settings[:cost_scanner] || {}
|
|
61
|
+
config[:slack_webhook]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module CostScanner
|
|
6
|
+
module Runners
|
|
7
|
+
module Scanner
|
|
8
|
+
def scan_all
|
|
9
|
+
accounts = scanner_config[:accounts] || []
|
|
10
|
+
results = accounts.map { |acct| scan_account(account_id: acct[:id], cloud: acct[:cloud]) }
|
|
11
|
+
|
|
12
|
+
{ success: true, accounts_scanned: results.size, results: results,
|
|
13
|
+
total_savings: Helpers::FindingsStore.total_savings }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def scan_account(account_id:, cloud: 'aws')
|
|
17
|
+
resources = fetch_resources(account_id: account_id, cloud: cloud)
|
|
18
|
+
findings_count = resources.count do |resource|
|
|
19
|
+
process_resource(account_id: account_id, resource: resource)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
{ success: true, account_id: account_id, scanned: resources.size,
|
|
23
|
+
findings: findings_count }
|
|
24
|
+
rescue StandardError => e
|
|
25
|
+
{ success: false, account_id: account_id, error: e.message }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def scan_stats
|
|
29
|
+
Helpers::FindingsStore.stats
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def scanner_config
|
|
35
|
+
return {} unless defined?(Legion::Settings)
|
|
36
|
+
|
|
37
|
+
Legion::Settings[:cost_scanner] || {}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def process_resource(account_id:, resource:)
|
|
41
|
+
return false if (resource[:monthly_cost] || 0) < Helpers::Constants::MIN_MONTHLY_COST
|
|
42
|
+
|
|
43
|
+
classification = Helpers::Classifier.classify(
|
|
44
|
+
resource_id: resource[:resource_id],
|
|
45
|
+
resource_type: resource[:resource_type],
|
|
46
|
+
monthly_cost: resource[:monthly_cost],
|
|
47
|
+
utilization: resource[:utilization] || {}
|
|
48
|
+
)
|
|
49
|
+
return false if classification[:finding_type] == :none
|
|
50
|
+
|
|
51
|
+
result = Helpers::FindingsStore.record(
|
|
52
|
+
account_id: account_id,
|
|
53
|
+
resource_id: resource[:resource_id],
|
|
54
|
+
resource_type: resource[:resource_type],
|
|
55
|
+
finding_type: classification[:finding_type],
|
|
56
|
+
severity: classification[:severity],
|
|
57
|
+
monthly_cost: resource[:monthly_cost],
|
|
58
|
+
estimated_monthly_savings: classification[:estimated_monthly_savings],
|
|
59
|
+
recommendation: classification[:recommendation]
|
|
60
|
+
)
|
|
61
|
+
result[:new]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def fetch_resources(_account_id:, _cloud: 'aws')
|
|
65
|
+
return [] unless defined?(Legion::Extensions::Http::Client)
|
|
66
|
+
|
|
67
|
+
[]
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'cost_scanner/version'
|
|
4
|
+
require_relative 'cost_scanner/helpers/constants'
|
|
5
|
+
require_relative 'cost_scanner/helpers/classifier'
|
|
6
|
+
require_relative 'cost_scanner/helpers/findings_store'
|
|
7
|
+
require_relative 'cost_scanner/runners/scanner'
|
|
8
|
+
require_relative 'cost_scanner/runners/reporter'
|
|
9
|
+
|
|
10
|
+
require_relative 'cost_scanner/actors/weekly_scan' if defined?(Legion::Extensions::Actors::Every)
|
|
11
|
+
|
|
12
|
+
module Legion
|
|
13
|
+
module Extensions
|
|
14
|
+
module CostScanner
|
|
15
|
+
extend Legion::Extensions::Core if defined?(Legion::Extensions::Core)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lex-cost-scanner
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Matthew Iverson
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: legion-cache
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 1.3.11
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 1.3.11
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: legion-crypt
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 1.4.9
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 1.4.9
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: legion-data
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: 1.4.17
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: 1.4.17
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: legion-json
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: 1.2.1
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: 1.2.1
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: legion-logging
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: 1.3.2
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: 1.3.2
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: legion-settings
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: 1.3.14
|
|
89
|
+
type: :runtime
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: 1.3.14
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: legion-transport
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - ">="
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: 1.3.9
|
|
103
|
+
type: :runtime
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: 1.3.9
|
|
110
|
+
description: Scans AWS/Azure accounts for idle resources, classifies findings via
|
|
111
|
+
LLM, delivers weekly Slack reports
|
|
112
|
+
email:
|
|
113
|
+
- matt@legionio.dev
|
|
114
|
+
executables: []
|
|
115
|
+
extensions: []
|
|
116
|
+
extra_rdoc_files: []
|
|
117
|
+
files:
|
|
118
|
+
- CHANGELOG.md
|
|
119
|
+
- README.md
|
|
120
|
+
- lib/legion/extensions/cost_scanner.rb
|
|
121
|
+
- lib/legion/extensions/cost_scanner/actors/weekly_scan.rb
|
|
122
|
+
- lib/legion/extensions/cost_scanner/helpers/classifier.rb
|
|
123
|
+
- lib/legion/extensions/cost_scanner/helpers/constants.rb
|
|
124
|
+
- lib/legion/extensions/cost_scanner/helpers/findings_store.rb
|
|
125
|
+
- lib/legion/extensions/cost_scanner/runners/reporter.rb
|
|
126
|
+
- lib/legion/extensions/cost_scanner/runners/scanner.rb
|
|
127
|
+
- lib/legion/extensions/cost_scanner/version.rb
|
|
128
|
+
homepage: https://github.com/LegionIO/lex-cost-scanner
|
|
129
|
+
licenses:
|
|
130
|
+
- MIT
|
|
131
|
+
metadata:
|
|
132
|
+
rubygems_mfa_required: 'true'
|
|
133
|
+
rdoc_options: []
|
|
134
|
+
require_paths:
|
|
135
|
+
- lib
|
|
136
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
137
|
+
requirements:
|
|
138
|
+
- - ">="
|
|
139
|
+
- !ruby/object:Gem::Version
|
|
140
|
+
version: '3.4'
|
|
141
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
142
|
+
requirements:
|
|
143
|
+
- - ">="
|
|
144
|
+
- !ruby/object:Gem::Version
|
|
145
|
+
version: '0'
|
|
146
|
+
requirements: []
|
|
147
|
+
rubygems_version: 3.6.9
|
|
148
|
+
specification_version: 4
|
|
149
|
+
summary: Cloud cost optimization scanner for LegionIO
|
|
150
|
+
test_files: []
|