feat(scanner): Implement Deno analyzer and associated tests
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Added Deno analyzer with comprehensive metadata and evidence structure.
- Created a detailed implementation plan for Sprint 130 focusing on Deno analyzer.
- Introduced AdvisoryAiGuardrailOptions for managing guardrail configurations.
- Developed GuardrailPhraseLoader for loading blocked phrases from JSON files.
- Implemented tests for AdvisoryGuardrailOptions binding and phrase loading.
- Enhanced telemetry for Advisory AI with metrics tracking.
- Added VexObservationProjectionService for querying VEX observations.
- Created extensive tests for VexObservationProjectionService functionality.
- Introduced Ruby language analyzer with tests for simple and complex workspaces.
- Added Ruby application fixtures for testing purposes.
This commit is contained in:
master
2025-11-12 10:01:54 +02:00
parent 0e8655cbb1
commit babb81af52
75 changed files with 3346 additions and 187 deletions

View File

@@ -0,0 +1,3 @@
---
BUNDLE_GEMFILE: Gemfile
BUNDLE_PATH: vendor/custom-bundle

View File

@@ -0,0 +1,20 @@
source "https://rubygems.org/"
gem "rack", "~> 3.1"
group :web do
gem "sinatra", "~> 3.1"
gem "pagy"
end
group :jobs do
gem "sidekiq", "~> 7.2"
end
group :ops do
gem "clockwork"
end
group :tools do
gem "pry", "= 0.14.2"
end

View File

@@ -0,0 +1,27 @@
GEM
remote: https://rubygems.org/
specs:
clockwork (3.0.0)
pagy (6.5.0)
pry (0.14.2)
coderay (~> 1.1)
method_source (~> 1.0)
rack (3.1.2)
sidekiq (7.2.1)
rack (~> 2.0)
sinatra (3.1.0)
rack (~> 3.0)
PLATFORMS
ruby
DEPENDENCIES
clockwork
pagy
pry (= 0.14.2)
rack (~> 3.1)
sidekiq (~> 7.2)
sinatra (~> 3.1)
BUNDLED WITH
2.5.3

View File

@@ -0,0 +1,18 @@
require "rack"
require "sinatra"
require "pagy/backend"
require "net/http"
require_relative '../config/environment'
module ConsoleApp
class Server < Sinatra::Base
get '/' do
http = Net::HTTP.new('example.invalid', 443)
http.use_ssl = true
http.start do |client|
client.get('/')
end
'ok'
end
end
end

View File

@@ -0,0 +1,10 @@
require "pagy"
require "json"
module ConsoleApp
module Boot
def self.load!
JSON.parse('{feature:advisory-ai}')
end
end
end

View File

