Repository: grafana/grafana-zabbix

Issue:
More info about acknowledges from zabbix
Hello, we are looking for more information from Zabbix about acknowledge actions. Currenty, only comments are imported, so if I do any "update" action in zabbix without comment (acknowledge, unacknowledge, change severity, etc.), no information about "what was done" is shown in Grafana, only user name.

Example:
What I see in zabbix:
<img width="512" height="156" alt="Image" src="https://github.com/user-attachments/assets/80f43a57-16a7-48e5-b389-6af830fd4540" />
What I see in Grafana (I also notice there is a problem in formatting/line ordering, when there is no comment, but this should be solved by this request):
<img width="397" height="107" alt="Image" src="https://github.com/user-attachments/assets/a0d96736-4a53-4423-b11a-b4cda3e586d9" />

My idea is to add description of action at the beginning of imported comment (inside brackets). Most important for me is info about acknowledge / unacknowlegde, but I expect other action should be done the same way.

Example (based on data from previous) of what I want to see:
<img width="450" height="111" alt="Image" src="https://github.com/user-attachments/assets/132a2c97-e2a7-4b4e-83ba-d719807ded80" />


Discussion:
@HH-Harry: One more note - what I added in sceenshot "what I want" was done by simple editing the "module.js" file in "panel-triggers" subfolder of my installed plugin (I am no react developer, so I did what I can :) ).
I simply replaced:
```createElement("span",{key:e.acknowledgeid,className:"problem-ack-message"},e.message)```
with:
```createElement("span",{key:e.acknowledgeid,className:"problem-ack-message"},function(e){return ((e.action & 2) !== 0)?`(Acknowledged)${e.message}`:(((e.action & 16) !== 0)?`(Unacknowledged)${e.message}`:e.message)}(e))```

From looking on the source code here, I expect, I need to edit "src/panel-triggers/components/Problems/AcknowledgesList.tsx" by adding new function "formatAckMessage" and calling it similar way like already existing "formatUserName", but I have no idea how to compile it.

Expected example of target new function "formatAckMessage" (to make processing this enhancement easier):
```
function formatAckMessage(ack: ZBXAcknowledge): string 
{
  if ((ack.action & 2) !== 0) {
    return `(Acknoledged) ${ack.message}`.trim();
  } else if ((ack.action & 16) !== 0) {
    return `(Unacknoledged) ${ack.message}`.trim();
  } else {
    return ack.message;
  }
}
```
Processing more action, when more that one could be done at same time (like acknowledge, severity change and temporarily supress), will be more complex, I am afraid.@zoltanbedi: Hey @HH-Harry, thanks for raising this issue for us. We are accepting PRs, feel free to open one with your proposed changes. I think those make sense.@HH-Harry: Thanks for quick answer @zoltanbedi , but as I write in the text, I am not React developer, so I am not sure with the proposed changes.
That is why I created this ticket and not a PR and why I would like someone else, more experienced, to do the PR based on this ticket.@HH-Harry: In the end, I tried :) PR created, I am just not sure if it is correct, so rest on you.@zoltanbedi: Fixed in #2071

Solution patch:
diff --git a/.changeset/plain-boxes-carry.md b/.changeset/plain-boxes-carry.md
new file mode 100644
index 0000000..3f848e4
--- /dev/null
+++ b/.changeset/plain-boxes-carry.md
@@ -0,0 +1,5 @@
+---
+'grafana-zabbix': minor
+---
+
+Feat: Show details about acknowledge actions
diff --git a/.changeset/polite-cobras-grin.md b/.changeset/polite-cobras-grin.md
new file mode 100644
index 0000000..a6e7d00
--- /dev/null
+++ b/.changeset/polite-cobras-grin.md
@@ -0,0 +1,5 @@
+---
+'grafana-zabbix': patch
+---
+
+Fix CVE-2025-10630 by implementing regex scanner for exploits and adding timeout to compilation
diff --git a/pkg/zabbix/utils.go b/pkg/zabbix/utils.go
index 8bea620..6afa87e 100644
--- a/pkg/zabbix/utils.go
+++ b/pkg/zabbix/utils.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"regexp"
 	"strings"
+	"time"
 
 	"github.com/dlclark/regexp2"
 	"github.com/grafana/grafana-plugin-sdk-go/backend"
@@ -65,6 +66,78 @@ func splitKeyParams(paramStr string) []string {
 	return params
 }
 
+// isPathologicalRegex detects potentially dangerous regex patterns that could cause ReDoS
+func isPathologicalRegex(pattern string) bool {
+	// Check for consecutive quantifiers
+	consecutiveQuantifiers := []string{`\*\*`, `\+\+`, `\*\+`, `\+\*`}
+	for _, q := range consecutiveQuantifiers {
+		if matched, _ := regexp.MatchString(q, pattern); matched {
+			return true
+		}
+	}
+	
+	// Check for nested quantifiers
+	nestedQuantifiers := []string{
+		`\([^)]*\+[^)]*\)\+`,     // (a+)+
+		`\([^)]*\*[^)]*\)\*`,     // (a*)*
+		`\([^)]*\+[^)]*\)\*`,     // (a+)*
+		`\([^)]*\*[^)]*\)\+`,     // (a*)+
+	}
+	for _, nested := range nestedQuantifiers {
+		if matched, _ := regexp.MatchString(nested, pattern); matched {
+			return true
+		}
+	}
+	
+	// Check for specific catastrophic patterns
+	catastrophicPatterns := []string{
+		`\(\.\*\)\*`,             // (.*)* 
+		`\(\.\+\)\+`,             // (.+)+
+		`\(\.\*\)\+`,             // (.*)+
+		`\(\.\+\)\*`,             // (.+)*
+	}
+	for _, catastrophic := range catastrophicPatterns {
+		if matched, _ := regexp.MatchString(catastrophic, pattern); matched {
+			return true
+		}
+	}
+	
+	// Check for obvious overlapping alternation (manual check for exact duplicates)
+	if strings.Contains(pattern, "(a|a)") || 
+	   strings.Contains(pattern, "(1|1)") || 
+	   strings.Contains(pattern, "(.*|.*)") {
+		return true
+	}
+	
+	return false
+}
+
+// safeRegexpCompile compiles a regex with timeout protection
+func safeRegexpCompile(pattern string) (*regexp2.Regexp, error) {
+	// Channel to receive compilation result
+	resultCh := make(chan struct {
+		regex *regexp2.Regexp
+		err   error
+	}, 1)
+	
+	// Compile regex in goroutine with timeout
+	go func() {
+		regex, err := regexp2.Compile(pattern, regexp2.RE2)
+		resultCh <- struct {
+			regex *regexp2.Regexp
+			err   error
+		}{regex, err}
+	}()
+	
+	// Wait for compilation or timeout
+	select {
+	case result := <-resultCh:
+		return result.regex, result.err
+	case <-time.After(5 * time.Second):
+		return nil, fmt.Errorf("regex compilation timeout (5s) - pattern may be too complex")
+	}
+}
+
 func parseFilter(filter string) (*regexp2.Regexp, error) {
 	vaildREModifiers := "imncsxrde"
 	regex := regexp.MustCompile(`^/(.+)/([imncsxrde]*)$`)
@@ -75,17 +148,35 @@ func parseFilter(filter string) (*regexp2.Regexp, error) {
 		return nil, nil
 	}
 
+	regexPattern := matches[1]
+	
+	// Security: Check for pathological regex patterns
+	if isPathologicalRegex(regexPattern) {
+		return nil, backend.DownstreamErrorf("error parsing regexp: potentially dangerous regex pattern detected")
+	}
+
+	// Security: Limit regex pattern length
+	if len(regexPattern) > 1000 {
+		return nil, backend.DownstreamErrorf("error parsing regexp: pattern too long (max 1000 characters)")
+	}
+
 	pattern := ""
 	if matches[2] != "" {
 		if flagRE.MatchString(matches[2]) {
 			pattern += "(?" + matches[2] + ")"
 		} else {
-			return nil, backend.DownstreamError(fmt.Errorf("error parsing regexp: unsupported flags `%s` (expected [%s])", matches[2], vaildREModifiers))
+			return nil, backend.DownstreamErrorf("error parsing regexp: unsupported flags `%s` (expected [%s])", matches[2], vaildREModifiers)
 		}
 	}
-	pattern += matches[1]
+	pattern += regexPattern
+
+	// Security: Test compilation with timeout
+	compiled, err := safeRegexpCompile(pattern)
+	if err != nil {
+		return nil, backend.DownstreamErrorf("error parsing regexp: %v", err)
+	}
 
-	return regexp2.Compile(pattern, regexp2.RE2)
+	return compiled, nil
 }
 
 func isRegex(filter string) bool {
diff --git a/src/panel-triggers/components/Problems/AcknowledgesList.tsx b/src/panel-triggers/components/Problems/AcknowledgesList.tsx
index 1c75fb1..01065fb 100644
--- a/src/panel-triggers/components/Problems/AcknowledgesList.tsx
+++ b/src/panel-triggers/components/Problems/AcknowledgesList.tsx
@@ -26,7 +26,7 @@ export default function AcknowledgesList(props: AcknowledgesListProps) {
       <div className="problem-ack-col problem-ack-message">
         {acknowledges.map((ack) => (
           <span key={ack.acknowledgeid} className="problem-ack-message">
-            {ack.message}
+            {formatAckMessage(ack)}
           </span>
         ))}
       </div>
@@ -41,3 +41,27 @@ function formatUserName(ack: ZBXAcknowledge): string {
     return `${ack.name} ${ack.surname}`.trim();
   }
 }
+
+function formatAckMessage(ack: ZBXAcknowledge): string {
+  let msg = '';
+  let action = parseInt(ack.action, 10);
+
+  if ((action & 2) !== 0) {
+    msg = msg + '(Acknowledged) ';
+  } else if ((action & 16) !== 0) {
+    msg = msg + '(Unacknowledged) ';
+  }
+
+  if ((action & 32) !== 0) {
+    msg = msg + '(Suppressed) ';
+  } else if ((action & 64) !== 0) {
+    msg = msg + '(Unsuppressed) ';
+  }
+
+  if ((action & 8) !== 0) {
+    msg = msg + '(Changed severity) ';
+  }
+
+  msg = msg + ack.message;
+  return msg.trim();
+}



Test patch:
diff --git a/pkg/zabbix/utils_test.go b/pkg/zabbix/utils_test.go
index d750a13..02e6f37 100644
--- a/pkg/zabbix/utils_test.go
+++ b/pkg/zabbix/utils_test.go
@@ -2,6 +2,7 @@ package zabbix
 
 import (
 	"reflect"
+	"strings"
 	"testing"
 
 	"github.com/dlclark/regexp2"
@@ -95,6 +96,48 @@ func TestParseFilter(t *testing.T) {
 			expectNoError: false,
 			expectedError: "",
 		},
+		{
+			name:          "Pathological regex - nested quantifiers (a+)+",
+			filter:        "/^(a+)+$/",
+			want:          nil,
+			expectNoError: false,
+			expectedError: "error parsing regexp: potentially dangerous regex pattern detected",
+		},
+		{
+			name:          "Pathological regex - (.*)* pattern",
+			filter:        "/(.*)*/",
+			want:          nil,
+			expectNoError: false,
+			expectedError: "error parsing regexp: potentially dangerous regex pattern detected",
+		},
+		{
+			name:          "Pathological regex - overlapping alternation",
+			filter:        "/(a|a)*/",
+			want:          nil,
+			expectNoError: false,
+			expectedError: "error parsing regexp: potentially dangerous regex pattern detected",
+		},
+		{
+			name:          "Pathological regex - consecutive quantifiers",
+			filter:        "/a**/",
+			want:          nil,
+			expectNoError: false,
+			expectedError: "error parsing regexp: potentially dangerous regex pattern detected",
+		},
+		{
+			name:          "Pattern too long",
+			filter:        "/" + strings.Repeat("a", 1001) + "/",
+			want:          nil,
+			expectNoError: false,
+			expectedError: "error parsing regexp: pattern too long (max 1000 characters)",
+		},
+		{
+			name:          "Safe complex regex",
+			filter:        "/^[a-zA-Z0-9_-]+\\.[a-zA-Z]{2,}$/",
+			want:          regexp2.MustCompile("^[a-zA-Z0-9_-]+\\.[a-zA-Z]{2,}$", regexp2.RE2),
+			expectNoError: true,
+			expectedError: "",
+		},
 	}
 
 	for _, tt := range tests {
@@ -113,3 +156,69 @@ func TestParseFilter(t *testing.T) {
 		})
 	}
 }
