Skip to content

Security monitoring feature parity with Agent V2#1463

Open
kamalchaturvedi wants to merge 6 commits intonginx:mainfrom
kamalchaturvedi:nginx_one_security_monitoring
Open

Security monitoring feature parity with Agent V2#1463
kamalchaturvedi wants to merge 6 commits intonginx:mainfrom
kamalchaturvedi:nginx_one_security_monitoring

Conversation

@kamalchaturvedi
Copy link
Contributor

@kamalchaturvedi kamalchaturvedi commented Dec 18, 2025

Proposed changes

Purpose of this PR is to over the gaps between the current custom securityviolationsprocessor in agent V3, with what level of parsing and transformations we had in agent V2 security-monitoring processor.
Additionally, a protobuf schema is defined to serve as contract between the agent and the downstream consumer.

Commit 1:
The securityviolationsprocessor now processes NGINX App Protect WAF syslog messages, and transforms them into SecurityViolationEvent protobuf messages. This protobuf definition replaces the existing struct definition in /internal folder. This was done to allow management-plane to import this schema as a contract for handling security violations.

Commit 2:
Additionally, added the following capabilities to the parsing the details extraction from raw violations, to bring the feature in parity with Agent V2 implementation:

  1. Parses XML violation details with context extraction (parameter, header, cookie, uri, request)
  2. Extracts attack signature details

