securenow 7.6.9 → 7.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/mcp/catalog.js CHANGED
@@ -230,8 +230,11 @@ const TOOLS = [
230
230
  readOnly: true,
231
231
  method: 'GET',
232
232
  endpoint: '/firewall/status',
233
- queryFields: ['environment'],
234
- inputSchema: objectSchema({ ...environmentInput }),
233
+ queryFields: ['environment', 'appKey'],
234
+ inputSchema: objectSchema({
235
+ appKey: string('Optional application key UUID to scope the status check.'),
236
+ ...environmentInput,
237
+ }),
235
238
  },
236
239
  {
237
240
  name: 'securenow_firewall_enable',
@@ -297,9 +300,10 @@ const TOOLS = [
297
300
  method: 'GET',
298
301
  endpoint: '/firewall/check/:ip',
299
302
  pathParams: ['ip'],
300
- queryFields: ['environment'],
303
+ queryFields: ['environment', 'appKey'],
301
304
  inputSchema: objectSchema({
302
305
  ip: string('IPv4 address to test.'),
306
+ appKey: string('Optional application key UUID to test the app/environment toggle and scoped lists.'),
303
307
  ...environmentInput,
304
308
  }, ['ip']),
305
309
  },
@@ -421,6 +425,19 @@ const TOOLS = [
421
425
  id: string('Notification id.'),
422
426
  }, ['id']),
423
427
  },
428
+ {
429
+ name: 'securenow_notifications_batch_get',
430
+ title: 'Get Notifications Batch',
431
+ description: 'Fetch multiple notification/case records in one request.',
432
+ scope: 'notifications:read',
433
+ readOnly: true,
434
+ method: 'POST',
435
+ endpoint: '/notifications/batch',
436
+ bodyFields: ['ids'],
437
+ inputSchema: objectSchema({
438
+ ids: arrayOfStrings('Notification ids to fetch, up to 50.'),
439
+ }, ['ids']),
440
+ },
424
441
  {
425
442
  name: 'securenow_human_actions_list',
426
443
  title: 'List Human Action Queue',
@@ -496,6 +513,26 @@ const TOOLS = [
496
513
  ...confirmSchema,
497
514
  }, ['notificationId', 'ip', 'confirm', 'reason']),
498
515
  },
516
+ {
517
+ name: 'securenow_human_case_action_update',
518
+ title: 'Update Case-Level Human Action',
519
+ description: 'Approve, reject, execute, or fail a case-level proposed action such as tune_rule or create_exclusion. Write action; requires confirmation.',
520
+ scope: 'notifications:write',
521
+ readOnly: false,
522
+ confirm: true,
523
+ method: 'PUT',
524
+ endpoint: '/notifications/:notificationId/agent-case/actions/:actionKey',
525
+ pathParams: ['notificationId', 'actionKey'],
526
+ bodyFields: ['status', 'result'],
527
+ reasonInResult: true,
528
+ inputSchema: objectSchema({
529
+ notificationId: string('Notification id from the case-action row.'),
530
+ actionKey: string('Proposed action key from the row, for example tune_rule:...'),
531
+ status: string('New status: proposed, approved, rejected, executed, or failed.'),
532
+ result: { type: 'object', additionalProperties: true, description: 'Optional structured result/audit details.' },
533
+ ...confirmSchema,
534
+ }, ['notificationId', 'actionKey', 'status', 'confirm', 'reason']),
535
+ },
499
536
  {
500
537
  name: 'securenow_ip_lookup',
501
538
  title: 'IP Intelligence Lookup',
@@ -567,6 +604,258 @@ const TOOLS = [
567
604
  instanceId: string('Optional ClickHouse instance id.'),
568
605
  }),
569
606
  },
607
+ {
608
+ name: 'securenow_automation_rules_list',
609
+ title: 'List Automation Rules',
610
+ description: 'List blocklist automation rules with app/environment scope and stats.',
611
+ scope: 'automation:read',
612
+ readOnly: true,
613
+ method: 'GET',
614
+ endpoint: '/automation-rules',
615
+ inputSchema: objectSchema({}),
616
+ },
617
+ {
618
+ name: 'securenow_automation_rule_get',
619
+ title: 'Get Automation Rule',
620
+ description: 'Fetch one automation rule.',
621
+ scope: 'automation:read',
622
+ readOnly: true,
623
+ method: 'GET',
624
+ endpoint: '/automation-rules/:id',
625
+ pathParams: ['id'],
626
+ inputSchema: objectSchema({
627
+ id: string('Automation rule id.'),
628
+ }, ['id']),
629
+ },
630
+ {
631
+ name: 'securenow_automation_rule_create',
632
+ title: 'Create Automation Rule',
633
+ description: 'Create a blocklist automation rule. Write action; requires confirmation.',
634
+ scope: 'automation:write',
635
+ readOnly: false,
636
+ confirm: true,
637
+ method: 'POST',
638
+ endpoint: '/automation-rules',
639
+ bodyFields: ['name', 'description', 'conditions', 'conditionLogic', 'actions', 'applicationsAll', 'applicationKeys', 'environmentsAll', 'environments'],
640
+ inputSchema: objectSchema({
641
+ name: string('Rule name.'),
642
+ description: string('Optional rule description.'),
643
+ conditions: { type: 'array', items: { type: 'object', additionalProperties: true }, description: 'Condition array. Fields include abuseConfidenceScore, riskScore, alertName, alertTag, attackType, path, environment.' },
644
+ conditionLogic: string('AND or OR.'),
645
+ actions: { type: 'array', items: { type: 'object', additionalProperties: true }, description: 'Action array, for example [{ "type":"addToBlocklist", "config":{ "reason":"...", "ttlHours":24 }}].' },
646
+ applicationsAll: boolean('Apply to all applications.'),
647
+ applicationKeys: arrayOfStrings('Application keys when not applying to all applications.'),
648
+ environmentsAll: boolean('Apply to all environments.'),
649
+ environments: arrayOfStrings('Deployment environments when not applying to all environments.'),
650
+ ...confirmSchema,
651
+ }, ['name', 'conditions', 'actions', 'confirm', 'reason']),
652
+ },
653
+ {
654
+ name: 'securenow_automation_rule_update',
655
+ title: 'Update Automation Rule',
656
+ description: 'Update a blocklist automation rule. Write action; requires confirmation.',
657
+ scope: 'automation:write',
658
+ readOnly: false,
659
+ confirm: true,
660
+ method: 'PUT',
661
+ endpoint: '/automation-rules/:id',
662
+ pathParams: ['id'],
663
+ bodyFields: ['name', 'description', 'conditions', 'conditionLogic', 'actions', 'status', 'applicationsAll', 'applicationKeys', 'environmentsAll', 'environments'],
664
+ inputSchema: objectSchema({
665
+ id: string('Automation rule id.'),
666
+ name: string('Rule name.'),
667
+ description: string('Optional rule description.'),
668
+ status: string('active or disabled.'),
669
+ conditions: { type: 'array', items: { type: 'object', additionalProperties: true }, description: 'Condition array.' },
670
+ conditionLogic: string('AND or OR.'),
671
+ actions: { type: 'array', items: { type: 'object', additionalProperties: true }, description: 'Action array.' },
672
+ applicationsAll: boolean('Apply to all applications.'),
673
+ applicationKeys: arrayOfStrings('Application keys when not applying to all applications.'),
674
+ environmentsAll: boolean('Apply to all environments.'),
675
+ environments: arrayOfStrings('Deployment environments when not applying to all environments.'),
676
+ ...confirmSchema,
677
+ }, ['id', 'confirm', 'reason']),
678
+ },
679
+ {
680
+ name: 'securenow_automation_rule_dry_run',
681
+ title: 'Dry-Run Automation Rule',
682
+ description: 'Preview automation matches without writing blocklist entries.',
683
+ scope: 'automation:read',
684
+ readOnly: true,
685
+ method: 'POST',
686
+ endpoint: '/automation-rules/:id/dry-run',
687
+ pathParams: ['id'],
688
+ bodyFields: ['limit', 'sampleLimit'],
689
+ inputSchema: objectSchema({
690
+ id: string('Automation rule id.'),
691
+ limit: number('Maximum notifications to scan.', { minimum: 1, maximum: 2000 }),
692
+ sampleLimit: number('Maximum sample matches to return.', { minimum: 1, maximum: 50 }),
693
+ }, ['id']),
694
+ },
695
+ {
696
+ name: 'securenow_automation_rule_execute',
697
+ title: 'Execute Automation Rule',
698
+ description: 'Execute an automation rule and add matching IPs to the blocklist. Write action; requires confirmation.',
699
+ scope: 'automation:write',
700
+ readOnly: false,
701
+ destructive: true,
702
+ confirm: true,
703
+ method: 'POST',
704
+ endpoint: '/automation-rules/:id/execute',
705
+ pathParams: ['id'],
706
+ inputSchema: objectSchema({
707
+ id: string('Automation rule id.'),
708
+ ...confirmSchema,
709
+ }, ['id', 'confirm', 'reason']),
710
+ },
711
+ {
712
+ name: 'securenow_automation_rule_delete',
713
+ title: 'Delete Automation Rule',
714
+ description: 'Delete an automation rule. Write action; requires confirmation.',
715
+ scope: 'automation:write',
716
+ readOnly: false,
717
+ destructive: true,
718
+ confirm: true,
719
+ method: 'DELETE',
720
+ endpoint: '/automation-rules/:id',
721
+ pathParams: ['id'],
722
+ inputSchema: objectSchema({
723
+ id: string('Automation rule id.'),
724
+ ...confirmSchema,
725
+ }, ['id', 'confirm', 'reason']),
726
+ },
727
+ {
728
+ name: 'securenow_alert_rules_list',
729
+ title: 'List Alert Rules',
730
+ description: 'List alert rules, including system rules and app scope.',
731
+ scope: 'alerts:read',
732
+ readOnly: true,
733
+ method: 'GET',
734
+ endpoint: '/alert-rules',
735
+ inputSchema: objectSchema({}),
736
+ },
737
+ {
738
+ name: 'securenow_alert_rule_get',
739
+ title: 'Get Alert Rule',
740
+ description: 'Fetch one alert rule.',
741
+ scope: 'alerts:read',
742
+ readOnly: true,
743
+ method: 'GET',
744
+ endpoint: '/alert-rules/:id',
745
+ pathParams: ['id'],
746
+ inputSchema: objectSchema({
747
+ id: string('Alert rule id.'),
748
+ }, ['id']),
749
+ },
750
+ {
751
+ name: 'securenow_alert_rule_update',
752
+ title: 'Update Alert Rule',
753
+ description: 'Update alert rule scope/status/schedule/throttle or record a review action. Write action; requires confirmation.',
754
+ scope: 'alerts:write',
755
+ readOnly: false,
756
+ confirm: true,
757
+ method: 'PUT',
758
+ endpoint: '/alert-rules/:id',
759
+ pathParams: ['id'],
760
+ bodyFields: ['name', 'description', 'status', 'applicationsAll', 'applications', 'schedule', 'throttle', 'alertChannelIds', 'reviewAction', 'reviewNote', 'reviewNotificationId'],
761
+ inputSchema: objectSchema({
762
+ id: string('Alert rule id.'),
763
+ name: string('Rule name for custom rules.'),
764
+ description: string('Rule description for custom rules.'),
765
+ status: string('Active, Disabled, or Paused.'),
766
+ applicationsAll: boolean('Scope rule to all applications.'),
767
+ applications: arrayOfStrings('Application keys when applicationsAll is false.'),
768
+ schedule: { type: 'object', additionalProperties: true, description: 'Schedule patch.' },
769
+ throttle: { type: 'object', additionalProperties: true, description: 'Throttle patch.' },
770
+ alertChannelIds: arrayOfStrings('Alert channel ids.'),
771
+ reviewAction: string('Rule review action, e.g. keep_active or saved_new_version.'),
772
+ reviewNote: string('Review note.'),
773
+ reviewNotificationId: string('Notification id tied to this review.'),
774
+ ...confirmSchema,
775
+ }, ['id', 'confirm', 'reason']),
776
+ },
777
+ {
778
+ name: 'securenow_alert_rule_test',
779
+ title: 'Test Alert Rule',
780
+ description: 'Start a live or dry-run alert rule test.',
781
+ scope: 'alerts:write',
782
+ readOnly: false,
783
+ confirm: true,
784
+ method: 'POST',
785
+ endpoint: '/alert-rules/:id/test',
786
+ pathParams: ['id'],
787
+ bodyFields: ['applicationKey', 'mode'],
788
+ inputSchema: objectSchema({
789
+ id: string('Alert rule id.'),
790
+ applicationKey: string('Application key to test.'),
791
+ mode: string('dry_run or live.'),
792
+ ...confirmSchema,
793
+ }, ['id', 'confirm', 'reason']),
794
+ },
795
+ {
796
+ name: 'securenow_alert_rule_test_result',
797
+ title: 'Get Alert Rule Test Result',
798
+ description: 'Poll alert rule test status and results.',
799
+ scope: 'alerts:read',
800
+ readOnly: true,
801
+ method: 'GET',
802
+ endpoint: '/alert-rules/:id/test/:testId',
803
+ pathParams: ['id', 'testId'],
804
+ inputSchema: objectSchema({
805
+ id: string('Alert rule id.'),
806
+ testId: string('Alert rule test id.'),
807
+ }, ['id', 'testId']),
808
+ },
809
+ {
810
+ name: 'securenow_alert_rule_exclusions_list',
811
+ title: 'List Alert Rule Exclusions',
812
+ description: 'List exclusions embedded on one alert rule.',
813
+ scope: 'alerts:read',
814
+ readOnly: true,
815
+ method: 'GET',
816
+ endpoint: '/alert-rules/:id/exclusions',
817
+ pathParams: ['id'],
818
+ inputSchema: objectSchema({
819
+ id: string('Alert rule id.'),
820
+ }, ['id']),
821
+ },
822
+ {
823
+ name: 'securenow_alert_rule_exclusion_add',
824
+ title: 'Add Alert Rule Exclusion',
825
+ description: 'Add a restrictive exclusion to one alert rule. Write action; requires confirmation.',
826
+ scope: 'alerts:write',
827
+ readOnly: false,
828
+ confirm: true,
829
+ method: 'POST',
830
+ endpoint: '/alert-rules/:id/exclusions',
831
+ pathParams: ['id'],
832
+ bodyFields: ['conditions', 'matchMode', 'reason', 'pathPattern', 'isActive'],
833
+ inputSchema: objectSchema({
834
+ id: string('Alert rule id.'),
835
+ conditions: { type: 'array', items: { type: 'object', additionalProperties: true }, description: 'Restrictive exclusion conditions.' },
836
+ matchMode: string('all or any.'),
837
+ reason: string('Exclusion reason.'),
838
+ pathPattern: string('Optional path pattern.'),
839
+ isActive: boolean('Whether the exclusion is active.'),
840
+ ...confirmSchema,
841
+ }, ['id', 'confirm', 'reason']),
842
+ },
843
+ {
844
+ name: 'securenow_alert_rule_exclusion_remove',
845
+ title: 'Remove Alert Rule Exclusion',
846
+ description: 'Remove an alert rule exclusion. Write action; requires confirmation.',
847
+ scope: 'alerts:write',
848
+ readOnly: false,
849
+ confirm: true,
850
+ method: 'DELETE',
851
+ endpoint: '/alert-rules/:id/exclusions/:exclusionId',
852
+ pathParams: ['id', 'exclusionId'],
853
+ inputSchema: objectSchema({
854
+ id: string('Alert rule id.'),
855
+ exclusionId: string('Exclusion id.'),
856
+ ...confirmSchema,
857
+ }, ['id', 'exclusionId', 'confirm', 'reason']),
858
+ },
570
859
  {
571
860
  name: 'securenow_blocklist_list',
572
861
  title: 'List Blocklist',
@@ -575,8 +864,12 @@ const TOOLS = [
575
864
  readOnly: true,
576
865
  method: 'GET',
577
866
  endpoint: '/blocklist',
578
- queryFields: ['page', 'limit'],
579
- inputSchema: objectSchema({ ...pagingInput }),
867
+ queryFields: ['page', 'limit', 'appKey', 'environment'],
868
+ inputSchema: objectSchema({
869
+ ...pagingInput,
870
+ appKey: string('Optional application key scope.'),
871
+ ...environmentInput,
872
+ }),
580
873
  },
581
874
  {
582
875
  name: 'securenow_blocklist_add',
@@ -587,12 +880,14 @@ const TOOLS = [
587
880
  confirm: true,
588
881
  method: 'POST',
589
882
  endpoint: '/blocklist',
590
- bodyFields: ['ip', 'reason', 'expiresAt', 'metadata'],
883
+ bodyFields: ['ip', 'reason', 'expiresAt', 'metadata', 'appKey', 'environment'],
591
884
  inputSchema: objectSchema({
592
885
  ip: string('IPv4 address or CIDR.'),
593
886
  reason: string('Reason for blocking.'),
594
887
  expiresAt: string('Optional expiry time as ISO 8601.'),
595
888
  metadata: { type: 'object', additionalProperties: true, description: 'Optional metadata.' },
889
+ appKey: string('Optional application key to scope this block. Omit for all apps.'),
890
+ ...environmentInput,
596
891
  ...confirmSchema,
597
892
  }, ['ip', 'confirm', 'reason']),
598
893
  },
@@ -629,8 +924,12 @@ const TOOLS = [
629
924
  readOnly: true,
630
925
  method: 'GET',
631
926
  endpoint: '/allowlist',
632
- queryFields: ['page', 'limit'],
633
- inputSchema: objectSchema({ ...pagingInput }),
927
+ queryFields: ['page', 'limit', 'appKey', 'environment'],
928
+ inputSchema: objectSchema({
929
+ ...pagingInput,
930
+ appKey: string('Optional application key scope.'),
931
+ ...environmentInput,
932
+ }),
634
933
  },
635
934
  {
636
935
  name: 'securenow_allowlist_add',
@@ -641,7 +940,7 @@ const TOOLS = [
641
940
  confirm: true,
642
941
  method: 'POST',
643
942
  endpoint: '/allowlist',
644
- bodyFields: ['ip', 'label', 'reason', 'expiresAt', 'applicationsAll', 'applicationKeys'],
943
+ bodyFields: ['ip', 'label', 'reason', 'expiresAt', 'applicationsAll', 'applicationKeys', 'environment'],
645
944
  inputSchema: objectSchema({
646
945
  ip: string('IPv4 address or CIDR.'),
647
946
  label: string('Human-readable label.'),
@@ -649,6 +948,7 @@ const TOOLS = [
649
948
  expiresAt: string('Optional expiry time as ISO 8601.'),
650
949
  applicationsAll: boolean('Apply to all applications.'),
651
950
  applicationKeys: arrayOfStrings('Application keys to scope this allowlist entry to.'),
951
+ ...environmentInput,
652
952
  ...confirmSchema,
653
953
  }, ['ip', 'confirm', 'reason']),
654
954
  },
@@ -675,7 +975,11 @@ const TOOLS = [
675
975
  readOnly: true,
676
976
  method: 'GET',
677
977
  endpoint: '/trusted-ips',
678
- inputSchema: objectSchema({}),
978
+ queryFields: ['appKey', 'environment'],
979
+ inputSchema: objectSchema({
980
+ appKey: string('Optional application key scope.'),
981
+ ...environmentInput,
982
+ }),
679
983
  },
680
984
  {
681
985
  name: 'securenow_trusted_add',
@@ -686,13 +990,14 @@ const TOOLS = [
686
990
  confirm: true,
687
991
  method: 'POST',
688
992
  endpoint: '/trusted-ips',
689
- bodyFields: ['ip', 'label', 'note', 'applicationsAll', 'applicationKeys'],
993
+ bodyFields: ['ip', 'label', 'note', 'applicationsAll', 'applicationKeys', 'environment'],
690
994
  inputSchema: objectSchema({
691
995
  ip: string('IPv4 address or CIDR.'),
692
996
  label: string('Human-readable label.'),
693
997
  note: string('Optional note.'),
694
998
  applicationsAll: boolean('Apply to all applications.'),
695
999
  applicationKeys: arrayOfStrings('Application keys to scope this trusted IP to.'),
1000
+ ...environmentInput,
696
1001
  ...confirmSchema,
697
1002
  }, ['ip', 'confirm', 'reason']),
698
1003
  },
@@ -883,10 +1188,11 @@ function promptMessages(name, args = {}) {
883
1188
  `Fetch page=${page}, limit=${limit} with securenow_human_actions_list, select row ${rowNumber}, then call securenow_notifications_get and securenow_human_action_report for that notificationId and IP.`,
884
1189
  'Read the AI report, finalDecision, DAG steps, findings, proofs, metadata paths/user agents/status codes, and trace IDs.',
885
1190
  'Open trace evidence with securenow_traces_show and correlated logs with securenow_logs_for_trace when trace IDs are available.',
886
- 'Return one of only two outcomes: Block IP or False Positive. If evidence is ambiguous, stop and explain what is missing.',
1191
+ 'Return one clear outcome: Block IP, False Positive, Rule Tuning Needed, or Ambiguous. If evidence is ambiguous, stop and explain what is missing.',
887
1192
  confirmWrites
888
1193
  ? 'The user requested execution. If evidence supports the decision, call securenow_human_action_block or securenow_human_action_false_positive with confirm:true and a precise reason.'
889
1194
  : 'Do not execute write tools yet. Prepare the recommended decision and exact tool call the user can approve.',
1195
+ 'If many IPs share the same benign path/status/user-agent pattern, recommend tightening the alert rule with a precise guard instead of reviewing each IP as malicious.',
890
1196
  'False positives must be narrow: app + alert rule + path + method/status/user-agent/body evidence where possible. Never globally trust an IP by default.',
891
1197
  ].join('\n'),
892
1198
  },
@@ -906,12 +1212,12 @@ function promptMessages(name, args = {}) {
906
1212
  'Work my SecureNow Requires Human queue like a senior security analyst using the MCP tools.',
907
1213
  `Review up to ${limit} row(s), most urgent first.${args.search ? ` Search filter: ${args.search}.` : ''}`,
908
1214
  'Start with securenow_human_actions_list. For each row, call securenow_notifications_get and securenow_human_action_report, inspect the AI report/DAG/proofs/trace IDs, and fetch trace/log evidence where useful.',
909
- 'For each row choose exactly one outcome: Block IP, False Positive, or Skip because evidence is insufficient. Explain skipped rows.',
1215
+ 'For each row choose exactly one outcome: Block IP, False Positive, Rule Tuning Needed, or Skip because evidence is insufficient. Explain skipped rows.',
910
1216
  confirmWrites
911
1217
  ? 'The user requested execution. For supported decisions, call the correct write tool with confirm:true and a precise reason, then continue.'
912
1218
  : 'Do not execute write tools yet. Produce a row-by-row action plan and exact MCP write calls for user approval.',
913
- 'For block decisions, use securenow_human_action_block. For false positives, use securenow_human_action_false_positive with restrictive conditions. Avoid broad/global trust.',
914
- 'End with counts: handled, proposed block, proposed false positive, skipped, still waiting.',
1219
+ 'For block decisions, use securenow_human_action_block. For false positives, use securenow_human_action_false_positive with restrictive conditions. For case-level tune_rule/create_exclusion rows, inspect securenow_notifications_get and then use securenow_human_case_action_update only when the action is safe to approve/reject.',
1220
+ 'End with counts: handled, proposed block, proposed false positive, rule tuning needed, skipped, still waiting.',
915
1221
  ].join('\n'),
916
1222
  },
917
1223
  },
@@ -991,6 +1297,12 @@ function buildApiRequest(tool, rawArgs = {}) {
991
1297
  if (tool.reasonAsNote && !body.note && args.reason) {
992
1298
  body.note = args.reason;
993
1299
  }
1300
+ if (tool.reasonInResult && args.reason) {
1301
+ body.result = {
1302
+ ...(body.result && typeof body.result === 'object' ? body.result : {}),
1303
+ reason: args.reason,
1304
+ };
1305
+ }
994
1306
 
995
1307
  return {
996
1308
  method: tool.method,
package/nextjs.js CHANGED
@@ -27,6 +27,7 @@
27
27
 
28
28
  const { randomUUID } = require('crypto');
29
29
  const appConfig = require('./app-config');
30
+ const { resolveClientIpWithDetails } = require('./resolve-ip');
30
31
  const otelResources = require('@opentelemetry/resources');
31
32
 
32
33
  const env = appConfig.env;
@@ -203,26 +204,11 @@ function registerSecureNow(options = {}) {
203
204
  const headers = request.headers || {};
204
205
 
205
206
  // ======== IP ADDRESS CAPTURE ========
206
- // Try different header sources for IP (priority order)
207
- const forwardedFor = headers['x-forwarded-for'];
208
- const realIp = headers['x-real-ip'];
209
- const cfConnectingIp = headers['cf-connecting-ip']; // Cloudflare
210
- const clientIp = headers['x-client-ip'];
211
- const socketIp = request.socket?.remoteAddress;
212
-
213
- const PRIVATE_RE = /^(127\.|::1$|::ffff:127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|f[cd][0-9a-f]{2}:)/;
214
- const isProxied = socketIp && PRIVATE_RE.test(socketIp);
215
- let primaryIp = socketIp || 'unknown';
216
- if (isProxied) {
217
- if (forwardedFor) {
218
- const chain = forwardedFor.split(',').map(s => s.trim()).filter(Boolean);
219
- for (let i = chain.length - 1; i >= 0; i--) {
220
- if (!PRIVATE_RE.test(chain[i])) { primaryIp = chain[i]; break; }
221
- }
222
- } else {
223
- primaryIp = realIp || cfConnectingIp || clientIp || primaryIp;
224
- }
225
- }
207
+ const ipDetails = resolveClientIpWithDetails(request);
208
+ const forwardedFor = ipDetails.forwardedFor;
209
+ const realIp = ipDetails.realIp;
210
+ const socketIp = ipDetails.socketIp;
211
+ const primaryIp = ipDetails.ip || 'unknown';
226
212
 
227
213
  // ======== PROTOCOL & CONNECTION ========
228
214
  const scheme = headers['x-forwarded-proto'] ||
@@ -249,9 +235,16 @@ function registerSecureNow(options = {}) {
249
235
  const attributes = {
250
236
  // IP & Network
251
237
  'http.client_ip': primaryIp,
238
+ 'http.client_ip.source': ipDetails.source,
252
239
  'http.forwarded_for': forwardedFor || '',
253
240
  'http.real_ip': realIp || '',
254
241
  'http.socket_ip': socketIp || '',
242
+ 'http.proxy.trusted': String(!!ipDetails.trustedProxy),
243
+ 'http.request.header.x_forwarded_for': forwardedFor || '',
244
+ 'http.request.header.x_real_ip': realIp || '',
245
+ 'http.request.header.cf_connecting_ip': ipDetails.cfConnectingIp || '',
246
+ 'http.request.header.true_client_ip': ipDetails.trueClientIp || '',
247
+ 'http.request.header.x_client_ip': ipDetails.clientIp || '',
255
248
 
256
249
  // Protocol & Host
257
250
  'http.scheme': scheme,
@@ -327,7 +320,7 @@ function registerSecureNow(options = {}) {
327
320
  if (env('NODE_ENV') === 'development' || env('OTEL_LOG_LEVEL') === 'debug') {
328
321
  console.log('[securenow] 📡 Captured IP: %s (from: %s)',
329
322
  primaryIp,
330
- forwardedFor ? 'x-forwarded-for' : realIp ? 'x-real-ip' : socketIp ? 'socket' : 'unknown'
323
+ ipDetails.source || 'unknown'
331
324
  );
332
325
  }
333
326
 
@@ -520,7 +513,8 @@ function registerSecureNow(options = {}) {
520
513
  const reqSpanCtx = otelTrace.getSpanContext(reqCtx);
521
514
  const duration = Date.now() - start;
522
515
  const status = res.statusCode;
523
- const ip = req.headers['x-forwarded-for'] || req.headers['x-real-ip'] || req.socket?.remoteAddress || '-';
516
+ const ipDetails = resolveClientIpWithDetails(req);
517
+ const ip = ipDetails.ip || '-';
524
518
  const ua = req.headers['user-agent'] || '-';
525
519
  const body = `${method} ${url} ${status} ${duration}ms ip=${ip} ua=${ua}`;
526
520
  const severity = status >= 500 ? SeverityNumber.ERROR : status >= 400 ? SeverityNumber.WARN : SeverityNumber.INFO;
@@ -536,7 +530,12 @@ function registerSecureNow(options = {}) {
536
530
  'http.url': url,
537
531
  'http.status_code': status,
538
532
  'http.duration_ms': duration,
539
- 'http.client_ip': String(ip).split(',')[0].trim(),
533
+ 'http.client_ip': ip,
534
+ 'http.client_ip.source': ipDetails.source || 'unknown',
535
+ 'http.socket_ip': ipDetails.socketIp || '',
536
+ 'http.forwarded_for': ipDetails.forwardedFor || '',
537
+ 'http.real_ip': ipDetails.realIp || '',
538
+ 'http.proxy.trusted': String(!!ipDetails.trustedProxy),
540
539
  'http.user_agent': ua,
541
540
  },
542
541
  ...(reqSpanCtx && { context: reqCtx }),
@@ -22,6 +22,7 @@ import { randomUUID } from 'node:crypto';
22
22
 
23
23
  const nodeRequire = createRequire(import.meta.url);
24
24
  const appConfig = nodeRequire('./app-config');
25
+ const { resolveClientIpWithDetails } = nodeRequire('./resolve-ip');
25
26
 
26
27
  // ── Helpers ──
27
28
 
@@ -137,17 +138,21 @@ export default defineNitroPlugin(async (nitroApp) => {
137
138
  requestHook: (span, request) => {
138
139
  try {
139
140
  const hdrs = request.headers || {};
140
- const fwd = hdrs['x-forwarded-for'];
141
- const clientIp =
142
- (fwd ? String(fwd).split(',')[0].trim() : null) ||
143
- hdrs['x-real-ip'] ||
144
- hdrs['cf-connecting-ip'] ||
145
- hdrs['x-client-ip'] ||
146
- request.socket?.remoteAddress ||
147
- 'unknown';
141
+ const ipDetails = resolveClientIpWithDetails(request);
142
+ const clientIp = ipDetails.ip || 'unknown';
148
143
 
149
144
  span.setAttributes({
150
145
  'http.client_ip': clientIp,
146
+ 'http.client_ip.source': ipDetails.source,
147
+ 'http.socket_ip': ipDetails.socketIp || '',
148
+ 'http.forwarded_for': ipDetails.forwardedFor || '',
149
+ 'http.real_ip': ipDetails.realIp || '',
150
+ 'http.proxy.trusted': String(!!ipDetails.trustedProxy),
151
+ 'http.request.header.x_forwarded_for': ipDetails.forwardedFor || '',
152
+ 'http.request.header.x_real_ip': ipDetails.realIp || '',
153
+ 'http.request.header.cf_connecting_ip': ipDetails.cfConnectingIp || '',
154
+ 'http.request.header.true_client_ip': ipDetails.trueClientIp || '',
155
+ 'http.request.header.x_client_ip': ipDetails.clientIp || '',
151
156
  'http.user_agent': hdrs['user-agent'] || '',
152
157
  'http.host': hdrs['x-forwarded-host'] || hdrs['host'] || '',
153
158
  'http.scheme':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securenow",
3
- "version": "7.6.9",
3
+ "version": "7.7.0",
4
4
  "description": "OpenTelemetry instrumentation for Node.js, Next.js, and Nuxt - Send traces and logs to any OTLP-compatible backend",
5
5
  "type": "commonjs",
6
6
  "main": "register.js",