-
Notifications
You must be signed in to change notification settings - Fork 9
[APPSEC-61865] (WIP) Add inferred span and AppSec calls for API Gateway #136
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
Strech
wants to merge
24
commits into
main
Choose a base branch
from
appsec-61865-add-inferred-span-for-api-gateway
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
04ef96d
EXPERIMENTAL
Strech 1da82aa
EXPERIMENTAL Add appsec to the request flow
Strech 0b6c3da
EXPERIMENTAL Add inferred span
Strech 0091b67
Simplify AppSec to pure gateway push with raw PORO data
Strech 4054800
Extract AppSec and InferredSpan modules from Listener
Strech 2e62588
Remove debug logging and revert accidental changes
Strech 416bd75
Move AppSec instrumentation to configure_apm, consolidate managed_ser…
Strech 8c09e5f
Add specs for AppSec and InferredSpan modules
Strech 32dfab7
Fix trace/span type confusion in AppSec context lifecycle
Strech 7596aee
Remove unnecessary change
Strech 84cb13f
Remove appsec request headers injection
Strech 9851294
Ignore test/ directory in RuboCop
Strech 8790c58
Move InferredSpan outside of Trace folder
Strech d5940df
Extract API Gateway parsers and rewrite InferredSpan as dispatcher
Strech cf7d919
Remove InferredSpan.finish wrapper and http.status_code tag
Strech 4b2b218
Make AppSec.on_start accept trace and span as explicit keyword arguments
Strech 9dc1bfa
Add enabled? guard to AppSec.on_finish and harden create_context
Strech c92e234
Remove managed_services_enabled? gate from InferredSpan
Strech aa04c33
Address PR review: trivial fixes across formatting, comments, and specs
Strech a5941fa
Address PR review: small structural fixes across source and specs
Strech 954a0e7
Address PR review: rename PARSERS to EVENT_SOURCES and extract ARN co…
Strech 44beb0a
Address PR review: test behavior not implementation in appsec spec
Strech eee4562
Address PR review: replace captured kwargs with behavioral assertion …
Strech 8862bfc
Address PR review: refactor build_span with kwargs and extracted methods
Strech File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,7 @@ | ||
| AllCops: | ||
| TargetRubyVersion: 3.2 | ||
| Exclude: | ||
| - 'test/**/*' | ||
|
|
||
| Metrics/MethodLength: | ||
| Max: 20 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module Datadog | ||
| module Lambda | ||
| module AppSec | ||
| class << self | ||
| def on_start(event, trace:, span:) | ||
| return unless enabled? | ||
|
|
||
| create_context(trace, span) | ||
| return unless Datadog::AppSec::Context.active | ||
|
|
||
| Datadog::AppSec::Instrumentation.gateway.push('aws_lambda.request.start', event) | ||
| rescue StandardError => e | ||
| Datadog::Utils.logger.debug "failed to start AppSec: #{e}" | ||
| end | ||
|
|
||
| def on_finish(response) | ||
| return unless enabled? | ||
|
|
||
| context = Datadog::AppSec::Context.active | ||
| return unless context | ||
|
|
||
| Datadog::AppSec::Instrumentation.gateway.push('aws_lambda.response.start', response) | ||
| Datadog::AppSec::Event.record(context, request: context.state[:request]) | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| context.export_metrics | ||
| context.export_request_telemetry | ||
| rescue StandardError => e | ||
| Datadog::Utils.logger.debug "failed to finish AppSec: #{e}" | ||
| ensure | ||
| Datadog::AppSec::Context.deactivate if context | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def enabled? | ||
| defined?(Datadog::AppSec) && | ||
| Datadog::AppSec.respond_to?(:enabled?) && | ||
| Datadog::AppSec.enabled? | ||
| end | ||
|
|
||
| def create_context(trace, span) | ||
| return if trace.nil? || span.nil? | ||
|
|
||
| security_engine = Datadog::AppSec.security_engine | ||
| return unless security_engine | ||
|
|
||
| Datadog::AppSec::Context.activate( | ||
| Datadog::AppSec::Context.new(trace, span, security_engine.new_runner) | ||
| ) | ||
|
|
||
| span.set_metric(Datadog::AppSec::Ext::TAG_APPSEC_ENABLED, 1) | ||
| end | ||
| end | ||
| end | ||
| end | ||
| end | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require_relative 'inferred_span/api_gateway_v1' | ||
| require_relative 'inferred_span/api_gateway_v2' | ||
|
|
||
| module Datadog | ||
| module Lambda | ||
| # Creates inferred spans representing upstream services | ||
| # in the Lambda invocation path (e.g. API Gateway). | ||
| # | ||
| # @see https://docs.datadoghq.com/tracing/trace_collection/proxy_setup/apigateway/ | ||
| module InferredSpan | ||
| EVENT_SOURCES = [ApiGatewayV1, ApiGatewayV2].freeze | ||
| ARN_REGION_INDEX = 3 | ||
| ARN_SPLIT_LIMIT = 5 | ||
|
|
||
| class << self | ||
| def create(event, request_context, trace_digest) | ||
| klass = EVENT_SOURCES.find { |event_source| event_source.match?(event) } | ||
| return unless klass | ||
|
|
||
| start_span(klass.new(event), request_context: request_context, trace_digest: trace_digest) | ||
| rescue StandardError => e | ||
| Datadog::Utils.logger.debug "failed to create inferred span: #{e}" | ||
| nil | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def start_span(event_source, request_context:, trace_digest:) | ||
| resource = "#{event_source.method} #{event_source.resource_path}" | ||
|
|
||
| tags = { | ||
| 'http.method' => event_source.method, | ||
| 'http.url' => http_url_for(event_source), | ||
| 'http.route' => event_source.resource_path, | ||
| 'endpoint' => event_source.path, | ||
| 'resource_names' => resource, | ||
| 'span.kind' => 'server', | ||
| 'apiid' => event_source.api_id, | ||
| 'apiname' => event_source.api_id, | ||
| 'stage' => event_source.stage, | ||
| 'request_id' => request_context.aws_request_id, | ||
| '_inferred_span.synchronicity' => 'sync', | ||
| '_inferred_span.tag_source' => 'self' | ||
| } | ||
|
|
||
| resource_key = resource_key_for(event_source, request_context) | ||
| tags['dd_resource_key'] = resource_key if resource_key | ||
| tags['http.useragent'] = event_source.user_agent if event_source.user_agent | ||
|
|
||
| options = { | ||
| service: event_source.domain.empty? ? nil : event_source.domain, | ||
| resource: resource, | ||
| type: 'web', | ||
| tags: tags | ||
| } | ||
| options[:continue_from] = trace_digest if trace_digest | ||
| options[:start_time] = ms_to_time(event_source.request_time_ms) if event_source.request_time_ms | ||
|
|
||
| span = Datadog::Tracing.trace(event_source.span_name, **options) | ||
| span.set_metric('_dd._inferred_span', 1.0) | ||
| span | ||
| end | ||
|
|
||
| def http_url_for(event_source) | ||
| return event_source.path if event_source.domain.empty? | ||
|
|
||
| "https://#{event_source.domain}#{event_source.path}" | ||
| end | ||
|
|
||
| def resource_key_for(event_source, request_context) | ||
| arn = request_context.invoked_function_arn.to_s | ||
| return unless arn.include?(':') | ||
|
|
||
| region = arn.split(':', ARN_SPLIT_LIMIT)[ARN_REGION_INDEX] | ||
| return if event_source.api_id.empty? || event_source.stage.empty? | ||
|
|
||
| "arn:aws:apigateway:#{region}::/#{event_source.arn_path_prefix}/#{event_source.api_id}/stages/#{event_source.stage}" | ||
| end | ||
|
|
||
| def ms_to_time(ms) | ||
| Time.at(ms / 1000.0) | ||
| end | ||
| end | ||
| end | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module Datadog | ||
| module Lambda | ||
| module InferredSpan | ||
| # Parses API Gateway REST API (v1) Lambda proxy integration events | ||
| # into a uniform interface. | ||
| # | ||
| # @see https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format | ||
| class ApiGatewayV1 | ||
| class << self | ||
| def match?(payload) | ||
| api_gateway?(payload) && payload.key?('httpMethod') | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def api_gateway?(payload) | ||
| payload.is_a?(Hash) && | ||
| payload.key?('requestContext') && payload['requestContext'].key?('stage') | ||
| end | ||
| end | ||
|
|
||
| def initialize(payload) | ||
| @payload = payload | ||
| @request_context = payload.fetch('requestContext', {}) | ||
| end | ||
|
|
||
| def span_name = 'aws.apigateway' | ||
| def method = @payload['httpMethod'] | ||
| def path = @payload.fetch('path', '/') | ||
| def resource_path = @request_context.fetch('resourcePath', path) | ||
| def domain = @request_context.fetch('domainName', '') | ||
| def api_id = @request_context.fetch('apiId', '') | ||
| def stage = @request_context.fetch('stage', '') | ||
| def request_time_ms = @request_context['requestTimeEpoch'] | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No fallback & coersion? |
||
| def user_agent = @request_context.dig('identity', 'userAgent') | ||
| def arn_path_prefix = 'restapis' | ||
| end | ||
| end | ||
| end | ||
| end | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module Datadog | ||
| module Lambda | ||
| module InferredSpan | ||
| # Parses API Gateway HTTP API (v2) Lambda proxy integration events | ||
| # into a uniform interface. | ||
| # | ||
| # @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html#http-api-develop-integrations-lambda.proxy-format | ||
| class ApiGatewayV2 | ||
| class << self | ||
| def match?(payload) | ||
| api_gateway?(payload) && payload.key?('routeKey') | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def api_gateway?(payload) | ||
| payload.is_a?(Hash) && | ||
| payload.key?('requestContext') && payload['requestContext'].key?('stage') | ||
| end | ||
| end | ||
|
|
||
| def initialize(payload) | ||
| @payload = payload | ||
| @request_context = payload.fetch('requestContext', {}) | ||
| @http = @request_context.fetch('http', {}) | ||
| end | ||
|
|
||
| def span_name = 'aws.httpapi' | ||
| def method = @http['method'] | ||
| def path = @payload.fetch('rawPath', '/') | ||
| def resource_path = @payload['routeKey']&.sub(/\A[A-Z]+ /, '') || path | ||
| def domain = @request_context.fetch('domainName', '') | ||
| def api_id = @request_context.fetch('apiId', '') | ||
| def stage = @request_context.fetch('stage', '') | ||
| def request_time_ms = @request_context['timeEpoch'] | ||
| def user_agent = @http['userAgent'] | ||
| def arn_path_prefix = 'apis' | ||
| end | ||
| end | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense to add comment about instrumentation guard on enabled from AppSec settings class and no auto instrumentation