@@ -0,0 +1,202 @@
[
{
"analyzerId": "ruby",
"componentKey": "observation::ruby",
"name": "Ruby Observation Summary",
"type": "ruby-observation",
"usedByEntrypoint": false,
"metadata": {
"ruby.observation.bundler_version": "2.5.3",
"ruby.observation.capability.exec": "false",
"ruby.observation.capability.net": "true",
"ruby.observation.capability.scheduler_list": "clockwork;sidekiq",
"ruby.observation.capability.schedulers": "2",
"ruby.observation.capability.serialization": "false",
"ruby.observation.packages": "6",
"ruby.observation.runtime_edges": "5"
},
"evidence": [
{
"kind": "derived",
"source": "ruby.observation",
"locator": "document",
"value": "{\u0022packages\u0022:[{\u0022name\u0022:\u0022clockwork\u0022,\u0022version\u0022:\u00223.0.0\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022ops\u0022]},{\u0022name\u0022:\u0022pagy\u0022,\u0022version\u0022:\u00226.5.0\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022web\u0022]},{\u0022name\u0022:\u0022pry\u0022,\u0022version\u0022:\u00220.14.2\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022tools\u0022]},{\u0022name\u0022:\u0022rack\u0022,\u0022version\u0022:\u00223.1.2\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022sidekiq\u0022,\u0022version\u0022:\u00227.2.1\u0022,\u0022source\u0022:\u0022vendor\u0022,\u0022declaredOnly\u0022:false,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022artifact\u0022:\u0022vendor/custom-bundle/cache/sidekiq-7.2.1.gem\u0022,\u0022groups\u0022:[\u0022jobs\u0022]},{\u0022name\u0022:\u0022sinatra\u0022,\u0022version\u0022:\u00223.1.0\u0022,\u0022source\u0022:\u0022vendor-cache\u0022,\u0022declaredOnly\u0022:false,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022artifact\u0022:\u0022vendor/cache/sinatra-3.1.0.gem\u0022,\u0022groups\u0022:[\u0022web\u0022]}],\u0022runtimeEdges\u0022:[{\u0022package\u0022:\u0022clockwork\u0022,\u0022usedByEntrypoint\u0022:false,\u0022files\u0022:[\u0022scripts/worker.rb\u0022],\u0022entrypoints\u0022:[],\u0022reasons\u0022:[\u0022require-static\u0022]},{\u0022package\u0022:\u0022pagy\u0022,\u0022usedByEntrypoint\u0022:true,\u0022files\u0022:[\u0022app/main.rb\u0022,\u0022config/environment.rb\u0022],\u0022entrypoints\u0022:[\u0022config/environment.rb\u0022],\u0022reasons\u0022:[\u0022require-static\u0022]},{\u0022package\u0022:\u0022rack\u0022,\u0022usedByEntrypoint\u0022:false,\u0022files\u0022:[\u0022app/main.rb\u0022],\u0022entrypoints\u0022:[],\u0022reasons\u0022:[\u0022require-static\u0022]},{\u0022package\u0022:\u0022sidekiq\u0022,\u0022usedByEntrypoint\u0022:false,\u0022files\u0022:[\u0022scripts/worker.rb\u0022],\u0022entrypoints\u0022:[],\u0022reasons\u0022:[\u0022require-static\u0022]},{\u0022package\u0022:\u0022sinatra\u0022,\u0022usedByEntrypoint\u0022:false,\u0022files\u0022:[\u0022app/main.rb\u0022],\u0022entrypoints\u0022:[],\u0022reasons\u0022:[\u0022require-static\u0022]}],\u0022capabilities\u0022:{\u0022usesExec\u0022:false,\u0022usesNetwork\u0022:true,\u0022usesSerialization\u0022:false,\u0022jobSchedulers\u0022:[\u0022clockwork\u0022,\u0022sidekiq\u0022]},\u0022bundledWith\u0022:\u00222.5.3\u0022}",
"sha256": "sha256:beaefa12ec1f49e62343781ffa949ec3fa006f0452cf8a342a9a12be3cda1d82"
}
]
},
{
"analyzerId": "ruby",
"componentKey": "purl::pkg:gem/clockwork@3.0.0",
"purl": "pkg:gem/clockwork@3.0.0",
"name": "clockwork",
"version": "3.0.0",
"type": "gem",
"usedByEntrypoint": false,
"metadata": {
"capability.net": "true",
"capability.scheduler": "clockwork;sidekiq",
"capability.scheduler.clockwork": "true",
"capability.scheduler.sidekiq": "true",
"declaredOnly": "true",
"groups": "ops",
"lockfile": "Gemfile.lock",
"runtime.files": "scripts/worker.rb",
"runtime.reasons": "require-static",
"runtime.used": "true",
"source": "https://rubygems.org/"
},
"evidence": [
{
"kind": "file",
"source": "Gemfile.lock",
"locator": "Gemfile.lock"
}
]
},
{
"analyzerId": "ruby",
"componentKey": "purl::pkg:gem/pagy@6.5.0",
"purl": "pkg:gem/pagy@6.5.0",
"name": "pagy",
"version": "6.5.0",
"type": "gem",
"usedByEntrypoint": true,
"metadata": {
"capability.net": "true",
"capability.scheduler": "clockwork;sidekiq",
"capability.scheduler.clockwork": "true",
"capability.scheduler.sidekiq": "true",
"declaredOnly": "true",
"groups": "web",
"lockfile": "Gemfile.lock",
"runtime.entrypoints": "config/environment.rb",
"runtime.files": "app/main.rb;config/environment.rb",
"runtime.reasons": "require-static",
"runtime.used": "true",
"source": "https://rubygems.org/"
},
"evidence": [
{
"kind": "file",
"source": "Gemfile.lock",
"locator": "Gemfile.lock"
}
]
},
{
"analyzerId": "ruby",
"componentKey": "purl::pkg:gem/pry@0.14.2",
"purl": "pkg:gem/pry@0.14.2",
"name": "pry",
"version": "0.14.2",
"type": "gem",
"usedByEntrypoint": false,
"metadata": {
"capability.net": "true",
"capability.scheduler": "clockwork;sidekiq",
"capability.scheduler.clockwork": "true",
"capability.scheduler.sidekiq": "true",
"declaredOnly": "true",
"groups": "tools",
"lockfile": "Gemfile.lock",
"source": "https://rubygems.org/"
},
"evidence": [
{
"kind": "file",
"source": "Gemfile.lock",
"locator": "Gemfile.lock"
}
]
},
{
"analyzerId": "ruby",
"componentKey": "purl::pkg:gem/rack@3.1.2",
"purl": "pkg:gem/rack@3.1.2",
"name": "rack",
"version": "3.1.2",
"type": "gem",
"usedByEntrypoint": false,
"metadata": {
"capability.net": "true",
"capability.scheduler": "clockwork;sidekiq",
"capability.scheduler.clockwork": "true",
"capability.scheduler.sidekiq": "true",
"declaredOnly": "true",
"groups": "default",
"lockfile": "Gemfile.lock",
"runtime.files": "app/main.rb",
"runtime.reasons": "require-static",
"runtime.used": "true",
"source": "https://rubygems.org/"
},
"evidence": [
{
"kind": "file",
"source": "Gemfile.lock",
"locator": "Gemfile.lock"
}
]
},
{
"analyzerId": "ruby",
"componentKey": "purl::pkg:gem/sidekiq@7.2.1",
"purl": "pkg:gem/sidekiq@7.2.1",
"name": "sidekiq",
"version": "7.2.1",
"type": "gem",
"usedByEntrypoint": false,
"metadata": {
"artifact": "vendor/custom-bundle/cache/sidekiq-7.2.1.gem",
"capability.net": "true",
"capability.scheduler": "clockwork;sidekiq",
"capability.scheduler.clockwork": "true",
"capability.scheduler.sidekiq": "true",
"declaredOnly": "false",
"groups": "jobs",
"lockfile": "Gemfile.lock",
"runtime.files": "scripts/worker.rb",
"runtime.reasons": "require-static",
"runtime.used": "true",
"source": "vendor"
},
"evidence": [
{
"kind": "file",
"source": "sidekiq-7.2.1.gem",
"locator": "vendor/custom-bundle/cache/sidekiq-7.2.1.gem"
}
]
},
{
"analyzerId": "ruby",
"componentKey": "purl::pkg:gem/sinatra@3.1.0",
"purl": "pkg:gem/sinatra@3.1.0",
"name": "sinatra",
"version": "3.1.0",
"type": "gem",
"usedByEntrypoint": false,
"metadata": {
"artifact": "vendor/cache/sinatra-3.1.0.gem",
"capability.net": "true",
"capability.scheduler": "clockwork;sidekiq",
"capability.scheduler.clockwork": "true",
"capability.scheduler.sidekiq": "true",
"declaredOnly": "false",
"groups": "web",
"lockfile": "Gemfile.lock",
"runtime.files": "app/main.rb",
"runtime.reasons": "require-static",
"runtime.used": "true",
"source": "vendor-cache"
},
"evidence": [
{
"kind": "file",
"source": "sinatra-3.1.0.gem",
"locator": "vendor/cache/sinatra-3.1.0.gem"
}
]
}
]

