mirror of
https://github.com/dromara/hertzbeat.git
synced 2026-09-17 18:19:02 +00:00
Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60a3db2702 | ||
|
|
dc8ae844c1 | ||
|
|
91a7593b87 | ||
|
|
4442a52adf | ||
|
|
3ce2838bfc | ||
|
|
60e3437e82 | ||
|
|
7e508fd4b6 | ||
|
|
014eb53357 | ||
|
|
b0e6a32fb5 | ||
|
|
3257652640 | ||
|
|
21d6ec2f1b | ||
|
|
1cb0d41943 | ||
|
|
bee9dfc44b | ||
|
|
3760bd713e | ||
|
|
4f6f5567f6 |
+12
-1
@@ -31,6 +31,8 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Send alarm information through Server
|
||||
*/
|
||||
@@ -54,7 +56,16 @@ public class ServerChanAlertNotifyHandlerImpl extends AbstractAlertNotifyHandler
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
HttpEntity<ServerChanAlertNotifyHandlerImpl.ServerChanWebHookDto> httpEntity = new HttpEntity<>(serverChanWebHookDto, headers);
|
||||
String webHookUrl = String.format(alerterProperties.getServerChanWebhookUrl(), receiver.getServerChanToken());
|
||||
String sanitizedToken = receiver.getServerChanToken().replaceAll("[^a-zA-Z0-9_-]", "");
|
||||
String webHookUrl = String.format(alerterProperties.getServerChanWebhookUrl(), sanitizedToken);
|
||||
|
||||
// Validate the constructed URL against a whitelist
|
||||
List<String> allowedBaseUrls = List.of("https://api.serverchan.com", "https://serverchan.example.com");
|
||||
boolean isValidUrl = allowedBaseUrls.stream().anyMatch(webHookUrl::startsWith);
|
||||
if (!isValidUrl) {
|
||||
throw new AlertNoticeException("Invalid webhook URL: " + webHookUrl);
|
||||
}
|
||||
|
||||
ResponseEntity<CommonRobotNotifyResp> responseEntity = restTemplate.postForEntity(webHookUrl,
|
||||
httpEntity, CommonRobotNotifyResp.class);
|
||||
if (responseEntity.getStatusCode() == HttpStatus.OK) {
|
||||
|
||||
+24
-1
@@ -17,6 +17,7 @@
|
||||
|
||||
package org.apache.hertzbeat.alert.notice.impl;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Objects;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
@@ -52,7 +53,12 @@ final class SlackAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
HttpEntity<SlackNotifyDTO> slackNotifyEntity = new HttpEntity<>(slackNotify, headers);
|
||||
var entity = restTemplate.postForEntity(receiver.getSlackWebHookUrl(), slackNotifyEntity, String.class);
|
||||
String slackWebHookUrl = receiver.getSlackWebHookUrl();
|
||||
if (!isValidSlackWebHookUrl(slackWebHookUrl)) {
|
||||
log.warn("Invalid Slack Webhook URL: {}", slackWebHookUrl);
|
||||
throw new AlertNoticeException("Invalid Slack Webhook URL");
|
||||
}
|
||||
var entity = restTemplate.postForEntity(slackWebHookUrl, slackNotifyEntity, String.class);
|
||||
if (entity.getStatusCode() == HttpStatus.OK && entity.getBody() != null) {
|
||||
var body = entity.getBody();
|
||||
if (Objects.equals(SUCCESS, body)) {
|
||||
@@ -81,4 +87,21 @@ final class SlackAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl {
|
||||
private String text;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Validate if the Slack Webhook URL belongs to an allowed domain.
|
||||
*
|
||||
* @param url the Slack Webhook URL to validate
|
||||
* @return true if the URL is valid, false otherwise
|
||||
*/
|
||||
private boolean isValidSlackWebHookUrl(String url) {
|
||||
try {
|
||||
URI uri = new URI(url);
|
||||
String host = uri.getHost();
|
||||
return "hooks.slack.com".equals(host);
|
||||
} catch (Exception e) {
|
||||
log.warn("Error validating Slack Webhook URL: {}", url, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
-3
@@ -42,9 +42,14 @@ import org.springframework.stereotype.Component;
|
||||
final class TelegramBotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl {
|
||||
|
||||
@Override
|
||||
public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) throws AlertNoticeException {
|
||||
public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert)
|
||||
throws AlertNoticeException {
|
||||
try {
|
||||
String url = String.format(alerterProperties.getTelegramWebhookUrl(), receiver.getTgBotToken());
|
||||
String token = receiver.getTgBotToken();
|
||||
if (!isValidTelegramToken(token)) {
|
||||
throw new AlertNoticeException("Invalid Telegram Bot Token");
|
||||
}
|
||||
String url = String.format(alerterProperties.getTelegramWebhookUrl(), token);
|
||||
TelegramBotNotifyDTO notifyBody = TelegramBotNotifyDTO.builder()
|
||||
.chatId(receiver.getTgUserId())
|
||||
.text(renderContent(noticeTemplate, alert))
|
||||
@@ -54,7 +59,8 @@ final class TelegramBotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandler
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
HttpEntity<TelegramBotNotifyDTO> telegramEntity = new HttpEntity<>(notifyBody, headers);
|
||||
ResponseEntity<TelegramBotNotifyResponse> entity = restTemplate.postForEntity(url, telegramEntity, TelegramBotNotifyResponse.class);
|
||||
ResponseEntity<TelegramBotNotifyResponse> entity = restTemplate.postForEntity(url, telegramEntity,
|
||||
TelegramBotNotifyResponse.class);
|
||||
if (entity.getStatusCode() == HttpStatus.OK && entity.getBody() != null) {
|
||||
TelegramBotNotifyResponse body = entity.getBody();
|
||||
if (body.ok) {
|
||||
@@ -99,4 +105,10 @@ final class TelegramBotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandler
|
||||
private String description;
|
||||
}
|
||||
|
||||
private boolean isValidTelegramToken(String token) {
|
||||
// Adjusted pattern to match real Telegram Bot tokens like
|
||||
// 110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw
|
||||
String tokenPattern = "^[0-9]+:[a-zA-Z0-9_-]+$";
|
||||
return token != null && token.matches(tokenPattern);
|
||||
}
|
||||
}
|
||||
|
||||
+17
-1
@@ -57,7 +57,12 @@ final class WeComRobotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerI
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
HttpEntity<WeWorkWebHookDto> httpEntity = new HttpEntity<>(weWorkWebHookDTO, headers);
|
||||
String webHookUrl = alerterProperties.getWeWorkWebhookUrl() + receiver.getWechatId();
|
||||
String wechatId = receiver.getWechatId();
|
||||
if (!isValidWechatId(wechatId)) {
|
||||
log.warn("Invalid WeChat ID: {}", wechatId);
|
||||
throw new AlertNoticeException("Invalid WeChat ID provided.");
|
||||
}
|
||||
String webHookUrl = alerterProperties.getWeWorkWebhookUrl() + wechatId;
|
||||
ResponseEntity<CommonRobotNotifyResp> entity = restTemplate.postForEntity(webHookUrl, httpEntity, CommonRobotNotifyResp.class);
|
||||
if (entity.getStatusCode() == HttpStatus.OK) {
|
||||
assert entity.getBody() != null;
|
||||
@@ -170,4 +175,15 @@ final class WeComRobotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerI
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the WeChat ID to ensure it meets the expected format.
|
||||
*
|
||||
* @param wechatId the WeChat ID to validate
|
||||
* @return true if valid, false otherwise
|
||||
*/
|
||||
private boolean isValidWechatId(String wechatId) {
|
||||
// Example validation: ensure the ID is alphanumeric and non-empty
|
||||
return StringUtils.isNotBlank(wechatId) && wechatId.matches("^[a-zA-Z0-9_-]+$");
|
||||
}
|
||||
}
|
||||
|
||||
+16
-26
@@ -20,6 +20,7 @@ package org.apache.hertzbeat.alert.notice.impl;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.apache.hertzbeat.alert.AlerterProperties;
|
||||
@@ -48,7 +49,7 @@ import java.util.ResourceBundle;
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ServerChanAlertNotifyHandlerImplTest {
|
||||
|
||||
|
||||
@Mock
|
||||
private RestTemplate restTemplate;
|
||||
|
||||
@@ -57,20 +58,21 @@ class ServerChanAlertNotifyHandlerImplTest {
|
||||
|
||||
@Mock
|
||||
private ResourceBundle bundle;
|
||||
|
||||
|
||||
@InjectMocks
|
||||
private ServerChanAlertNotifyHandlerImpl serverChanAlertNotifyHandler;
|
||||
|
||||
|
||||
private NoticeReceiver receiver;
|
||||
private GroupAlert groupAlert;
|
||||
private NoticeTemplate template;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
receiver = new NoticeReceiver();
|
||||
receiver.setId(1L);
|
||||
receiver.setName("test-receiver");
|
||||
receiver.setAccessToken("test-token");
|
||||
receiver.setServerChanToken("SCT193569TSNm6xIabdjqeZPtOGOWcvU1e");
|
||||
|
||||
groupAlert = new GroupAlert();
|
||||
SingleAlert singleAlert = new SingleAlert();
|
||||
@@ -87,43 +89,31 @@ class ServerChanAlertNotifyHandlerImplTest {
|
||||
template.setName("test-template");
|
||||
template.setContent("test content");
|
||||
|
||||
when(alerterProperties.getServerChanWebhookUrl()).thenReturn("http://test.url/");
|
||||
when(bundle.getString("alerter.notify.title")).thenReturn("Alert Notification");
|
||||
lenient().when(alerterProperties.getServerChanWebhookUrl())
|
||||
.thenReturn("https://api.serverchan.com/send/%s");
|
||||
lenient().when(bundle.getString("alerter.notify.title")).thenReturn("Alert Notification");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testNotifyAlertSuccess() {
|
||||
CommonRobotNotifyResp successResp = new CommonRobotNotifyResp();
|
||||
successResp.setErrCode(0);
|
||||
successResp.setMsg("success");
|
||||
ResponseEntity<CommonRobotNotifyResp> responseEntity =
|
||||
new ResponseEntity<>(successResp, HttpStatus.OK);
|
||||
|
||||
ResponseEntity<CommonRobotNotifyResp> responseEntity = new ResponseEntity<>(successResp, HttpStatus.OK);
|
||||
|
||||
when(restTemplate.postForEntity(
|
||||
any(String.class),
|
||||
any(),
|
||||
eq(CommonRobotNotifyResp.class)
|
||||
)).thenReturn(responseEntity);
|
||||
eq(CommonRobotNotifyResp.class))).thenReturn(responseEntity);
|
||||
|
||||
serverChanAlertNotifyHandler.send(receiver, template, groupAlert);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testNotifyAlertFailure() {
|
||||
CommonRobotNotifyResp failResp = new CommonRobotNotifyResp();
|
||||
failResp.setCode(1);
|
||||
failResp.setErrMsg("Test Error");
|
||||
ResponseEntity<CommonRobotNotifyResp> responseEntity =
|
||||
new ResponseEntity<>(failResp, HttpStatus.BAD_REQUEST);
|
||||
|
||||
when(restTemplate.postForEntity(
|
||||
any(String.class),
|
||||
any(),
|
||||
eq(CommonRobotNotifyResp.class)
|
||||
)).thenReturn(responseEntity);
|
||||
public void testNotifyAlertWithInvalidUrl() {
|
||||
when(alerterProperties.getServerChanWebhookUrl()).thenReturn("http://invalid-url.com/%s");
|
||||
|
||||
assertThrows(AlertNoticeException.class,
|
||||
assertThrows(AlertNoticeException.class,
|
||||
() -> serverChanAlertNotifyHandler.send(receiver, template, groupAlert));
|
||||
}
|
||||
}
|
||||
|
||||
+11
-15
@@ -51,7 +51,7 @@ class SlackAlertNotifyHandlerImplTest {
|
||||
|
||||
@Mock
|
||||
private RestTemplate restTemplate;
|
||||
|
||||
|
||||
@Mock
|
||||
private ResourceBundle bundle;
|
||||
|
||||
@@ -68,18 +68,18 @@ class SlackAlertNotifyHandlerImplTest {
|
||||
receiver.setId(1L);
|
||||
receiver.setName("test-receiver");
|
||||
receiver.setAccessToken("test-token");
|
||||
receiver.setSlackWebHookUrl("http://localhost:8080");
|
||||
|
||||
receiver.setSlackWebHookUrl("https://hooks.slack.com/services/ABCDEF/GHIJKL/mnopqrstuvwxyz");
|
||||
|
||||
groupAlert = new GroupAlert();
|
||||
SingleAlert singleAlert = new SingleAlert();
|
||||
singleAlert.setLabels(new HashMap<>());
|
||||
singleAlert.getLabels().put("severity", "critical");
|
||||
singleAlert.getLabels().put("alertname", "Test Alert");
|
||||
|
||||
|
||||
List<SingleAlert> alerts = new ArrayList<>();
|
||||
alerts.add(singleAlert);
|
||||
groupAlert.setAlerts(alerts);
|
||||
|
||||
|
||||
template = new NoticeTemplate();
|
||||
template.setId(1L);
|
||||
template.setName("test-template");
|
||||
@@ -90,22 +90,18 @@ class SlackAlertNotifyHandlerImplTest {
|
||||
|
||||
@Test
|
||||
public void testNotifyAlertSuccess() {
|
||||
ResponseEntity<String> responseEntity =
|
||||
new ResponseEntity<>("ok", HttpStatus.OK);
|
||||
|
||||
ResponseEntity<String> responseEntity = new ResponseEntity<>("ok", HttpStatus.OK);
|
||||
|
||||
when(restTemplate.postForEntity(any(String.class), any(), eq(String.class))).thenReturn(responseEntity);
|
||||
|
||||
|
||||
slackAlertNotifyHandler.send(receiver, template, groupAlert);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotifyAlertFailure() {
|
||||
ResponseEntity<String> responseEntity =
|
||||
new ResponseEntity<>("invalid_payload", HttpStatus.BAD_REQUEST);
|
||||
public void testNotifyAlertWithInvalidUrl() {
|
||||
receiver.setSlackWebHookUrl("http://localhost:8080");
|
||||
|
||||
when(restTemplate.postForEntity(any(String.class), any(), eq(String.class))).thenReturn(responseEntity);
|
||||
|
||||
assertThrows(AlertNoticeException.class,
|
||||
assertThrows(AlertNoticeException.class,
|
||||
() -> slackAlertNotifyHandler.send(receiver, template, groupAlert));
|
||||
}
|
||||
}
|
||||
|
||||
+25
-25
@@ -20,6 +20,7 @@ package org.apache.hertzbeat.alert.notice.impl;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.apache.hertzbeat.alert.AlerterProperties;
|
||||
@@ -51,10 +52,10 @@ class TelegramBotAlertNotifyHandlerImplTest {
|
||||
|
||||
@Mock
|
||||
private RestTemplate restTemplate;
|
||||
|
||||
|
||||
@Mock
|
||||
private AlerterProperties alerterProperties;
|
||||
|
||||
|
||||
@Mock
|
||||
private ResourceBundle bundle;
|
||||
|
||||
@@ -70,58 +71,57 @@ class TelegramBotAlertNotifyHandlerImplTest {
|
||||
receiver = new NoticeReceiver();
|
||||
receiver.setId(1L);
|
||||
receiver.setName("test-receiver");
|
||||
receiver.setAccessToken("test-token");
|
||||
receiver.setTgBotToken("123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11");
|
||||
receiver.setTgUserId("123456789"); // Telegram specific - chat ID
|
||||
|
||||
|
||||
groupAlert = new GroupAlert();
|
||||
SingleAlert singleAlert = new SingleAlert();
|
||||
singleAlert.setLabels(new HashMap<>());
|
||||
singleAlert.getLabels().put("severity", "critical");
|
||||
singleAlert.getLabels().put("alertname", "Test Alert");
|
||||
|
||||
|
||||
List<SingleAlert> alerts = new ArrayList<>();
|
||||
alerts.add(singleAlert);
|
||||
groupAlert.setAlerts(alerts);
|
||||
|
||||
|
||||
template = new NoticeTemplate();
|
||||
template.setId(1L);
|
||||
template.setName("test-template");
|
||||
template.setContent("test content");
|
||||
|
||||
when(alerterProperties.getTelegramWebhookUrl()).thenReturn("https://api.telegram.org/bot%s/sendMessage");
|
||||
when(bundle.getString("alerter.notify.title")).thenReturn("Alert Notification");
|
||||
|
||||
lenient().when(alerterProperties.getTelegramWebhookUrl())
|
||||
.thenReturn("https://api.telegram.org/bot%s/sendMessage");
|
||||
lenient().when(bundle.getString("alerter.notify.title")).thenReturn("Alert Notification");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotifyAlertSuccess() {
|
||||
TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse successResp =
|
||||
new TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse();
|
||||
TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse successResp = new TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse();
|
||||
successResp.setOk(true);
|
||||
successResp.setDescription("Test Success");
|
||||
|
||||
ResponseEntity<TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse> responseEntity =
|
||||
new ResponseEntity<>(successResp, HttpStatus.OK);
|
||||
|
||||
when(restTemplate.postForEntity(any(String.class), any(),
|
||||
|
||||
ResponseEntity<TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse> responseEntity = new ResponseEntity<>(
|
||||
successResp, HttpStatus.OK);
|
||||
|
||||
when(restTemplate.postForEntity(any(String.class), any(),
|
||||
eq(TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse.class))).thenReturn(responseEntity);
|
||||
|
||||
|
||||
telegramBotAlertNotifyHandler.send(receiver, template, groupAlert);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotifyAlertFailure() {
|
||||
TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse successResp =
|
||||
new TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse();
|
||||
successResp.setOk(false);
|
||||
successResp.setDescription("Test failed");
|
||||
TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse failureResp = new TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse();
|
||||
failureResp.setOk(false);
|
||||
failureResp.setDescription("Test failed");
|
||||
|
||||
ResponseEntity<TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse> responseEntity =
|
||||
new ResponseEntity<>(successResp, HttpStatus.BAD_REQUEST);
|
||||
ResponseEntity<TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse> responseEntity = new ResponseEntity<>(
|
||||
failureResp, HttpStatus.OK);
|
||||
|
||||
when(restTemplate.postForEntity(any(String.class), any(),
|
||||
eq(TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse.class))).thenReturn(responseEntity);
|
||||
|
||||
assertThrows(AlertNoticeException.class,
|
||||
|
||||
assertThrows(AlertNoticeException.class,
|
||||
() -> telegramBotAlertNotifyHandler.send(receiver, template, groupAlert));
|
||||
}
|
||||
}
|
||||
|
||||
+13
-16
@@ -48,7 +48,7 @@ import java.util.ResourceBundle;
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class WeComRobotAlertNotifyHandlerImplTest {
|
||||
|
||||
|
||||
@Mock
|
||||
private RestTemplate restTemplate;
|
||||
|
||||
@@ -57,20 +57,21 @@ class WeComRobotAlertNotifyHandlerImplTest {
|
||||
|
||||
@Mock
|
||||
private ResourceBundle bundle;
|
||||
|
||||
|
||||
@InjectMocks
|
||||
private WeComRobotAlertNotifyHandlerImpl weComRobotAlertNotifyHandler;
|
||||
|
||||
|
||||
private NoticeReceiver receiver;
|
||||
private GroupAlert groupAlert;
|
||||
private NoticeTemplate template;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
receiver = new NoticeReceiver();
|
||||
receiver.setId(1L);
|
||||
receiver.setName("test-receiver");
|
||||
receiver.setAccessToken("test-token");
|
||||
receiver.setWechatId("test-wechat-id");
|
||||
|
||||
groupAlert = new GroupAlert();
|
||||
SingleAlert singleAlert = new SingleAlert();
|
||||
@@ -95,33 +96,29 @@ class WeComRobotAlertNotifyHandlerImplTest {
|
||||
public void testNotifyAlertSuccess() {
|
||||
CommonRobotNotifyResp successResp = new CommonRobotNotifyResp();
|
||||
successResp.setErrCode(0);
|
||||
ResponseEntity<CommonRobotNotifyResp> responseEntity =
|
||||
new ResponseEntity<>(successResp, HttpStatus.OK);
|
||||
|
||||
ResponseEntity<CommonRobotNotifyResp> responseEntity = new ResponseEntity<>(successResp, HttpStatus.OK);
|
||||
|
||||
when(restTemplate.postForEntity(
|
||||
any(String.class),
|
||||
any(),
|
||||
eq(CommonRobotNotifyResp.class)
|
||||
)).thenReturn(responseEntity);
|
||||
eq(CommonRobotNotifyResp.class))).thenReturn(responseEntity);
|
||||
|
||||
weComRobotAlertNotifyHandler.send(receiver, template, groupAlert);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testNotifyAlertFailure() {
|
||||
CommonRobotNotifyResp failResp = new CommonRobotNotifyResp();
|
||||
failResp.setCode(1);
|
||||
failResp.setErrMsg("Test Error");
|
||||
ResponseEntity<CommonRobotNotifyResp> responseEntity =
|
||||
new ResponseEntity<>(failResp, HttpStatus.OK);
|
||||
|
||||
ResponseEntity<CommonRobotNotifyResp> responseEntity = new ResponseEntity<>(failResp, HttpStatus.OK);
|
||||
|
||||
when(restTemplate.postForEntity(
|
||||
any(String.class),
|
||||
any(),
|
||||
eq(CommonRobotNotifyResp.class)
|
||||
)).thenReturn(responseEntity);
|
||||
eq(CommonRobotNotifyResp.class))).thenReturn(responseEntity);
|
||||
|
||||
assertThrows(AlertNoticeException.class,
|
||||
assertThrows(AlertNoticeException.class,
|
||||
() -> weComRobotAlertNotifyHandler.send(receiver, template, groupAlert));
|
||||
}
|
||||
}
|
||||
|
||||
+97
-12
@@ -66,7 +66,7 @@ public class JmxCollectImpl extends AbstractCollect {
|
||||
private static final String JMX_URL_PREFIX = "service:jmx:rmi:///jndi/rmi://";
|
||||
|
||||
private static final String JMX_URL_SUFFIX = "/jmxrmi";
|
||||
|
||||
|
||||
private static final String IGNORED_STUB = "/stub/";
|
||||
|
||||
private static final String SUB_ATTRIBUTE = "->";
|
||||
@@ -75,7 +75,6 @@ public class JmxCollectImpl extends AbstractCollect {
|
||||
|
||||
private final ClassLoader jmxClassLoader;
|
||||
|
||||
|
||||
public JmxCollectImpl() {
|
||||
jmxClassLoader = new JmxClassLoader(ClassLoader.getSystemClassLoader());
|
||||
}
|
||||
@@ -83,13 +82,66 @@ public class JmxCollectImpl extends AbstractCollect {
|
||||
@Override
|
||||
public void preCheck(Metrics metrics) throws IllegalArgumentException {
|
||||
Assert.isTrue(metrics != null && metrics.getJmx() != null, "JMX collect must have JMX params");
|
||||
JmxProtocol jmxProtocol = metrics.getJmx();
|
||||
|
||||
String url = metrics.getJmx().getUrl();
|
||||
// Validate JMX URL if provided
|
||||
String url = jmxProtocol.getUrl();
|
||||
if (StringUtils.hasText(url)) {
|
||||
Assert.doesNotContain(url, IGNORED_STUB, "JMX url prohibit contains stub, please check");
|
||||
|
||||
// Prevent JNDI injection by validating URL format
|
||||
validateJmxUrl(url);
|
||||
} else {
|
||||
// Validate host and port inputs
|
||||
String host = jmxProtocol.getHost();
|
||||
int port = Integer.parseInt(jmxProtocol.getPort());
|
||||
|
||||
// Validate host format (only allow valid hostnames or IP addresses)
|
||||
Assert.isTrue(isValidHostname(host), "Invalid hostname format");
|
||||
Assert.isTrue(port > 0 && port <= 65535, "Port must be between 1 and 65535");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate JMX URL
|
||||
*
|
||||
* @param url JMX URL to validate
|
||||
* @throws IllegalArgumentException if URL is potentially malicious
|
||||
*/
|
||||
private void validateJmxUrl(String url) throws IllegalArgumentException {
|
||||
// Only allow service:jmx:rmi protocol
|
||||
Assert.isTrue(url.startsWith("service:jmx:rmi:"), "Only service:jmx:rmi protocol is supported");
|
||||
|
||||
String[] disallowedPatterns = { "ldap:", "rmi:", "iiop:", "nis:", "dns:", "corbaname:", "http:", "https:" };
|
||||
for (String pattern : disallowedPatterns) {
|
||||
if (url.contains(pattern) && !pattern.equals("rmi:///jndi/rmi:")) {
|
||||
throw new IllegalArgumentException("Potentially unsafe JNDI protocol detected in URL: " + pattern);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for suspicious patterns
|
||||
if (url.contains("${") || url.contains("$[") || url.contains(":#") || url.contains(":/")) {
|
||||
throw new IllegalArgumentException("Potentially malicious pattern detected in JMX URL");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate hostname format
|
||||
*
|
||||
* @param hostname Hostname to validate
|
||||
* @return true if hostname is valid
|
||||
*/
|
||||
private boolean isValidHostname(String hostname) {
|
||||
if (hostname == null || hostname.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Simplified hostname/IP validation regex
|
||||
// This regex accepts valid hostnames, IPv4 and IPv6 addresses
|
||||
String hostnameRegex = "^([a-zA-Z0-9][-a-zA-Z0-9]*\\.)+[a-zA-Z0-9][-a-zA-Z0-9]*$|^(\\d{1,3}\\.){3}\\d{1,3}$|^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$";
|
||||
return hostname.matches(hostnameRegex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
|
||||
ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
|
||||
@@ -172,16 +224,18 @@ public class JmxCollectImpl extends AbstractCollect {
|
||||
log.info("attribute {} value is null.", attribute.getName());
|
||||
continue;
|
||||
}
|
||||
if (value instanceof Number || value instanceof String || value instanceof ObjectName
|
||||
if (value instanceof Number || value instanceof String || value instanceof ObjectName
|
||||
|| value instanceof Boolean || value instanceof Date || value instanceof TimeUnit) {
|
||||
attributeValueMap.put(attribute.getName(), value.toString());
|
||||
} else if (value instanceof CompositeData compositeData) {
|
||||
} else if (value instanceof CompositeData) {
|
||||
CompositeData compositeData = (CompositeData) value;
|
||||
CompositeType compositeType = compositeData.getCompositeType();
|
||||
for (String typeKey : compositeType.keySet()) {
|
||||
Object fieldValue = compositeData.get(typeKey);
|
||||
attributeValueMap.put(attribute.getName() + SUB_ATTRIBUTE + typeKey, fieldValue.toString());
|
||||
}
|
||||
} else if (value instanceof String[] values) {
|
||||
} else if (value instanceof String[]) {
|
||||
String[] values = (String[]) value;
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int index = 0; index < values.length; index++) {
|
||||
builder.append(values[index]);
|
||||
@@ -219,12 +273,33 @@ public class JmxCollectImpl extends AbstractCollect {
|
||||
String url;
|
||||
if (jmxProtocol.getUrl() != null) {
|
||||
url = jmxProtocol.getUrl();
|
||||
// Double check URL format for security
|
||||
if (!url.startsWith("service:jmx:rmi:")) {
|
||||
throw new IOException("Unsupported JMX URL protocol. Only service:jmx:rmi: is allowed.");
|
||||
}
|
||||
} else {
|
||||
url = JMX_URL_PREFIX + jmxProtocol.getHost() + ":" + jmxProtocol.getPort() + JMX_URL_SUFFIX;
|
||||
// More strict formatting with proper escaping
|
||||
String host = jmxProtocol.getHost();
|
||||
int port = Integer.parseInt(jmxProtocol.getPort());
|
||||
|
||||
// Additional validation at connection time
|
||||
if (!isValidHostname(host)) {
|
||||
throw new IOException("Invalid hostname format for JMX connection: " + host);
|
||||
}
|
||||
if (port <= 0 || port > 65535) {
|
||||
throw new IOException("Invalid port for JMX connection: " + port);
|
||||
}
|
||||
|
||||
url = JMX_URL_PREFIX + host + ":" + port + JMX_URL_SUFFIX;
|
||||
}
|
||||
|
||||
// Set security properties to prevent remote class loading
|
||||
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "false");
|
||||
System.setProperty("com.sun.jndi.cosnaming.object.trustURLCodebase", "false");
|
||||
|
||||
Map<String, Object> environment = new HashMap<>(4);
|
||||
if (StringUtils.hasText(jmxProtocol.getUsername()) && StringUtils.hasText(jmxProtocol.getPassword())) {
|
||||
String[] credential = new String[] {jmxProtocol.getUsername(), jmxProtocol.getPassword()};
|
||||
String[] credential = new String[] { jmxProtocol.getUsername(), jmxProtocol.getPassword() };
|
||||
environment.put(javax.management.remote.JMXConnector.CREDENTIALS, credential);
|
||||
}
|
||||
if (Boolean.TRUE.toString().equals(jmxProtocol.getSsl())) {
|
||||
@@ -233,10 +308,20 @@ public class JmxCollectImpl extends AbstractCollect {
|
||||
environment.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, clientSocketFactory);
|
||||
environment.put("com.sun.jndi.rmi.factory.socket", clientSocketFactory);
|
||||
}
|
||||
JMXServiceURL jmxServiceUrl = new JMXServiceURL(url);
|
||||
conn = JMXConnectorFactory.connect(jmxServiceUrl, environment);
|
||||
connectionCommonCache.addCache(identifier, new JmxConnect(conn));
|
||||
return conn;
|
||||
|
||||
// Limit JMX connection timeout
|
||||
environment.put("jmx.remote.x.client.connection.timeout", 10000);
|
||||
environment.put("jmx.remote.x.server.connection.timeout", 10000);
|
||||
|
||||
try {
|
||||
JMXServiceURL jmxServiceUrl = new JMXServiceURL(url);
|
||||
conn = JMXConnectorFactory.connect(jmxServiceUrl, environment);
|
||||
connectionCommonCache.addCache(identifier, new JmxConnect(conn));
|
||||
return conn;
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to connect to JMX server: {}", e.getMessage());
|
||||
throw new IOException("Failed to connect to JMX server: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-4
@@ -150,10 +150,7 @@ public class ServiceDiscoveryWorker implements InitializingBean {
|
||||
// Thus, all monitors still in hostMonitorMap need to be cancelled.
|
||||
final Set<Long> needCancelMonitorIdSet = subMonitorBindMap.values().stream()
|
||||
.map(MonitorBind::getMonitorId).collect(Collectors.toSet());
|
||||
monitorService.cancelManageMonitors(needCancelMonitorIdSet);
|
||||
for (Long id : needCancelMonitorIdSet) {
|
||||
monitorBindDao.deleteMonitorBindByBizIdAndMonitorId(monitorId, id);
|
||||
}
|
||||
monitorService.deleteMonitors(needCancelMonitorIdSet);
|
||||
} catch (Exception exception) {
|
||||
log.error(exception.getMessage(), exception);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
package org.apache.hertzbeat.manager.dao;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.hertzbeat.common.entity.manager.MonitorBind;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
@@ -31,9 +33,15 @@ public interface MonitorBindDao extends JpaRepository<MonitorBind, Long>, JpaSpe
|
||||
|
||||
List<MonitorBind> findMonitorBindsByBizId(Long bizId);
|
||||
|
||||
List<MonitorBind> findMonitorBindsByBizIdIn(Set<Long> bizIds);
|
||||
|
||||
|
||||
void deleteByMonitorId(Long monitorId);
|
||||
|
||||
@Modifying
|
||||
@Transactional
|
||||
void deleteMonitorBindByBizIdAndMonitorId(Long bizId, Long monitorId);
|
||||
|
||||
@Modifying
|
||||
void deleteMonitorBindByBizIdIn(Set<Long> bizIds);
|
||||
}
|
||||
|
||||
+11
-1
@@ -37,6 +37,7 @@ import org.apache.hertzbeat.common.entity.manager.Collector;
|
||||
import org.apache.hertzbeat.common.entity.manager.CollectorMonitorBind;
|
||||
import org.apache.hertzbeat.common.entity.manager.Label;
|
||||
import org.apache.hertzbeat.common.entity.manager.Monitor;
|
||||
import org.apache.hertzbeat.common.entity.manager.MonitorBind;
|
||||
import org.apache.hertzbeat.common.entity.manager.Param;
|
||||
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
@@ -86,6 +87,7 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
@@ -529,12 +531,16 @@ public class MonitorServiceImpl implements MonitorService {
|
||||
if (CollectionUtils.isEmpty(ids)) {
|
||||
return;
|
||||
}
|
||||
List<Monitor> monitors = monitorDao.findMonitorsByIdIn(ids);
|
||||
Set<Long> subMonitorIds = monitorBindDao.findMonitorBindsByBizIdIn(ids).stream().map(MonitorBind::getMonitorId).collect(Collectors.toSet());
|
||||
Set<Long> allMonitorIds = new HashSet<>(ids);
|
||||
allMonitorIds.addAll(subMonitorIds);
|
||||
List<Monitor> monitors = monitorDao.findMonitorsByIdIn(allMonitorIds);
|
||||
if (!monitors.isEmpty()) {
|
||||
monitorDao.deleteAll(monitors);
|
||||
paramDao.deleteParamsByMonitorIdIn(ids);
|
||||
Set<Long> monitorIds = monitors.stream().map(Monitor::getId).collect(Collectors.toSet());
|
||||
alertDefineBindDao.deleteAlertDefineMonitorBindsByMonitorIdIn(monitorIds);
|
||||
monitorBindDao.deleteMonitorBindByBizIdIn(monitorIds);
|
||||
for (Monitor monitor : monitors) {
|
||||
monitorBindDao.deleteByMonitorId(monitor.getId());
|
||||
collectorMonitorBindDao.deleteCollectorMonitorBindsByMonitorId(monitor.getId());
|
||||
@@ -650,6 +656,8 @@ public class MonitorServiceImpl implements MonitorService {
|
||||
}
|
||||
// Update monitoring status Delete corresponding monitoring periodic task
|
||||
// The jobId is not deleted, and the jobId is reused again after the management is started.
|
||||
Set<Long> subMonitorIds = monitorBindDao.findMonitorBindsByBizIdIn(ids).stream().map(MonitorBind::getMonitorId).collect(Collectors.toSet());
|
||||
ids.addAll(subMonitorIds);
|
||||
List<Monitor> managedMonitors = monitorDao.findMonitorsByIdIn(ids)
|
||||
.stream().filter(monitor ->
|
||||
monitor.getStatus() != CommonConstants.MONITOR_PAUSED_CODE)
|
||||
@@ -666,6 +674,8 @@ public class MonitorServiceImpl implements MonitorService {
|
||||
@Override
|
||||
public void enableManageMonitors(Set<Long> ids) {
|
||||
// Update monitoring status Add corresponding monitoring periodic task
|
||||
Set<Long> subMonitorIds = monitorBindDao.findMonitorBindsByBizIdIn(ids).stream().map(MonitorBind::getMonitorId).collect(Collectors.toSet());
|
||||
ids.addAll(subMonitorIds);
|
||||
List<Monitor> unManagedMonitors = monitorDao.findMonitorsByIdIn(ids)
|
||||
.stream().filter(monitor ->
|
||||
monitor.getStatus() == CommonConstants.MONITOR_PAUSED_CODE)
|
||||
|
||||
Reference in New Issue
Block a user