Skip to content

Commit

Permalink
Add AppSec integration specs
Browse files Browse the repository at this point in the history
This covers the following AppSec integrations:
- Rack
- Rails
- Sinatra
  • Loading branch information
lloeki committed Oct 11, 2022
1 parent 731028a commit 35f67c4
Show file tree
Hide file tree
Showing 4 changed files with 853 additions and 0 deletions.
267 changes: 267 additions & 0 deletions spec/datadog/appsec/contrib/rack/integration_test_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
# typed: ignore

require 'datadog/tracing/contrib/support/spec_helper'
require 'rack/test'

require 'securerandom'
require 'rack'
require 'rack/contrib/json_body_parser'

require 'ddtrace'
require 'datadog/tracing/contrib/rack/middlewares'

require 'datadog/appsec'
require 'datadog/appsec/contrib/rack/request_middleware'

RSpec.describe 'Rack integration tests' do
include Rack::Test::Methods

let(:appsec_enabled) { true }
let(:tracing_enabled) { true }

before do
Datadog.configure do |c|
c.tracing.enabled = tracing_enabled
c.tracing.instrument :rack

c.appsec.enabled = appsec_enabled
c.appsec.instrument :rack
end
end

after { Datadog.registry[:rack].reset_configuration! }

context 'for an application' do
# TODO: also test without Tracing: it should run without trace transport

let(:middlewares) do
[
Datadog::Tracing::Contrib::Rack::TraceMiddleware,
Datadog::AppSec::Contrib::Rack::RequestMiddleware
]
end

let(:app) do
app_routes = routes
app_middlewares = middlewares

Rack::Builder.new do
app_middlewares.each { |m| use m }
instance_eval(&app_routes)
end.to_app
end

let(:triggers) do
json = trace.send(:meta)['_dd.appsec.json']

JSON.parse(json).fetch('triggers', []) if json
end

shared_examples 'a GET 200 span' do
it { expect(span.get_tag('http.method')).to eq('GET') }
it { expect(span.get_tag('http.status_code')).to eq('200') }
it { expect(span.status).to eq(0) }
end

shared_examples 'a GET 403 span' do
it { expect(span.get_tag('http.method')).to eq('GET') }
it { expect(span.get_tag('http.status_code')).to eq('403') }
it { expect(span.status).to eq(0) }
end

shared_examples 'a GET 404 span' do
it { expect(span.get_tag('http.method')).to eq('GET') }
it { expect(span.get_tag('http.status_code')).to eq('404') }
it { expect(span.status).to eq(0) }
end

shared_examples 'a POST 200 span' do
it { expect(span.get_tag('http.method')).to eq('POST') }
it { expect(span.get_tag('http.status_code')).to eq('200') }
it { expect(span.status).to eq(0) }
end

shared_examples 'a trace without AppSec tags' do
it { expect(trace.send(:metrics)['_dd.appsec.enabled']).to be_nil }
it { expect(trace.send(:meta)['_dd.runtime_family']).to be_nil }
it { expect(trace.send(:meta)['_dd.appsec.waf.version']).to be_nil }
end

shared_examples 'a trace with AppSec tags' do
it { expect(trace.send(:metrics)['_dd.appsec.enabled']).to eq(1.0) }
it { expect(trace.send(:meta)['_dd.runtime_family']).to eq('ruby') }
it { expect(trace.send(:meta)['_dd.appsec.waf.version']).to match(/^\d+\.\d+\.\d+/) }

context 'with appsec disabled' do
let(:appsec_enabled) { false }

it_behaves_like 'a trace without AppSec tags'
end
end

shared_examples 'a trace without AppSec events' do
it { expect(spans.select { |s| s.get_tag('appsec.event') }).to be_empty }
it { expect(trace.send(:meta)['_dd.appsec.triggers']).to be_nil }
end

shared_examples 'a trace with AppSec events' do
it { expect(spans.select { |s| s.get_tag('appsec.event') }).to_not be_empty }
it { expect(trace.send(:meta)['_dd.appsec.json']).to be_a String }

context 'with appsec disabled' do
let(:appsec_enabled) { false }

it_behaves_like 'a trace without AppSec events'
end
end

context 'with a basic route' do
let(:routes) do
proc do
map '/success' do
run(proc { |_env| [200, { 'Content-Type' => 'text/html' }, ['OK']] })
end
end
end

before do
response
expect(spans).to have(1).items
end