View File

@@ -0,0 +1,15 @@
require "sidekiq"
require "clockwork"
require "open3"
module ConsoleApp
class Worker
include Sidekiq::Worker
def perform
Clockwork.every(1.hour, 'ping') do
Open3.popen3('echo', 'ping') { |_stdin, stdout, _stderr, wait_thr| wait_thr.value }
end
end
end
end

View File

@@ -0,0 +1,20 @@
source "https://rubygems.org"
ruby "3.1.2"
gem "rack", "~> 3.0"
gem "rails", "7.1.0"
gem "puma", "~> 6.1", group: [:web]
gem "sqlite3", group: :db
group :jobs do
gem "sidekiq", "7.2.1"
end
group :development, :test do
gem "pry", "0.14.2"
end
group :test do
gem "rspec", "3.12.0"
end

View File

@@ -0,0 +1,33 @@
GEM
remote: https://rubygems.org/
specs:
coderay (1.1.3)
connection_pool (2.4.1)
method_source (1.0.0)
pry (0.14.2)
coderay (~> 1.1)
method_source (~> 1.0)
puma (6.1.1)
rack (3.0.8)
rails (7.1.0)
rspec (3.12.0)
sidekiq (7.2.1)
connection_pool (>= 2.3.0)
rack (~> 2.0)
sqlite3 (1.6.0-x86_64-linux)
PLATFORMS
ruby
x86_64-linux
DEPENDENCIES
pry (= 0.14.2)
puma (~> 6.1)
rack (~> 3.0)
rails (= 7.1.0)
rspec (= 3.12.0)
sidekiq (= 7.2.1)
sqlite3
BUNDLED WITH
2.4.22

