Skip to content
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

New feature and optimizations for security agent #988

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion v3/integrations/nrecho-v3/nrecho.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func Middleware(app *newrelic.Application) func(echo.HandlerFunc) echo.HandlerFu
txn.SetWebResponse(nil).WriteHeader(http.StatusInternalServerError)
}
if newrelic.IsSecurityAgentPresent() {
newrelic.GetSecurityAgentInterface().SendEvent("RESPONSE_HEADER", c.Response().Header())
newrelic.GetSecurityAgentInterface().SendEvent("RESPONSE_HEADER", c.Response().Header(), txn.GetLinkingMetadata().TraceID)
}
}

Expand Down
2 changes: 1 addition & 1 deletion v3/integrations/nrecho-v4/nrecho.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func Middleware(app *newrelic.Application, opts ...ConfigOption) func(echo.Handl
txn.SetWebResponse(nil).WriteHeader(http.StatusInternalServerError)
}
if newrelic.IsSecurityAgentPresent() {
newrelic.GetSecurityAgentInterface().SendEvent("RESPONSE_HEADER", c.Response().Header())
newrelic.GetSecurityAgentInterface().SendEvent("RESPONSE_HEADER", c.Response().Header(), txn.GetLinkingMetadata().TraceID)
}
}

Expand Down
4 changes: 3 additions & 1 deletion v3/integrations/nrgin/nrgin.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ func WrapRouter(engine *gin.Engine) {
}
func middleware(app *newrelic.Application, useNewNames bool) gin.HandlerFunc {
return func(c *gin.Context) {
traceID := ""
if app != nil {
name := c.Request.Method + " " + getName(c, useNewNames)

Expand All @@ -185,10 +186,11 @@ func middleware(app *newrelic.Application, useNewNames bool) gin.HandlerFunc {
defer repl.flushHeader()

c.Set(internal.GinTransactionContextKey, txn)
traceID = txn.GetLinkingMetadata().TraceID
}
c.Next()
if newrelic.IsSecurityAgentPresent() {
newrelic.GetSecurityAgentInterface().SendEvent("RESPONSE_HEADER", c.Writer.Header())
newrelic.GetSecurityAgentInterface().SendEvent("RESPONSE_HEADER", c.Writer.Header(), traceID)
}
}
}
2 changes: 1 addition & 1 deletion v3/integrations/nrgorilla/nrgorilla.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ func Middleware(app *newrelic.Application) mux.MiddlewareFunc {
r = newrelic.RequestWithTransactionContext(r, txn)
next.ServeHTTP(w, r)
if newrelic.IsSecurityAgentPresent() {
newrelic.GetSecurityAgentInterface().SendEvent("RESPONSE_HEADER", w.Header())
newrelic.GetSecurityAgentInterface().SendEvent("RESPONSE_HEADER", w.Header(), txn.GetLinkingMetadata().TraceID)
}
})
}
Expand Down
18 changes: 17 additions & 1 deletion v3/integrations/nrgraphqlgo/nrgraphqlgo.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ type Extension struct{}
var _ graphql.Extension = Extension{}