+
+func TestIsPathologicalRegex(t *testing.T) {
+	tests := []struct {
+		name     string
+		pattern  string
+		expected bool
+	}{
+		{
+			name:     "Safe pattern",
+			pattern:  "^[a-zA-Z0-9]+$",
+			expected: false,
+		},
+		{
+			name:     "Nested quantifiers (a+)+",
+			pattern:  "^(a+)+$",
+			expected: true,
+		},
+		{
+			name:     "Nested quantifiers (a*)*",
+			pattern:  "(a*)*",
+			expected: true,
+		},
+		{
+			name:     "Overlapping alternation (a|a)*",
+			pattern:  "(a|a)*",
+			expected: true,
+		},
+		{
+			name:     "Consecutive quantifiers **",
+			pattern:  "a**",
+			expected: true,
+		},
+		{
+			name:     "Consecutive quantifiers ++",
+			pattern:  "a++",
+			expected: true,
+		},
+		{
+			name:     "Catastrophic (.*)* pattern",
+			pattern:  "(.*)*",
+			expected: true,
+		},
+		{
+			name:     "Catastrophic (.+)+ pattern",
+			pattern:  "(.+)+",
+			expected: true,
+		},
+		{
+			name:     "Safe alternation with different patterns",
+			pattern:  "(cat|dog)*",
+			expected: false,
+		},
+		{
+			name:     "Safe quantifier usage",
+			pattern:  "[0-9]+\\.[0-9]*",
+			expected: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := isPathologicalRegex(tt.pattern)
+			assert.Equal(t, tt.expected, result, "Pattern: %s", tt.pattern)
+		})
+	}
+}