View File

@@ -0,0 +1,17 @@
#!/usr/bin/env ruby
require "rack"
require "puma"
require "sidekiq"
require "yaml"
require "net/http"
require_relative "app/workers/email_worker"
class App
def call(env)
EmailWorker.perform_async(env["PATH_INFO"])
[200, { "Content-Type" => "text/plain" }, ["ok"]]
end
end
run App.new

View File

@@ -0,0 +1,13 @@
require "sidekiq"
require "net/http"
require "yaml"
class EmailWorker
include Sidekiq::Worker
def perform(user_id)
system("echo sending email #{user_id}")
Net::HTTP.get(URI("https://example.com/users/#{user_id}"))
YAML.load(File.read("config/clock.rb"))
end
end

View File

@@ -0,0 +1,4 @@
require "rack"
require_relative "app.rb"
run App.new

View File

@@ -0,0 +1,5 @@
require "clockwork"
module Clockwork
every(1.hour, "cleanup") { puts "cleanup" }
end

View File

@@ -0,0 +1,319 @@
[
{
"analyzerId": "ruby",
"componentKey": "observation::ruby",
"name": "Ruby Observation Summary",
"type": "ruby-observation",
"usedByEntrypoint": false,
"metadata": {
"ruby.observation.bundler_version": "2.4.22",
"ruby.observation.capability.exec": "true",
"ruby.observation.capability.net": "true",
"ruby.observation.capability.scheduler_list": "sidekiq",
"ruby.observation.capability.schedulers": "1",
"ruby.observation.capability.serialization": "true",
"ruby.observation.packages": "11",
"ruby.observation.runtime_edges": "3"
},
"evidence": [
{
"kind": "derived",
"source": "ruby.observation",
"locator": "document",
"value": "{\u0022packages\u0022:[{\u0022name\u0022:\u0022coderay\u0022,\u0022version\u0022:\u00221.1.3\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022connection_pool\u0022,\u0022version\u0022:\u00222.4.1\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022method_source\u0022,\u0022version\u0022:\u00221.0.0\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022pry\u0022,\u0022version\u0022:\u00220.14.2\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022development\u0022,\u0022test\u0022]},{\u0022name\u0022:\u0022puma\u0022,\u0022version\u0022:\u00226.1.1\u0022,\u0022source\u0022:\u0022vendor-cache\u0022,\u0022declaredOnly\u0022:false,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022artifact\u0022:\u0022vendor/cache/puma-6.1.1.gem\u0022,\u0022groups\u0022:[\u0022web\u0022]},{\u0022name\u0022:\u0022rack\u0022,\u0022version\u0022:\u00223.0.8\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022rails\u0022,\u0022version\u0022:\u00227.1.0\u0022,\u0022source\u0022:\u0022vendor-cache\u0022,\u0022platform\u0022:\u0022x86_64-linux\u0022,\u0022declaredOnly\u0022:false,\u0022artifact\u0022:\u0022vendor/cache/rails-7.1.0-x86_64-linux.gem\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022rails\u0022,\u0022version\u0022:\u00227.1.0\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022rspec\u0022,\u0022version\u0022:\u00223.12.0\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022test\u0022]},{\u0022name\u0022:\u0022sidekiq\u0022,\u0022version\u0022:\u00227.2.1\u0022,\u0022source\u0022:\u0022vendor-bundle\u0022,\u0022declaredOnly\u0022:false,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022artifact\u0022:\u0022vendor/bundle/ruby/3.1.0/gems/sidekiq-7.2.1\u0022,\u0022groups\u0022:[\u0022jobs\u0022]},{\u0022name\u0022:\u0022sqlite3\u0022,\u0022version\u0022:\u00221.6.0-x86_64-linux\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022db\u0022]}],\u0022runtimeEdges\u0022:[{\u0022package\u0022:\u0022puma\u0022,\u0022usedByEntrypoint\u0022:true,\u0022files\u0022:[\u0022app.rb\u0022],\u0022entrypoints\u0022:[\u0022app.rb\u0022],\u0022reasons\u0022:[\u0022require-static\u0022]},{\u0022package\u0022:\u0022rack\u0022,\u0022usedByEntrypoint\u0022:true,\u0022files\u0022:[\u0022app.rb\u0022,\u0022config.ru\u0022],\u0022entrypoints\u0022:[\u0022app.rb\u0022,\u0022config.ru\u0022],\u0022reasons\u0022:[\u0022require-static\u0022]},{\u0022package\u0022:\u0022sidekiq\u0022,\u0022usedByEntrypoint\u0022:true,\u0022files\u0022:[\u0022app.rb\u0022,\u0022app/workers/email_worker.rb\u0022],\u0022entrypoints\u0022:[\u0022app.rb\u0022,\u0022app/workers/email_worker.rb\u0022],\u0022reasons\u0022:[\u0022require-static\u0022]}],\u0022capabilities\u0022:{\u0022usesExec\u0022:true,\u0022usesNetwork\u0022:true,\u0022usesSerialization\u0022:true,\u0022jobSchedulers\u0022:[\u0022sidekiq\u0022]},\u0022bundledWith\u0022:\u00222.4.22\u0022}",
"sha256": "sha256:30b34afcf1a3ae3a32f1088ca535ca5359f9ed1ecf53850909b2bcd4da663ace"
}
]
},
{
"analyzerId": "ruby",
"componentKey": "purl::pkg:gem/coderay@1.1.3",
"purl": "pkg:gem/coderay@1.1.3",
"name": "coderay",
"version": "1.1.3",
"type": "gem",
"usedByEntrypoint": false,
"metadata": {
"capability.exec": "true",
"capability.net": "true",
"capability.scheduler": "sidekiq",
"capability.scheduler.sidekiq": "true",
"capability.serialization": "true",
"declaredOnly": "true",
"groups": "default",
"lockfile": "Gemfile.lock",
"source": "https://rubygems.org/"
},
"evidence": [
{
"kind": "file",
"source": "Gemfile.lock",
"locator": "Gemfile.lock"
}
]
},
{
"analyzerId": "ruby",
"componentKey": "purl::pkg:gem/connection_pool@2.4.1",
"purl": "pkg:gem/connection_pool@2.4.1",
"name": "connection_pool",
"version": "2.4.1",
"type": "gem",
"usedByEntrypoint": false,
"metadata": {
"capability.exec": "true",
"capability.net": "true",
"capability.scheduler": "sidekiq",
"capability.scheduler.sidekiq": "true",
"capability.serialization": "true",
"declaredOnly": "true",
"groups": "default",
"lockfile": "Gemfile.lock",
"source": "https://rubygems.org/"
},
"evidence": [
{
"kind": "file",
"source": "Gemfile.lock",
"locator": "Gemfile.lock"
}
]
},
{
"analyzerId": "ruby",
"componentKey": "purl::pkg:gem/method_source@1.0.0",
"purl": "pkg:gem/method_source@1.0.0",
"name": "method_source",
"version": "1.0.0",
"type": "gem",
"usedByEntrypoint": false,
"metadata": {
"capability.exec": "true",
"capability.net": "true",
"capability.scheduler": "sidekiq",
"capability.scheduler.sidekiq": "true",
"capability.serialization": "true",
"declaredOnly": "true",
"groups": "default",
"lockfile": "Gemfile.lock",
"source": "https://rubygems.org/"
},
"evidence": [
{
"kind": "file",
"source": "Gemfile.lock",
"locator": "Gemfile.lock"
}
]
},
{
"analyzerId": "ruby",
"componentKey": "purl::pkg:gem/pry@0.14.2",
"purl": "pkg:gem/pry@0.14.2",
"name": "pry",
"version": "0.14.2",
"type": "gem",
"usedByEntrypoint": false,
"metadata": {
"capability.exec": "true",
"capability.net": "true",
"capability.scheduler": "sidekiq",
"capability.scheduler.sidekiq": "true",
"capability.serialization": "true",
"declaredOnly": "true",
"groups": "development;test",
"lockfile": "Gemfile.lock",
"source": "https://rubygems.org/"
},
"evidence": [
{
"kind": "file",
"source": "Gemfile.lock",
"locator": "Gemfile.lock"
}
]
},
{
"analyzerId": "ruby",
"componentKey": "purl::pkg:gem/puma@6.1.1",
"purl": "pkg:gem/puma@6.1.1",
"name": "puma",
"version": "6.1.1",
"type": "gem",
"usedByEntrypoint": true,
"metadata": {
"artifact": "vendor/cache/puma-6.1.1.gem",
"capability.exec": "true",
"capability.net": "true",
"capability.scheduler": "sidekiq",
"capability.scheduler.sidekiq": "true",
"capability.serialization": "true",
"declaredOnly": "false",
"groups": "web",
"lockfile": "Gemfile.lock",
"runtime.entrypoints": "app.rb",
"runtime.files": "app.rb",
"runtime.reasons": "require-static",
"runtime.used": "true",
"source": "vendor-cache"
},
"evidence": [
{
"kind": "file",
"source": "puma-6.1.1.gem",
"locator": "vendor/cache/puma-6.1.1.gem"
}
]
},
{
"analyzerId": "ruby",
"componentKey": "purl::pkg:gem/rack@3.0.8",
"purl": "pkg:gem/rack@3.0.8",
"name": "rack",
"version": "3.0.8",
"type": "gem",
"usedByEntrypoint": true,
"metadata": {
"capability.exec": "true",
"capability.net": "true",
"capability.scheduler": "sidekiq",
"capability.scheduler.sidekiq": "true",
"capability.serialization": "true",
"declaredOnly": "true",
"groups": "default",
"lockfile": "Gemfile.lock",
"runtime.entrypoints": "app.rb;config.ru",
"runtime.files": "app.rb;config.ru",
"runtime.reasons": "require-static",
"runtime.used": "true",
"source": "https://rubygems.org/"
},
"evidence": [
{
"kind": "file",
"source": "Gemfile.lock",
"locator": "Gemfile.lock"
}
]
},
{
"analyzerId": "ruby",
"componentKey": "purl::pkg:gem/rails@7.1.0",
"purl": "pkg:gem/rails@7.1.0",
"name": "rails",
"version": "7.1.0",
"type": "gem",
"usedByEntrypoint": false,
"metadata": {
"artifact": "vendor/cache/rails-7.1.0-x86_64-linux.gem",
"capability.exec": "true",
"capability.net": "true",
"capability.scheduler": "sidekiq",
"capability.scheduler.sidekiq": "true",
"capability.serialization": "true",
"declaredOnly": "false",
"groups": "default",
"lockfile": "vendor/cache/rails-7.1.0-x86_64-linux.gem",
"platform": "x86_64-linux",
"source": "vendor-cache"
},
"evidence": [
{
"kind": "file",
"source": "Gemfile.lock",
"locator": "Gemfile.lock"
},
{
"kind": "file",
"source": "rails-7.1.0-x86_64-linux.gem",
"locator": "vendor/cache/rails-7.1.0-x86_64-linux.gem"
}
]
},
{
"analyzerId": "ruby",
"componentKey": "purl::pkg:gem/rspec@3.12.0",
"purl": "pkg:gem/rspec@3.12.0",
"name": "rspec",
"version": "3.12.0",
"type": "gem",
"usedByEntrypoint": false,
"metadata": {
"capability.exec": "true",
"capability.net": "true",
"capability.scheduler": "sidekiq",
"capability.scheduler.sidekiq": "true",
"capability.serialization": "true",
"declaredOnly": "true",
"groups": "test",
"lockfile": "Gemfile.lock",
"source": "https://rubygems.org/"
},
"evidence": [
{
"kind": "file",
"source": "Gemfile.lock",
"locator": "Gemfile.lock"
}
]
},
{
"analyzerId": "ruby",
"componentKey": "purl::pkg:gem/sidekiq@7.2.1",
"purl": "pkg:gem/sidekiq@7.2.1",
"name": "sidekiq",
"version": "7.2.1",
"type": "gem",
"usedByEntrypoint": true,
"metadata": {
"artifact": "vendor/bundle/ruby/3.1.0/gems/sidekiq-7.2.1",
"capability.exec": "true",
"capability.net": "true",
"capability.scheduler": "sidekiq",
"capability.scheduler.sidekiq": "true",
"capability.serialization": "true",
"declaredOnly": "false",
"groups": "jobs",
"lockfile": "Gemfile.lock",
"runtime.entrypoints": "app.rb;app/workers/email_worker.rb",
"runtime.files": "app.rb;app/workers/email_worker.rb",
"runtime.reasons": "require-static",
"runtime.used": "true",
"source": "vendor-bundle"
},
"evidence": [
{
"kind": "file",
"source": "sidekiq-7.2.1",
"locator": "vendor/bundle/ruby/3.1.0/gems/sidekiq-7.2.1"
}
]
},
{
"analyzerId": "ruby",
"componentKey": "purl::pkg:gem/sqlite3@1.6.0-x86_64-linux",
"purl": "pkg:gem/sqlite3@1.6.0-x86_64-linux",
"name": "sqlite3",
"version": "1.6.0-x86_64-linux",
"type": "gem",
"usedByEntrypoint": false,
"metadata": {
"capability.exec": "true",
"capability.net": "true",
"capability.scheduler": "sidekiq",
"capability.scheduler.sidekiq": "true",
"capability.serialization": "true",
"declaredOnly": "true",
"groups": "db",
"lockfile": "Gemfile.lock",
"source": "https://rubygems.org/"
},
"evidence": [
{
"kind": "file",
"source": "Gemfile.lock",
"locator": "Gemfile.lock"
}
]
}
]