// Init is used to help you initialize the extension - in this case, a noop
func (Extension) Init(ctx context.Context, _ *graphql.Params) context.Context {
func (Extension) Init(ctx context.Context, params *graphql.Params) context.Context {
if params != nil && newrelic.IsSecurityAgentPresent() {
newrelic.GetSecurityAgentInterface().SendEvent("GRAPHQL", params.RequestString != "", len(params.VariableValues) != 0)
}
return ctx
}

Expand Down Expand Up @@ -79,6 +82,10 @@ func (Extension) ValidationDidStart(ctx context.Context) (context.Context, graph
func (Extension) ExecutionDidStart(ctx context.Context) (context.Context, graphql.ExecutionFinishFunc) {
txn := newrelic.FromContext(ctx)
seg := txn.StartSegment("Execution")
if newrelic.IsSecurityAgentPresent() {
csecData := newrelic.GetSecurityAgentInterface().SendEvent("NEW_GOROUTINE", "")
txn.SetCsecAttributes("CSEC_DATA", csecData)
}
return ctx, func(res *graphql.Result) {
// noticing here also captures those during resolve
for _, err := range res.Errors {
Expand All @@ -91,7 +98,16 @@ func (Extension) ExecutionDidStart(ctx context.Context) (context.Context, graphq
// ResolveFieldDidStart is called at the start of the resolving of a field
func (Extension) ResolveFieldDidStart(ctx context.Context, i *graphql.ResolveInfo) (context.Context, graphql.ResolveFieldFinishFunc) {
seg := newrelic.FromContext(ctx).StartSegment("ResolveField:" + i.FieldName)
if newrelic.IsSecurityAgentPresent() {
txn := newrelic.FromContext(ctx)
csecData := txn.GetCsecAttributes()["CSEC_DATA"]
newrelic.GetSecurityAgentInterface().SendEvent("NEW_GOROUTINE_LINKER", csecData)
}

return ctx, func(interface{}, error) {
if newrelic.IsSecurityAgentPresent() {
newrelic.GetSecurityAgentInterface().SendEvent("NEW_GOROUTINE_END", "")
}
seg.End()
}
}
Expand Down
4 changes: 3 additions & 1 deletion v3/integrations/nrhttprouter/nrhttprouter.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ func (r *Router) HandlerFunc(method, path string, handler http.HandlerFunc) {

// ServeHTTP replaces httprouter.Router.ServeHTTP.
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
traceID := ""
if nil != r.application {
h, _, _ := r.Router.Lookup(req.Method, req.URL.Path)
if nil == h {
Expand All @@ -155,11 +156,12 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {

txn.SetWebRequestHTTP(req)
w = txn.SetWebResponse(w)
traceID = txn.GetLinkingMetadata().TraceID
}
}

r.Router.ServeHTTP(w, req)
if newrelic.IsSecurityAgentPresent() {
newrelic.GetSecurityAgentInterface().SendEvent("RESPONSE_HEADER", w.Header())
newrelic.GetSecurityAgentInterface().SendEvent("RESPONSE_HEADER", w.Header(), traceID)
}
}
81 changes: 72 additions & 9 deletions v3/integrations/nrsecurityagent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,22 +40,85 @@ NEW_RELIC_SECURITY_CONFIG_PATH={YOUR_PATH}/myappsecurity.yaml

The YAML file should have these contents (adjust as needed for your application):
```
# Determines whether the security data is sent to New Relic or not. When this is disabled and agent.enabled is
# true, the security module will run but data will not be sent. Default is false.
enabled: true

# NR security provides two modes IAST and RASP
# Default is IAST
# New Relic Security provides two modes: IAST and RASP
# Default is IAST. Due to the invasive nature of IAST scanning, DO NOT enable this mode in either a
# production environment or an environment where production data is processed.
mode: IAST

# New Relics SaaS connection URLs
# New Relic Security's SaaS connection URL
validator_service_url: wss://csec.nr-data.net

# Following category of security events
# can be disabled from generating.
# These are the category of security events that can be detected. Set to false to disable detection of
# individual event types. Default is true for each event type.
# This config is deprecated,
detection:
rxss:
enabled: true
request:
body_limit:1
rci:
enabled: true
rxss:
enabled: true
deserialization:
enabled: true

# Unique test identifier when runnning IAST with CI/CD
iast_test_identifier: ""

# IAST scan controllers to get more control over IAST analysis
scan_controllers:
# maximum number of replay requests IAST Agent
# can fire in a minute. Default is 3600. Minimum is 12 and maximum is 3600
iast_scan_request_rate_limit: 3600
# The number of application instances for a specific entity where IAST analysis is performed.
# Values are 0 or 1, 0 signifies run on all application instances
scan_instance_count: 0

# The scan_schedule configuration allows to specify when IAST scans should be executed
scan_schedule:
# The delay field specifies the delay in minutes before the IAST scan starts.
# This allows to schedule the scan to start at a later time. In minutes, default is 0 min
delay: 0
# The duration field specifies the duration of the IAST scan in minutes.
# This determines how long the scan will run. In minutes, default is forever
duration: 0
# The schedule field specifies a cron expression that defines when the IAST scan should start.
schedule: ""
# Allow continuously sample collection of IAST events regardless of scan schedule. Default is false
always_sample_traces: false

# The exclude_from_iast_scan configuration allows to specify APIs, parameters,
# and categories that should not be scanned by Security Agents.
exclude_from_iast_scan:
# The api field specifies list of APIs using regular expression (regex) patterns that follow the syntax of Perl 5.
# The regex pattern should provide a complete match for the URL without the endpoint.
api: []
# The http_request_parameters configuration allows users to specify headers, query parameters,
# and body keys that should be excluded from IAST scans.
http_request_parameters:
# A list of HTTP header keys. If a request includes any headers with these keys,
# the corresponding IAST scan will be skipped.
header: []
# A list of query parameter keys. The presence of these parameters in the request's query string
# will lead to skipping the IAST scan.
query: []
# A list of keys within the request body. If these keys are found in the body content,
# the IAST scan will be omitted.
body: []
# The iast_detection_category configuration allows to specify which categories
# of vulnerabilities should not be detected by Security Agents.
iast_detection_category:
insecure_settings: false
invalid_file_access: false
sql_injection: false
nosql_injection: false
ldap_injection: false
javascript_injection: false
command_injection: false
xpath_injection: false
ssrf: false
rxss: false
```

* Based on additional packages imported by the user application, add suitable instrumentation package imports.
Expand Down
2 changes: 1 addition & 1 deletion v3/integrations/nrsecurityagent/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/newrelic/go-agent/v3/integrations/nrsecurityagent
go 1.21

require (
github.com/newrelic/csec-go-agent v1.5.0
github.com/newrelic/csec-go-agent v1.6.0
github.com/newrelic/go-agent/v3 v3.35.0
github.com/newrelic/go-agent/v3/integrations/nrsqlite3 v1.2.0
gopkg.in/yaml.v2 v2.4.0
Expand Down
20 changes: 20 additions & 0 deletions v3/integrations/nrsecurityagent/nrsecurityagent.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,14 @@ func ConfigSecurityFromYaml() ConfigOption {
// NEW_RELIC_SECURITY_MODE scanning mode: "IAST" for now
// NEW_RELIC_SECURITY_AGENT_ENABLED (boolean)
// NEW_RELIC_SECURITY_REQUEST_BODY_LIMIT (integer) set limit on read request body in kb. By default, this is "300"
// NEW_RELIC_SECURITY_IAST_TEST_IDENTIFIER (string) This configuration allows users to specify a unique test identifier when running IAST Scan with CI/CD
//
// NEW_RELIC_SECURITY_SCAN_SCHEDULE_DELAY (integer) The delay field indicated time in minutes before the IAST scan starts after the application starts. By default is 0 min.
// NEW_RELIC_SECURITY_SCAN_SCHEDULE_DURATION (integer) The duration field specifies the duration of the IAST scan in minutes. This determines how long the scan will run. By default is forever.
// NEW_RELIC_SECURITY_SCAN_SCHEDULE_SCHEDULE (string) The schedule field specifies a cron expression that defines when the IAST scan should run.
// NEW_RELIC_SECURITY_SCAN_SCHEDULE_ALWAYS_SAMPLE_TRACES (boolean) always_sample_traces permits IAST to actively gather trace data in the background, and the collected data will be used by Security Agent to perform an IAST Scan at the scheduled time.
// NEW_RELIC_SECURITY_SCAN_CONTROLLERS_IAST_SCAN_REQUEST_RATE_LIMIT (integer) The IAST Scan Rate Limit settings limit the maximum number of analysis probes or requests that can be sent to the application in a minute, By default is 3600.
// NEW_RELIC_SECURITY_SCAN_CONTROLLERS_SCAN_INSTANCE_COUNT (integer) This configuration allows users to the number of application instances for a specific entity where IAST analysis is performed.
//
// NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_IAST_DETECTION_CATEGORY_INSECURE_SETTINGS (boolean)
// NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_IAST_DETECTION_CATEGORY_INVALID_FILE_ACCESS (boolean)
Expand Down Expand Up @@ -167,12 +169,14 @@ func ConfigSecurityFromEnvironment() ConfigOption {
assignBool(&cfg.Security.Agent.Enabled, "NEW_RELIC_SECURITY_AGENT_ENABLED")
assignBool(&cfg.Security.Detection.Rxss.Enabled, "NEW_RELIC_SECURITY_DETECTION_RXSS_ENABLED")
assignInt(&cfg.Security.Request.BodyLimit, "NEW_RELIC_SECURITY_REQUEST_BODY_LIMIT")
assignString(&cfg.Security.IastTestIdentifier, "NEW_RELIC_SECURITY_IAST_TEST_IDENTIFIER")

assignInt(&cfg.Security.ScanSchedule.Delay, "NEW_RELIC_SECURITY_SCAN_SCHEDULE_DELAY")
assignInt(&cfg.Security.ScanSchedule.Duration, "NEW_RELIC_SECURITY_SCAN_SCHEDULE_DURATION")
assignString(&cfg.Security.ScanSchedule.Schedule, "NEW_RELIC_SECURITY_SCAN_SCHEDULE_SCHEDULE")
assignBool(&cfg.Security.ScanSchedule.AllowIastSampleCollection, "NEW_RELIC_SECURITY_SCAN_SCHEDULE_ALWAYS_SAMPLE_TRACES")
assignInt(&cfg.Security.ScanControllers.IastScanRequestRateLimit, "NEW_RELIC_SECURITY_SCAN_CONTROLLERS_IAST_SCAN_REQUEST_RATE_LIMIT")
assignInt(&cfg.Security.ScanControllers.ScanInstanceCount, "NEW_RELIC_SECURITY_SCAN_CONTROLLERS_SCAN_INSTANCE_COUNT")

assignBool(&cfg.Security.ExcludeFromIastScan.IastDetectionCategory.InsecureSettings, "NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_IAST_DETECTION_CATEGORY_INSECURE_SETTINGS")
assignBool(&cfg.Security.ExcludeFromIastScan.IastDetectionCategory.InvalidFileAccess, "NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_IAST_DETECTION_CATEGORY_INVALID_FILE_ACCESS")
Expand Down Expand Up @@ -215,6 +219,14 @@ func ConfigSecurityValidatorServiceEndPointUrl(url string) ConfigOption {
}
}

// ConfigSecurityIastTestIdentifier sets the iast test identifier.
// This configuration allows users to specify a unique test identifier when running IAST Scan with CI/CD.
func ConfigSecurityIastTestIdentifier(testIdentifier string) ConfigOption {
return func(cfg *SecurityConfig) {
cfg.Security.IastTestIdentifier = testIdentifier
}
}

// ConfigSecurityDetectionDisableRxss is used to enable or disable RXSS validation.
func ConfigSecurityDetectionDisableRxss(isDisable bool) ConfigOption {
return func(cfg *SecurityConfig) {
Expand Down Expand Up @@ -275,3 +287,11 @@ func ConfigIastScanRequestRateLimit(limit int) ConfigOption {
cfg.Security.ScanControllers.IastScanRequestRateLimit = limit
}
}

// ConfigScanIstanceCount is used to set scan instance count.
// This configuration allows users to the number of application instances for a specific entity where IAST analysis is performed.
func ConfigScanInstanceCount(limit int) ConfigOption {
return func(cfg *SecurityConfig) {
cfg.Security.ScanControllers.ScanInstanceCount = limit
}
}
2 changes: 1 addition & 1 deletion v3/newrelic/instrumentation.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func WrapHandle(app *Application, pattern string, handler http.Handler, options

handler.ServeHTTP(w, r)
if IsSecurityAgentPresent() {
secureAgent.SendEvent("RESPONSE_HEADER", w.Header())
secureAgent.SendEvent("RESPONSE_HEADER", w.Header(), txn.GetLinkingMetadata().TraceID)
}
})
}
Expand Down
3 changes: 1 addition & 2 deletions v3/newrelic/internal_response_writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,8 @@ func (rw *replacementResponseWriter) Write(b []byte) (n int, err error) {
n, err = rw.original.Write(b)

headersJustWritten(rw.thd, http.StatusOK, hdr)

if IsSecurityAgentPresent() {
secureAgent.SendEvent("INBOUND_WRITE", string(b), hdr)
secureAgent.SendEvent("INBOUND_WRITE", string(b), hdr, rw.thd.GetLinkingMetadata().TraceID)
}
return
}
Expand Down
7 changes: 5 additions & 2 deletions v3/newrelic/internal_txn.go
Original file line number Diff line number Diff line change
Expand Up @@ -1396,13 +1396,16 @@ func (txn *txn) setCsecData() {
}
}

func (txn *txn) getCsecAttributes() any {
func (txn *txn) getCsecAttributes() map[string]any {
txn.Lock()
defer txn.Unlock()
if txn.csecAttributes == nil {
return map[string]any{}
}
return txn.csecAttributes
}

func (txn *txn) setCsecAttributes(key, value string) {
func (txn *txn) setCsecAttributes(key string, value any) {
txn.Lock()
defer txn.Unlock()
if txn.csecAttributes == nil {
Expand Down
6 changes: 3 additions & 3 deletions v3/newrelic/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func (txn *Transaction) End() {
}
}
if txn.thread.IsWeb && IsSecurityAgentPresent() {
secureAgent.SendEvent("INBOUND_END", "")
secureAgent.SendEvent("INBOUND_END", txn.GetLinkingMetadata().TraceID)
}
txn.thread.logAPIError(txn.thread.End(r), "end transaction", nil)
}
Expand Down Expand Up @@ -545,14 +545,14 @@ func (txn *Transaction) IsSampled() bool {
return txn.thread.IsSampled()
}

func (txn *Transaction) GetCsecAttributes() any {
func (txn *Transaction) GetCsecAttributes() map[string]any {
if txn == nil || txn.thread == nil {
return nil
}
return txn.thread.getCsecAttributes()
}

func (txn *Transaction) SetCsecAttributes(key, value string) {
func (txn *Transaction) SetCsecAttributes(key string, value any) {
if txn == nil || txn.thread == nil {
return
}
Expand Down
Loading