describe 'GET request' do
subject(:response) { get url, params, headers }

let(:url) { '/success' }
let(:params) { {} }
let(:headers) { {} }

context 'with a non-event-triggering request' do
it { is_expected.to be_ok }

it_behaves_like 'a GET 200 span'
it_behaves_like 'a trace with AppSec tags'
it_behaves_like 'a trace without AppSec events'
end

context 'with an event-triggering request in headers' do
let(:headers) { { 'HTTP_USER_AGENT' => 'Nessus SOAP' } }

it { is_expected.to be_ok }
it { expect(triggers).to be_a Array }

it_behaves_like 'a GET 200 span'
it_behaves_like 'a trace with AppSec tags'
it_behaves_like 'a trace with AppSec events'
end

context 'with an event-triggering request in query string' do
let(:params) { { q: '1 OR 1;' } }

it { is_expected.to be_ok }

it_behaves_like 'a GET 200 span'
it_behaves_like 'a trace with AppSec tags'
it_behaves_like 'a trace with AppSec events'
end

context 'with an event-triggering request in IP' do
skip 'TODO: config not implemented'

# TODO: let(:config) { { ip_denylist: ['1.2.3.4'] } }
let(:headers) { { 'HTTP_X_FORWARDED_FOR' => '1.2.3.4' } }

it { is_expected.to be_ok }

# TODO: it_behaves_like 'a GET 403 span'
it_behaves_like 'a trace with AppSec tags'
# TODO: it_behaves_like 'a trace with AppSec events'
end

context 'with an event-triggering response' do
let(:url) { '/admin.php' } # well-known scanned path

it { is_expected.to be_not_found }
it { expect(triggers).to be_a Array }

it_behaves_like 'a GET 404 span'
it_behaves_like 'a trace with AppSec tags'
it_behaves_like 'a trace with AppSec events'
end
end

describe 'POST request' do
subject(:response) { post url, params, headers }

let(:url) { '/success' }
let(:params) { {} }
let(:headers) { {} }

context 'with a non-event-triggering request' do
it { is_expected.to be_ok }

it_behaves_like 'a POST 200 span'
it_behaves_like 'a trace with AppSec tags'
it_behaves_like 'a trace without AppSec events'
end

context 'with an event-triggering request in application/x-www-form-url-encoded body' do
let(:params) { { q: '1 OR 1;' } }

let(:middlewares) do
[
Datadog::Tracing::Contrib::Rack::TraceMiddleware,
Datadog::AppSec::Contrib::Rack::RequestMiddleware,
Datadog::AppSec::Contrib::Rack::RequestBodyMiddleware,
]
end

it { is_expected.to be_ok }

it_behaves_like 'a POST 200 span'
it_behaves_like 'a trace with AppSec tags'
it_behaves_like 'a trace with AppSec events'
end

context 'with an event-triggering request in multipart/form-data body' do
let(:params) { Rack::Test::Utils.build_multipart({ q: '1 OR 1;' }, true, true) }
let(:headers) { { 'CONTENT_TYPE' => "multipart/form-data; boundary=#{Rack::Test::MULTIPART_BOUNDARY}" } }

let(:middlewares) do
[
Datadog::Tracing::Contrib::Rack::TraceMiddleware,
Datadog::AppSec::Contrib::Rack::RequestMiddleware,
Datadog::AppSec::Contrib::Rack::RequestBodyMiddleware,
]
end

it { is_expected.to be_ok }

it_behaves_like 'a POST 200 span'
it_behaves_like 'a trace with AppSec tags'
it_behaves_like 'a trace with AppSec events'
end

context 'with an event-triggering request as JSON' do
let(:middlewares) do
[
Datadog::Tracing::Contrib::Rack::TraceMiddleware,
Datadog::AppSec::Contrib::Rack::RequestMiddleware,
Rack::JSONBodyParser,
Datadog::AppSec::Contrib::Rack::RequestBodyMiddleware,
]
end

let(:params) { JSON.generate('q' => '1 OR 1;') }
let(:headers) { { 'CONTENT_TYPE' => 'application/json' } }

it { is_expected.to be_ok }

it_behaves_like 'a POST 200 span'
it_behaves_like 'a trace with AppSec tags'
it_behaves_like 'a trace with AppSec events'
end
end
end
end
end
Loading

0 comments on commit 35f67c4

Please sign in to comment.