Commit 3:
These changes were thoroughly tested with addition of /testdata in Agent V2 implementation (https://github.com/nginx/agent/tree/dev-v2/src/extensions/nginx-app-protect/monitoring/processor/testdata) and additional variety of violations, to ensure robust coverage.

Commit 4:
Added NAP V5 support: Lookup for docker0 interface IP, which is then utilized to validate for configured syslog IP in NGINX Config.

Commit 5:
Added architecture diagram to describe the custom securityviolations processor.

Agent Config Modifications for Test

features:
  - certificates
  - configuration
  - metrics
  - file-watcher
  - api-action
  - logs-nap 

collector: 
  exporters:
    debug: {}
  processors:
    batch:
      "logs":
        send_batch_size: 1000
        timeout: 30s
        send_batch_max_size: 1000
  pipelines:
   logs:
     "default-security-events":
       receivers: ["tcplog/nginx_app_protect"]
       processors: ["securityviolations/default","batch/logs"]
       exporters: ["debug","otlp/default"]

Testing

Violations Triggered

  1. curl -v 'http://127.0.0.1/myfile.tmp'

Expected Violations: VIOL_FILETYPE, VIOL_HTTP_PROTOCOL, VIOL_BOT_CLIENT

Output Payload:

{"resource": {"service.instance.id": "f904b8d3-4d6f-46b8-9b77-ba3926cc3407", "service.name": "otel-nginx-agent", "service.version": "v3.6.2"}, "otelcol.component.id": "securityviolations/default", "otelcol.component.kind": "processor", "otelcol.pipeline.id": "logs/default-security-events", "otelcol.signal": "logs", "protobuf": "policy_name:\"nms_app_protect_strict_policy\"  support_id:\"2378510662008429009\"  request_outcome:REQUEST_OUTCOME_REJECTED  request_outcome_reason:SECURITY_WAF_VIOLATION  blocking_exception_reason:\"N/A\"  method:\"GET\"  protocol:\"HTTP\"  xff_header_value:\"N/A\"  uri:\"/myfile.tmp\"  request:\"GET /myfile.tmp HTTP/1.1\\\\r\\\\nHost: 127.0.0.1\\\\r\\\\nUser-Agent: curl/8.5.0\\\\r\\\\nAccept: */*\\\\r\\\\n\\\\r\\\\n\"  request_status:REQUEST_STATUS_BLOCKED  vs_name:\"1-localhost:1-/\"  remote_addr:\"127.0.0.1\"  destination_port:80  server_port:33336  violations:\"HTTP protocol compliance failed::Illegal file type::Bot Client Detected\"  sub_violations:\"HTTP protocol compliance failed:Host header contains IP address\"  violation_rating:2  sig_set_names:\"N/A\"  sig_cves:\"N/A\"  client_class:\"Untrusted Bot\"  client_application:\"N/A\"  client_application_version:\"N/A\"  severity:SEVERITY_CRITICAL  threat_campaign_names:\"N/A\"  bot_anomalies:\"N/A\"  bot_category:\"HTTP Library\"  enforced_bot_anomalies:\"N/A\"  bot_signature_name:\"curl\"  system_id:\"ea6197e981ac\"  parent_hostname:\"ea6197e981ac\"  violations_data:{violation_data_name:\"VIOL_HTTP_PROTOCOL\"  violation_data_context_data:{}}  violations_data:{violation_data_name:\"VIOL_FILETYPE\"  violation_data_context_data:{}}  violations_data:{violation_data_name:\"VIOL_BOT_CLIENT\"  violation_data_context_data:{}}"}
  1. curl -X SEARCH -k -v 'http://127.0.0.1/hello'

Expected Violations: VIOL_METHOD, VIOL_HTTP_PROTOCOL, VIOL_BOT_CLIENT

Output Payload:

{"resource": {"service.instance.id": "f904b8d3-4d6f-46b8-9b77-ba3926cc3407", "service.name": "otel-nginx-agent", "service.version": "v3.6.2"}, "otelcol.component.id": "securityviolations/default", "otelcol.component.kind": "processor", "otelcol.pipeline.id": "logs/default-security-events", "otelcol.signal": "logs", "protobuf": "policy_name:\"nms_app_protect_strict_policy\"  support_id:\"2378510662008429519\"  request_outcome:REQUEST_OUTCOME_REJECTED  request_outcome_reason:SECURITY_WAF_VIOLATION  blocking_exception_reason:\"N/A\"  method:\"SEARCH\"  protocol:\"HTTP\"  xff_header_value:\"N/A\"  uri:\"/hello\"  request:\"SEARCH /hello HTTP/1.1\\\\r\\\\nHost: 127.0.0.1\\\\r\\\\nUser-Agent: curl/8.5.0\\\\r\\\\nAccept: */*\\\\r\\\\n\\\\r\\\\n\"  request_status:REQUEST_STATUS_BLOCKED  vs_name:\"1-localhost:1-/\"  remote_addr:\"127.0.0.1\"  destination_port:80  server_port:33344  violations:\"HTTP protocol compliance failed::Illegal method::Bot Client Detected\"  sub_violations:\"HTTP protocol compliance failed:Host header contains IP address\"  violation_rating:2  sig_set_names:\"N/A\"  sig_cves:\"N/A\"  client_class:\"Untrusted Bot\"  client_application:\"N/A\"  client_application_version:\"N/A\"  severity:SEVERITY_CRITICAL  threat_campaign_names:\"N/A\"  bot_anomalies:\"N/A\"  bot_category:\"HTTP Library\"  enforced_bot_anomalies:\"N/A\"  bot_signature_name:\"curl\"  system_id:\"ea6197e981ac\"  parent_hostname:\"ea6197e981ac\"  violations_data:{violation_data_name:\"VIOL_HTTP_PROTOCOL\"  violation_data_context_data:{}}  violations_data:{violation_data_name:\"VIOL_METHOD\"  violation_data_context_data:{}}  violations_data:{violation_data_name:\"VIOL_BOT_CLIENT\"  violation_data_context_data:{}}"}

Expected Violations: VIOL_ATTACK_SIGNATURE, VIOL_HTTP_PROTOCOL, VIOL_BOT_CLIENT, VIOL_URL_METACHAR, VIOL_RATING_THREAT
Expected Signature IDs ": 200000099, 200000093

Output Payload:

{"resource": {"service.instance.id": "f904b8d3-4d6f-46b8-9b77-ba3926cc3407", "service.name": "otel-nginx-agent", "service.version": "v3.6.2"}, "otelcol.component.id": "securityviolations/default", "otelcol.component.kind": "processor", "otelcol.pipeline.id": "logs/default-security-events", "otelcol.signal": "logs", "protobuf": "policy_name:\"nms_app_protect_strict_policy\"  support_id:\"2378510662008430029\"  request_outcome:REQUEST_OUTCOME_REJECTED  request_outcome_reason:SECURITY_WAF_VIOLATION  blocking_exception_reason:\"N/A\"  method:\"GET\"  protocol:\"HTTP\"  xff_header_value:\"N/A\"  uri:\"/a=<script>getAllMoney()</script>\"  request:\"GET /a=<script>getAllMoney()</script> HTTP/1.1\\\\r\\\\nHost: 127.0.0.1\\\\r\\\\nUser-Agent: curl/8.5.0\\\\r\\\\nAccept: */*\\\\r\\\\n\\\\r\\\\n\"  request_status:REQUEST_STATUS_BLOCKED  vs_name:\"1-localhost:1-/\"  remote_addr:\"127.0.0.1\"  destination_port:80  server_port:33358  violations:\"HTTP protocol compliance failed::Illegal meta character in URL::Attack signature detected::Violation Rating Threat detected::Bot Client Detected\"  sub_violations:\"HTTP protocol compliance failed:Host header contains IP address\"  violation_rating:5  sig_set_names:\"{High Accuracy Signatures;Cross Site Scripting Signatures;Generic Detection Signatures (High/Medium Accuracy)}\"  sig_cves:\"{High Accuracy Signatures;Cross Site Scripting Signatures;Generic Detection Signatures (High/Medium Accuracy)}\"  client_class:\"Untrusted Bot\"  client_application:\"N/A\"  client_application_version:\"N/A\"  severity:SEVERITY_CRITICAL  threat_campaign_names:\"N/A\"  bot_anomalies:\"N/A\"  bot_category:\"HTTP Library\"  enforced_bot_anomalies:\"N/A\"  bot_signature_name:\"curl\"  system_id:\"ea6197e981ac\"  parent_hostname:\"ea6197e981ac\"  violations_data:{violation_data_name:\"VIOL_ATTACK_SIGNATURE\"  violation_data_context:\"uri\"  violation_data_context_data:{context_data_name:\"uri\"  context_data_value:\"/a=<script>getAllMoney()</script>\"}  violation_data_signatures:{sig_data_id:200000099  sig_data_blocking_mask:\"3\"  sig_data_buffer:\"/a=<script>getAllMoney()</script>\"  sig_data_offset:3  sig_data_length:7}  violation_data_signatures:{sig_data_id:200000093  sig_data_blocking_mask:\"3\"  sig_data_buffer:\"/a=<script>getAllMoney()</script>\"  sig_data_offset:4  sig_data_length:7}}  violations_data:{violation_data_name:\"VIOL_HTTP_PROTOCOL\"  violation_data_context_data:{}}  violations_data:{violation_data_name:\"VIOL_URL_METACHAR\"  violation_data_context:\"uri\"  violation_data_context_data:{context_data_name:\"uri\"  context_data_value:\"L2E9PHNjcmlwdD5nZXRBbGxNb25leSgpPC9zY3JpcHQ+\"}}  violations_data:{violation_data_name:\"VIOL_URL_METACHAR\"  violation_data_context:\"uri\"  violation_data_context_data:{context_data_name:\"uri\"  context_data_value:\"L2E9PHNjcmlwdD5nZXRBbGxNb25leSgpPC9zY3JpcHQ+\"}}  violations_data:{violation_data_name:\"VIOL_URL_METACHAR\"  violation_data_context:\"uri\"  violation_data_context_data:{context_data_name:\"uri\"  context_data_value:\"L2E9PHNjcmlwdD5nZXRBbGxNb25leSgpPC9zY3JpcHQ+\"}}  violations_data:{violation_data_name:\"VIOL_BOT_CLIENT\"  violation_data_context_data:{}}  violations_data:{violation_data_name:\"VIOL_RATING_THREAT\"  violation_data_context_data:{}}"}

Checklist

Before creating a PR, run through this checklist and mark each as complete.

  • I have read the CONTRIBUTING document
  • I have run make install-tools and have attached any dependency changes to this pull request
  • If applicable, I have added tests that prove my fix is effective or that my feature works
  • If applicable, I have checked that any relevant tests pass after adding my changes
  • If applicable, I have updated any relevant documentation (README.md)
  • If applicable, I have tested my cross-platform changes on Ubuntu 22, Redhat 8, SUSE 15 and FreeBSD 13

@kamalchaturvedi kamalchaturvedi requested a review from a team as a code owner December 18, 2025 05:28
@github-actions
Copy link
Contributor

github-actions bot commented Dec 18, 2025

✅ All required contributors have signed the F5 CLA for this PR. Thank you!
Posted by the CLA Assistant Lite bot.

@github-actions github-actions bot added chore Pull requests for routine tasks documentation Improvements or additions to documentation enhancement New feature or request labels Dec 18, 2025
@kamalchaturvedi kamalchaturvedi force-pushed the nginx_one_security_monitoring branch from b3af22a to 5271a0f Compare December 18, 2025 17:34
@kamalchaturvedi kamalchaturvedi force-pushed the nginx_one_security_monitoring branch from cbf8c1d to 36b2d35 Compare January 27, 2026 20:39
@kamalchaturvedi kamalchaturvedi changed the title Draft: Security monitoring feature parity with Agent V2 Security monitoring feature parity with Agent V2 Jan 28, 2026
if i >= len(fieldOrder) {
break
}
fieldValueMap[fieldOrder[i]] = strings.TrimSpace(field)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we prebuild this map? If we expect to be high-volume, rebuilding the map log parse is probably not needed?

Copy link
Contributor Author

@kamalchaturvedi kamalchaturvedi Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now allocating 33 size to the map upon initialization. Anything additional you were thinking to optimize this further ?


parts := strings.Split(value, ",")

var trimmedParts []string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, you can preallocate with length

trimmedParts := make([]string, 0, len(parts))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was in the legacy code and no longer needed. Removed.

// Remove the "ASM:" prefix if present so we only process the values
message = strings.TrimPrefix(message, "ASM:")

fields := strings.Split(message, ",")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did we explore the golang csv parser? https://pkg.go.dev/encoding/csv

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. Updated to using standard csv parser.

@kamalchaturvedi kamalchaturvedi force-pushed the nginx_one_security_monitoring branch from 0f31873 to e08cb5d Compare January 29, 2026 04:39
@CVanF5
Copy link
Collaborator

CVanF5 commented Jan 29, 2026

Hi Kunal, thanks for the PR!

My only comment is that this implementation converts App Protect security violations into a custom/proprietary format that essentially uses OTel as a pipe. It does the job, no doubt.

Taking a step back, one of the key reasons we have OTel in NGINX Agent V3 is to enable customers to view their own telemetry. The goal is to able users to customize Agent's OTel collector and send telemetry to their own, perhaps private collectors. Or they have a plan with Datadog or some cloud provider. If the security violations don't follow the OTel standard for log processing, then it makes it that much harder (if not impossible) for the customer to view the security violations on their own systems. This is a key feature of Agent V3 and something we strive to maintain.

…asic struct.

    This has been done to allow for management-plane can reference it as a contract with backward/forward compatibility
… adds additional assertions and validations to ensure of expected final output
@kamalchaturvedi kamalchaturvedi force-pushed the nginx_one_security_monitoring branch from e08cb5d to dd8047f Compare January 31, 2026 00:14
@kamalchaturvedi
Copy link
Contributor Author

Hi Kunal, thanks for the PR!

My only comment is that this implementation converts App Protect security violations into a custom/proprietary format that essentially uses OTel as a pipe. It does the job, no doubt.

Taking a step back, one of the key reasons we have OTel in NGINX Agent V3 is to enable customers to view their own telemetry. The goal is to able users to customize Agent's OTel collector and send telemetry to their own, perhaps private collectors. Or they have a plan with Datadog or some cloud provider. If the security violations don't follow the OTel standard for log processing, then it makes it that much harder (if not impossible) for the customer to view the security violations on their own systems. This is a key feature of Agent V3 and something we strive to maintain.

Hi Chris. Yes, understood. It makes complete sense to continue allowing users to build their own unique telemetry experiences, which I believe does not get disrupted with this processor.

  • This custom processor is just another pluggable processor, that can be enabled via the specification of a log pipeline, as done in the example above.
  • We are still conformant to the standard OTEL protocol and format. The protobuf is just encoded as bytes array to the log-record body. The structure of the output message is still OTLP.
  • If the customer intends to process these violations in their own system, they get a properly structured violation with this custom processor, with a clearly defined contract. The default syslog outputted log from NAP engine is jumbled (text, json and xml combination), which is very hard to parse and make any sense of.

Here is an example anonymized default syslog output violation:

Aug 18 09:15:23 server-01 ASM:attack_type="Non-browser Client,Forceful Browsing",blocking_exception_reason="N/A",date_time="2025-08-18 09:15:22",dest_port="81",ip_client="192.0.2.100",is_truncated="false",method="GET",policy_name="app_protect_policy",protocol="HTTP",request_status="blocked",response_code="0",severity="N/A",sig_cves="N/A",sig_ids="N/A",sig_names="N/A",sig_set_names="N/A",src_port="45366",sub_violations="N/A",support_id="1234567890123456789",threat_campaign_names="N/A",unit_hostname="server-01",uri="/index.tmp",violation_rating="2",vs_name="example.com:1-/",x_forwarded_for_header_value="192.0.2.100",outcome="REJECTED",outcome_reason="SECURITY_WAF_VIOLATION",violations="Illegal file type,Bot Client Detected",json_log="{""id"":""1234567890123456789"",""violations"":[{""enforcementState"":{""isBlocked"":true,""isAlarmed"":true,""isLearned"":false,""attackType"":[{""name"":""Forceful Browsing""}]},""violation"":{""name"":""VIOL_FILETYPE""},""policyEntity"":{""filetypes"":[{""name"":""tmp"",""allowed"":false}]}},{""enforcementState"":{""isBlocked"":false,""isAlarmed"":true,""isLearned"":true,""attackType"":[{""name"":""Non-browser Client""}]},""violation"":{""name"":""VIOL_BOT_CLIENT""},""botSignature"":{""name"":""curl"",""category"":""HTTP Library"",""botClass"":""Untrusted Bot""}}],""enforcementAction"":""block"",""method"":""GET"",""clientPort"":45366,""clientIp"":""192.0.2.100"",""host"":""internal-host"",""responseCode"":0,""serverIp"":""0.0.0.0"",""serverPort"":81,""requestStatus"":""blocked"",""url"":""L2luZGV4LnRtcA=="",""virtualServerName"":""example.com:1-/"",""geolocationCountryCode"":""US"",""enforcementState"":{""isBlocked"":true,""isAlarmed"":true,""rating"":2,""attackType"":[{""name"":""Non-browser Client""},{""name"":""Forceful Browsing""}],""ratingIncludingViolationsInStaging"":2,""stagingCertificationDatetime"":""2026-01-01T00:00:00Z""},""requestDatetime"":""2025-08-18T09:15:22Z"",""rawRequest"":{""actualSize"":111,""httpRequest"":""R0VUIC9pbmRleC50bXAgSFRUUC8xLjENCkhvc3Q6IGV4YW1wbGUuY29tDQpVc2VyLUFnZW50OiBjdXJsLzcuNTguMA0KQWNjZXB0OiAqLyoNClgtRm9yd2FyZGVkLUZvcjogMTkyLjAuMi4xMDANCg0K"",""isTruncated"":false},""requestPolicy"":{""fullPath"":""app_protect_policy""}}",violation_details="<?xml version='1.0' encoding='UTF-8'?><BAD_MSG><violation_masks><block>2414200001008faa-ba030308b0000072-8000000000000000-0</block><alarm>2475f0ffcb9d8fea-befbf358b000007e-f400000000000000-0</alarm><learn>412200000008f8a-b0000020-0-0</learn><staging>0-0-0-0</staging></violation_masks><request-violations><violation><viol_index>39</viol_index><viol_name>VIOL_FILETYPE</viol_name><extension>dG1w</extension><flg_disallowed_file_type>1</flg_disallowed_file_type></violation><violation><viol_index>122</viol_index><viol_name>VIOL_BOT_CLIENT</viol_name></violation></request-violations></BAD_MSG>",bot_signature_name="curl",bot_category="HTTP Library",bot_anomalies="N/A",enforced_bot_anomalies="N/A",client_class="Untrusted Bot",client_application="N/A",client_application_version="N/A",request="GET /index.tmp HTTP/1.1\r\nHost: example.com\r\nUser-Agent: curl/7.58.0\r\nAccept: */*\r\nX-Forwarded-For: 192.0.2.100\r\n\r\n",transport_protocol="HTTP/1.1")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

chore Pull requests for routine tasks documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants