Skip to content

Commit bbb297a

Browse files
feat(providers/huaweicloud): add smn_topic_subscriptions check (#12186)
Co-authored-by: tomitobio <tomitobio@users.noreply.github.com> Co-authored-by: Hugo P.Brito <hugopbrit@gmail.com>
1 parent d0d2910 commit bbb297a

13 files changed

Lines changed: 569 additions & 0 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`smn_topic_subscriptions` check for Huawei Cloud provider: SMN topics have at least one subscription configured

prowler/providers/huaweicloud/models.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,19 @@ def client(self, service: str, region: str = None) -> Any:
384384
.build()
385385
)
386386

387+
elif service == "smn":
388+
from huaweicloudsdksmn.v2 import SmnClient
389+
from huaweicloudsdksmn.v2.region.smn_region import SmnRegion
390+
391+
client_region = region or self._region
392+
return (
393+
SmnClient.new_builder()
394+
.with_credentials(self._get_basic_credentials(client_region))
395+
.with_http_config(self._http_config())
396+
.with_region(_aligned_region(SmnRegion, client_region))
397+
.build()
398+
)
399+
387400
else:
388401
raise HuaweiCloudServiceError(
389402
message=f"Huawei Cloud service '{service}' is not supported"

prowler/providers/huaweicloud/services/smn/__init__.py

Whitespace-only changes.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from prowler.providers.huaweicloud.services.smn.smn_service import SMN
2+
from prowler.providers.common.provider import Provider
3+
4+
smn_client = SMN(Provider.get_global_provider())
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
from typing import List
2+
3+
from pydantic.v1 import BaseModel
4+
5+
from prowler.lib.logger import logger
6+
from prowler.providers.huaweicloud.lib.service.service import HuaweiCloudService
7+
8+
SMN_PAGE_SIZE = 100
9+
10+
11+
class SMN(HuaweiCloudService):
12+
"""
13+
SMN (Simple Message Notification) service class for Huawei Cloud.
14+
15+
This class provides methods to interact with Huawei Cloud SMN service
16+
to retrieve notification topics and their subscription counts.
17+
"""
18+
19+
def __init__(self, provider):
20+
super().__init__(__class__.__name__, provider)
21+
22+
self.topics: List[SMNTopic] = []
23+
24+
if getattr(self.session, "is_mock", False):
25+
self._load_mock_data()
26+
return
27+
28+
self._list_topics()
29+
30+
def _load_mock_data(self):
31+
"""Load mock data for testing."""
32+
region = "la-south-2"
33+
self.topics = [
34+
SMNTopic(
35+
topic_urn="urn:smn:la-south-2:123456789012:alert-topic",
36+
topic_id="topic-001",
37+
name="alert-topic",
38+
display_name="Alert Topic",
39+
push_policy=0,
40+
confirmed_subscription_count=2,
41+
region=region,
42+
),
43+
SMNTopic(
44+
topic_urn="urn:smn:la-south-2:123456789012:empty-topic",
45+
topic_id="topic-002",
46+
name="empty-topic",
47+
display_name="Empty Topic",
48+
push_policy=0,
49+
confirmed_subscription_count=0,
50+
region=region,
51+
),
52+
]
53+
54+
def _list_topics(self):
55+
"""List all SMN topics across regions and get their subscription counts."""
56+
if not self.regional_clients:
57+
return
58+
59+
for region, client in self.regional_clients.items():
60+
logger.info(f"SMN - Listing Topics in {region}...")
61+
62+
try:
63+
from huaweicloudsdksmn.v2 import (
64+
ListSubscriptionsByTopicRequest,
65+
ListTopicsRequest,
66+
)
67+
68+
offset = 0
69+
while True:
70+
request = ListTopicsRequest(offset=offset, limit=SMN_PAGE_SIZE)
71+
response = self._call_with_retries(client.list_topics, request)
72+
topics = getattr(response, "topics", None) or []
73+
74+
for topic in topics:
75+
topic_urn = getattr(topic, "topic_urn", "") or ""
76+
topic_id = getattr(topic, "topic_id", "") or ""
77+
name = getattr(topic, "name", "") or ""
78+
display_name = getattr(topic, "display_name", "") or ""
79+
push_policy = getattr(topic, "push_policy", None)
80+
81+
try:
82+
confirmed_subscription_count = 0
83+
subscription_offset = 0
84+
while True:
85+
sub_request = ListSubscriptionsByTopicRequest(
86+
topic_urn=topic_urn,
87+
offset=subscription_offset,
88+
limit=SMN_PAGE_SIZE,
89+
)
90+
sub_response = self._call_with_retries(
91+
client.list_subscriptions_by_topic, sub_request
92+
)
93+
subscriptions = (
94+
getattr(sub_response, "subscriptions", None) or []
95+
)
96+
confirmed_subscription_count += sum(
97+
getattr(subscription, "status", None) == 1
98+
for subscription in subscriptions
99+
)
100+
subscription_count = (
101+
getattr(sub_response, "subscription_count", 0) or 0
102+
)
103+
subscription_offset += SMN_PAGE_SIZE
104+
if subscription_offset >= subscription_count:
105+
break
106+
except Exception as sub_error:
107+
logger.error(
108+
f"{region} -- {sub_error.__class__.__name__}"
109+
f"[{sub_error.__traceback__.tb_lineno}]: {sub_error}"
110+
)
111+
continue
112+
113+
self.topics.append(
114+
SMNTopic(
115+
topic_urn=topic_urn,
116+
topic_id=topic_id,
117+
name=name,
118+
display_name=display_name,
119+
push_policy=push_policy,
120+
confirmed_subscription_count=confirmed_subscription_count,
121+
region=region,
122+
)
123+
)
124+
125+
offset += SMN_PAGE_SIZE
126+
topic_count = getattr(response, "topic_count", 0) or 0
127+
if offset >= topic_count:
128+
break
129+
130+
except Exception as error:
131+
logger.error(
132+
f"{region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
133+
)
134+
135+
136+
class SMNTopic(BaseModel):
137+
"""SMN topic model."""
138+
139+
topic_urn: str
140+
topic_id: str = ""
141+
name: str = ""
142+
display_name: str = ""
143+
push_policy: int = None
144+
confirmed_subscription_count: int = 0
145+
region: str = ""

prowler/providers/huaweicloud/services/smn/smn_topic_subscriptions/__init__.py

Whitespace-only changes.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"Provider": "huaweicloud",
3+
"CheckID": "smn_topic_subscriptions",
4+
"CheckTitle": "SMN topics have at least one confirmed subscription",
5+
"CheckType": [],
6+
"ServiceName": "smn",
7+
"SubServiceName": "",
8+
"ResourceIdTemplate": "",
9+
"Severity": "low",
10+
"ResourceType": "HUAWEICLOUD::SMN::Topic",
11+
"ResourceGroup": "messaging",
12+
"Description": "Ensure that SMN notification topics have at least one confirmed subscription so alerts can be delivered to recipients.",
13+
"Risk": "Topics without confirmed subscriptions cannot deliver notifications, meaning critical alerts may go unnoticed by operations and security teams.",
14+
"RelatedUrl": "",
15+
"AdditionalURLs": [
16+
"https://support.huaweicloud.com/intl/en-us/api-smn/ListSubscriptionsByTopic.html",
17+
"https://support.huaweicloud.com/intl/en-us/api-smn/AddSubscription.html"
18+
],
19+
"Remediation": {
20+
"Code": {
21+
"CLI": "hcloud SMN AddSubscription --topic_urn=<topic_urn> --protocol=<protocol> --endpoint=<endpoint>",
22+
"NativeIaC": "",
23+
"Other": "1. Log on to the Huawei Cloud console.\n2. Navigate to Simple Message Notification (SMN).\n3. Select the topic without subscriptions.\n4. Click Add Subscription.\n5. Configure the subscription protocol and endpoint.\n6. Confirm the subscription.",
24+
"Terraform": ""
25+
},
26+
"Recommendation": {
27+
"Text": "Add and confirm at least one subscription for each SMN topic so notifications can be delivered.",
28+
"Url": "https://hub.prowler.com/check/smn_topic_subscriptions"
29+
}
30+
},
31+
"Categories": [
32+
"logging"
33+
],
34+
"DependsOn": [],
35+
"RelatedTo": [],
36+
"Notes": "Only confirmed subscriptions (status 1) satisfy this check. Unconfirmed or canceled subscriptions cannot provide notification coverage."
37+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from prowler.lib.check.models import Check, CheckReportHuaweiCloud
2+
from prowler.providers.huaweicloud.services.smn.smn_client import smn_client
3+
4+
5+
class smn_topic_subscriptions(Check):
6+
"""Check if SMN topics have at least one subscription configured."""
7+
8+
def execute(self) -> list[CheckReportHuaweiCloud]:
9+
findings = []
10+
11+
for topic in smn_client.topics:
12+
report = CheckReportHuaweiCloud(
13+
metadata=self.metadata(),
14+
resource=topic,
15+
)
16+
report.region = topic.region
17+
report.resource_id = topic.topic_id
18+
report.resource_name = topic.name
19+
report.resource_arn = topic.topic_urn
20+
21+
if topic.confirmed_subscription_count > 0:
22+
report.status = "PASS"
23+
report.status_extended = (
24+
f"SMN topic '{topic.name}' ({topic.topic_id}) has "
25+
f"{topic.confirmed_subscription_count} confirmed subscription(s)."
26+
)
27+
else:
28+
report.status = "FAIL"
29+
report.status_extended = (
30+
f"SMN topic '{topic.name}' ({topic.topic_id}) has no confirmed "
31+
"subscriptions. Notifications will not be delivered."
32+
)
33+
34+
findings.append(report)
35+
36+
return findings

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ dependencies = [
133133
"huaweicloudsdkkms==3.1.204",
134134
"huaweicloudsdkobs==3.1.204",
135135
"huaweicloudsdkrds==3.1.204",
136+
"huaweicloudsdksmn==3.1.204",
136137
"huaweicloudsdkvpc==3.1.204",
137138
"huaweicloudsdkwaf==3.1.204",
138139
"zstandard==0.25.0"
@@ -271,6 +272,7 @@ constraint-dependencies = [
271272
"huaweicloudsdkkms==3.1.204",
272273
"huaweicloudsdkobs==3.1.204",
273274
"huaweicloudsdkrds==3.1.204",
275+
"huaweicloudsdksmn==3.1.204",
274276
"huaweicloudsdkvpc==3.1.204",
275277
"huaweicloudsdkwaf==3.1.204",
276278
"hyperframe==6.1.0",

tests/providers/huaweicloud/huaweicloud_provider_test.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,32 @@ def test_aligned_region_unknown_region_falls_through(self):
292292
endpoint = "https://ecs.af-north-1.myhuaweicloud.com"
293293
assert _align_endpoint_tld("af-north-1", endpoint) == endpoint
294294

295+
def test_smn_client_uses_europe_endpoint(self):
296+
session = HuaweiCloudSession(
297+
HuaweiCloudCredentials(ak=ACCESS_KEY, sk=SECRET_KEY),
298+
region="eu-west-101",
299+
)
300+
builder = mock.MagicMock()
301+
builder.with_credentials.return_value = builder
302+
builder.with_http_config.return_value = builder
303+
builder.with_region.return_value = builder
304+
expected_client = mock.MagicMock()
305+
builder.build.return_value = expected_client
306+
307+
with (
308+
mock.patch(
309+
"huaweicloudsdksmn.v2.SmnClient.new_builder", return_value=builder
310+
),
311+
mock.patch.object(session, "_http_config"),
312+
mock.patch.object(session, "_get_basic_credentials"),
313+
):
314+
client = session.client("smn", "eu-west-101")
315+
316+
assert client is expected_client
317+
region = builder.with_region.call_args.args[0]
318+
assert region.id == "eu-west-101"
319+
assert region.endpoints == ["https://smn.eu-west-101.myhuaweicloud.eu"]
320+
295321

296322
class TestHuaweiCloudProviderValidationRegion:
297323
def test_no_regions_uses_default(self):

0 commit comments

Comments
 (0)