View File

@@ -0,0 +1,89 @@
using System.Text.Json;
using StellaOps.Scanner.Analyzers.Lang;
using StellaOps.Scanner.Analyzers.Lang.Ruby;
using StellaOps.Scanner.Analyzers.Lang.Tests.Harness;
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
using StellaOps.Scanner.Core.Contracts;
namespace StellaOps.Scanner.Analyzers.Lang.Ruby.Tests;
public sealed class RubyLanguageAnalyzerTests
{
[Fact]
public async Task SimpleWorkspaceProducesDeterministicOutputAsync()
{
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "simple-app");
var goldenPath = Path.Combine(fixturePath, "expected.json");
var analyzers = new ILanguageAnalyzer[] { new RubyLanguageAnalyzer() };
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
fixturePath,
goldenPath,
analyzers,
TestContext.Current.CancellationToken);
}
[Fact]
public async Task AnalyzerEmitsObservationPayloadWithSummaryAsync()
{
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "simple-app");
var store = new ScanAnalysisStore();
var analyzers = new ILanguageAnalyzer[] { new RubyLanguageAnalyzer() };
var engine = new LanguageAnalyzerEngine(analyzers);
var context = new LanguageAnalyzerContext(
fixturePath,
TimeProvider.System,
usageHints: null,
services: null,
analysisStore: store);
var result = await engine.AnalyzeAsync(context, TestContext.Current.CancellationToken);
var snapshots = result.ToSnapshots();
var summary = Assert.Single(snapshots, snapshot => snapshot.Type == "ruby-observation");
Assert.Equal("Ruby Observation Summary", summary.Name);
Assert.Equal("observation::ruby", summary.ComponentKey);
Assert.True(summary.Metadata.TryGetValue("ruby.observation.packages", out var packageCount));
Assert.Equal("11", packageCount);
Assert.Equal("3", summary.Metadata["ruby.observation.runtime_edges"]);
Assert.Equal("true", summary.Metadata["ruby.observation.capability.exec"]);
Assert.Equal("true", summary.Metadata["ruby.observation.capability.net"]);
Assert.Equal("true", summary.Metadata["ruby.observation.capability.serialization"]);
Assert.Equal("2.4.22", summary.Metadata["ruby.observation.bundler_version"]);
Assert.True(store.TryGet(ScanAnalysisKeys.RubyObservationPayload, out AnalyzerObservationPayload payload));
Assert.Equal("ruby", payload.AnalyzerId);
Assert.Equal("ruby.observation", payload.Kind);
Assert.Equal("application/json", payload.MediaType);
Assert.NotNull(payload.Metadata);
Assert.Equal("11", payload.Metadata!["ruby.observation.packages"]);
using var document = JsonDocument.Parse(payload.Content.ToArray());
var root = document.RootElement;
var packages = root.GetProperty("packages");
Assert.Equal(11, packages.GetArrayLength());
var runtimeEdges = root.GetProperty("runtimeEdges");
Assert.True(runtimeEdges.GetArrayLength() >= 1);
var capabilities = root.GetProperty("capabilities");
Assert.True(capabilities.GetProperty("usesExec").GetBoolean());
Assert.True(capabilities.GetProperty("usesNetwork").GetBoolean());
Assert.True(capabilities.GetProperty("usesSerialization").GetBoolean());
Assert.Equal("2.4.22", root.GetProperty("bundledWith").GetString());
}
[Fact]
public async Task ComplexWorkspaceProducesDeterministicOutputAsync()
{
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "complex-app");
var goldenPath = Path.Combine(fixturePath, "expected.json");
var analyzers = new ILanguageAnalyzer[] { new RubyLanguageAnalyzer() };
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
fixturePath,
goldenPath,
analyzers,
TestContext.Current.CancellationToken);
}
}

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Remove="Microsoft.NET.Test.Sdk" />
<PackageReference Remove="xunit" />
<PackageReference Remove="xunit.runner.visualstudio" />
<PackageReference Remove="Microsoft.AspNetCore.Mvc.Testing" />
<PackageReference Remove="Mongo2Go" />
<PackageReference Remove="coverlet.collector" />
<PackageReference Remove="Microsoft.Extensions.TimeProvider.Testing" />
<ProjectReference Remove="..\StellaOps.Concelier.Testing\StellaOps.Concelier.Testing.csproj" />
<Compile Remove="$(MSBuildThisFileDirectory)..\StellaOps.Concelier.Tests.Shared\AssemblyInfo.cs" />
<Compile Remove="$(MSBuildThisFileDirectory)..\StellaOps.Concelier.Tests.Shared\MongoFixtureCollection.cs" />
<Using Remove="StellaOps.Concelier.Testing" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit.v3" Version="3.0.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Scanner.Analyzers.Lang.Tests\StellaOps.Scanner.Analyzers.Lang.Tests.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.Lang/StellaOps.Scanner.Analyzers.Lang.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.Lang.Ruby/StellaOps.Scanner.Analyzers.Lang.Ruby.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Core/StellaOps.Scanner.Core.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>