Skip to content

Commit

Permalink
Add specs for AppSec ActiveRecord SQLi detection
Browse files Browse the repository at this point in the history
  • Loading branch information
y9v committed Nov 26, 2024
1 parent 353910f commit ec7d62d
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 10 deletions.
3 changes: 3 additions & 0 deletions Matrixfile
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,9 @@
'redis-4' => '✅ 2.5 / ✅ 2.6 / ✅ 2.7 / ✅ 3.0 / ✅ 3.1 / ✅ 3.2 / ✅ 3.3 / ✅ 3.4 / ✅ jruby',
'redis-5' => '✅ 2.5 / ✅ 2.6 / ✅ 2.7 / ✅ 3.0 / ✅ 3.1 / ✅ 3.2 / ✅ 3.3 / ✅ 3.4 / ✅ jruby'
},
'appsec:active_record' => {
'relational_db' => '❌ 2.5 / ❌ 2.6 / ✅ 2.7 / ✅ 3.0 / ✅ 3.1 / ✅ 3.2 / ✅ 3.3 / ✅ 3.4 / ✅ jruby',
},
'appsec:rack' => {
'rack-latest' => '✅ 2.5 / ✅ 2.6 / ✅ 2.7 / ✅ 3.0 / ✅ 3.1 / ✅ 3.2 / ✅ 3.3 / ✅ 3.4 / ✅ jruby',
'rack-3' => '✅ 2.5 / ✅ 2.6 / ✅ 2.7 / ✅ 3.0 / ✅ 3.1 / ✅ 3.2 / ✅ 3.3 / ✅ 3.4 / ✅ jruby',
Expand Down
3 changes: 2 additions & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ namespace :spec do
end

namespace :appsec do
task all: [:main, :rack, :rails, :sinatra, :devise, :graphql]
task all: [:main, :active_record, :rack, :rails, :sinatra, :devise, :graphql]

# Datadog AppSec main specs
desc '' # "Explicitly hiding from `rake -T`"
Expand All @@ -298,6 +298,7 @@ namespace :spec do

# Datadog AppSec integrations
[
:active_record,
:rack,
:sinatra,
:rails,
Expand Down
6 changes: 3 additions & 3 deletions gemfiles/ruby_3.3_rails61_mysql2.gemfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions gemfiles/ruby_3.3_rails61_postgres.gemfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions gemfiles/ruby_3.3_relational_db.gemfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

150 changes: 150 additions & 0 deletions spec/datadog/appsec/contrib/active_record/multi_adapter_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
require 'datadog/appsec/spec_helper'
require 'active_record'

require 'spec/datadog/tracing/contrib/rails/support/deprecation'

require 'mysql2'
require 'sqlite3'
require 'pg'

RSpec.describe 'AppSec ActiveRecord integration' do
let(:telemetry) { instance_double(Datadog::Core::Telemetry::Component) }
let(:ruleset) { Datadog::AppSec::Processor::RuleLoader.load_rules(ruleset: :recommended, telemetry: telemetry) }
let(:processor) { Datadog::AppSec::Processor.new(ruleset: ruleset, telemetry: telemetry) }
let(:context) { processor.new_context }

let(:span) { Datadog::Tracing::SpanOperation.new('root') }
let(:trace) { Datadog::Tracing::TraceOperation.new }

let!(:user_class) do
stub_const('User', Class.new(ActiveRecord::Base)).tap do |klass|
klass.establish_connection(db_config)

klass.connection.create_table 'users', force: :cascade do |t|
t.string :name, null: false
t.string :email, null: false
t.timestamps
end

# prevent internal sql requests from showing up
klass.count
end
end

before do
Datadog.configure do |c|
c.appsec.enabled = true
c.appsec.instrument :active_record
end

Datadog::AppSec::Scope.activate_scope(trace, span, processor)

raise_on_rails_deprecation!
end

after do
Datadog.configuration.reset!

Datadog::AppSec::Scope.deactivate_scope
processor.finalize
end

shared_examples 'calls_waf_with_correct_arguments' do
it 'calls waf with correct arguments' do
expect(Datadog::AppSec.active_scope.processor_context).to(
receive(:run).with(
{},
{
'server.db.statement' => expected_db_statement,
'server.db.system' => expected_db_system
},
Datadog.configuration.appsec.waf_timeout
).and_call_original
)

active_record_scope.to_a
end
end

context 'mysql2 adapter' do
let(:db_config) do
{
adapter: 'mysql2',
database: ENV.fetch('TEST_MYSQL_DB', 'mysql'),
host: ENV.fetch('TEST_MYSQL_HOST', '127.0.0.1'),
password: ENV.fetch('TEST_MYSQL_ROOT_PASSWORD', 'root'),
port: ENV.fetch('TEST_MYSQL_PORT', '3306')
}
end

let(:expected_db_system) { 'mysql' }

context 'when using .where' do
let(:active_record_scope) { User.where(name: 'Bob') }
let(:expected_db_statement) { "SELECT `users`.* FROM `users` WHERE `users`.`name` = 'Bob'" }

include_examples 'calls_waf_with_correct_arguments'
end

context 'when using .find_by_sql' do
let(:active_record_scope) { User.find_by_sql("SELECT * FROM users WHERE name = 'Bob'") }
let(:expected_db_statement) { "SELECT * FROM users WHERE name = 'Bob'" }

include_examples 'calls_waf_with_correct_arguments'
end
end

context 'postgres adapter' do
let(:db_config) do
{
adapter: 'postgresql',
database: ENV.fetch('TEST_POSTGRES_DB', 'postgres'),
host: ENV.fetch('TEST_POSTGRES_HOST', '127.0.0.1'),
port: ENV.fetch('TEST_POSTGRES_PORT', 5432),
username: ENV.fetch('TEST_POSTGRES_USER', 'postgres'),
password: ENV.fetch('TEST_POSTGRES_PASSWORD', 'postgres')
}
end

let(:expected_db_system) { 'postgresql' }

context 'when using .where' do
let(:active_record_scope) { User.where(name: 'Bob') }
let(:expected_db_statement) { 'SELECT "users".* FROM "users" WHERE "users"."name" = $1' }

include_examples 'calls_waf_with_correct_arguments'
end

context 'when using .find_by_sql' do
let(:active_record_scope) { User.find_by_sql("SELECT * FROM users WHERE name = 'Bob'") }
let(:expected_db_statement) { "SELECT * FROM users WHERE name = 'Bob'" }

include_examples 'calls_waf_with_correct_arguments'
end
end

context 'sqlite3 adapter' do
let(:db_config) do
{
adapter: 'sqlite3',
database: ':memory:'
}
end

let(:expected_db_system) { 'sqlite' }

context 'when using .where' do
let(:active_record_scope) { User.where(name: 'Bob') }
let(:expected_db_statement) { 'SELECT "users".* FROM "users" WHERE "users"."name" = ?' }

include_examples 'calls_waf_with_correct_arguments'
end

context 'when using .find_by_sql' do
let(:active_record_scope) { User.find_by_sql("SELECT * FROM users WHERE name = 'Bob'") }
let(:expected_db_statement) { "SELECT * FROM users WHERE name = 'Bob'" }

include_examples 'calls_waf_with_correct_arguments'
end
end
end
44 changes: 44 additions & 0 deletions spec/datadog/appsec/contrib/active_record/patcher_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

require 'datadog/appsec/spec_helper'
require 'datadog/appsec/contrib/active_record/patcher'

RSpec.describe Datadog::AppSec::Contrib::ActiveRecord::Patcher do
describe '#prepended_class_name' do
before do
stub_const('::ActiveRecord', Struct.new(:gem_version).new(Gem::Version.new(active_record_version)))
end

context 'when ActiveRecord version is 7.1 or higher' do
let(:active_record_version) { Gem::Version.new('7.1') }

it 'returns Instrumentation::InternalExecQueryAdapterPatch' do
expect(described_class.prepended_class_name(:postgresql)).to eq(
Datadog::AppSec::Contrib::ActiveRecord::Instrumentation::InternalExecQueryAdapterPatch
)
end
end

context 'when ActiveRecord version is lower than 7.1' do
let(:active_record_version) { Gem::Version.new('7.0') }

context 'for postgresql adapter' do
it 'returns Instrumentation::ExecuteAndClearAdapterPatch' do
expect(described_class.prepended_class_name(:postgresql)).to eq(
Datadog::AppSec::Contrib::ActiveRecord::Instrumentation::ExecuteAndClearAdapterPatch
)
end
end

%i[mysql2 sqlite3].each do |adapter_name|
context "for #{adapter_name} adapter" do
it 'returns Instrumentation::ExecQueryAdapterPatch' do
expect(described_class.prepended_class_name(adapter_name)).to eq(
Datadog::AppSec::Contrib::ActiveRecord::Instrumentation::ExecQueryAdapterPatch
)
end
end
end
end
end
end

0 comments on commit ec7d62d

Please sign in to comment.