Skip to content

Commit 6271eaf

Browse files
committed
feat(reporter): add S002 SSL/TLS settings report
Add new security report S002 that collects SSL/TLS related PostgreSQL settings to assess the security posture of encrypted connections. Settings collected: - ssl (on/off status) - ssl_min_protocol_version / ssl_max_protocol_version - ssl_cert_file, ssl_key_file, ssl_ca_file - ssl_ciphers, ssl_prefer_server_ciphers - ssl_ecdh_curve, ssl_dh_params_file - ssl_crl_file, ssl_crl_dir - ssl_passphrase_command, ssl_passphrase_command_supports_reload
1 parent 6d64dfc commit 6271eaf

3 files changed

Lines changed: 180 additions & 1 deletion

File tree

reporter/postgres_reports.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,73 @@ def generate_a007_altered_settings_report(self, cluster: str = "local", node_nam
596596

597597
return self.format_report_data("A007", altered_settings, node_name, postgres_version=self._get_postgres_version_info(cluster, node_name))
598598

599+
def generate_s002_ssl_tls_report(self, cluster: str = "local", node_name: str = "node-01") -> Dict[str, Any]:
600+
"""
601+
Generate S002 SSL/TLS Settings report.
602+
603+
This report collects SSL/TLS related PostgreSQL settings to assess
604+
the security posture of encrypted connections.
605+
606+
Args:
607+
cluster: Cluster name
608+
node_name: Node name
609+
610+
Returns:
611+
Dictionary containing SSL/TLS settings information
612+
"""
613+
logger.info("Generating S002 SSL/TLS Settings report...")
614+
615+
# SSL-related settings to collect
616+
ssl_settings = [
617+
'ssl',
618+
'ssl_ca_file',
619+
'ssl_cert_file',
620+
'ssl_ciphers',
621+
'ssl_crl_dir',
622+
'ssl_crl_file',
623+
'ssl_dh_params_file',
624+
'ssl_ecdh_curve',
625+
'ssl_key_file',
626+
'ssl_max_protocol_version',
627+
'ssl_min_protocol_version',
628+
'ssl_passphrase_command',
629+
'ssl_passphrase_command_supports_reload',
630+
'ssl_prefer_server_ciphers',
631+
]
632+
633+
# Query all PostgreSQL settings using the pgwatch_settings_configured metric
634+
settings_query = f'last_over_time(pgwatch_settings_configured{{cluster="{cluster}", node_name="{node_name}"}}[3h])'
635+
result = self.query_instant(settings_query)
636+
637+
settings_data = {}
638+
if result.get('status') == 'success' and result.get('data', {}).get('result'):
639+
for item in result['data']['result']:
640+
setting_name = item['metric'].get('setting_name', '')
641+
642+
# Only include SSL-related settings
643+
if setting_name not in ssl_settings:
644+
continue
645+
646+
setting_value = item['metric'].get('setting_value', '')
647+
category = item['metric'].get('category', 'Other')
648+
unit = item['metric'].get('unit', '')
649+
context = item['metric'].get('context', '')
650+
vartype = item['metric'].get('vartype', '')
651+
652+
settings_data[setting_name] = {
653+
"setting": setting_value,
654+
"unit": unit,
655+
"category": category,
656+
"context": context,
657+
"vartype": vartype,
658+
"pretty_value": self.format_setting_value(setting_name, setting_value, unit)
659+
}
660+
else:
661+
logger.warning(f"S002 - No settings data returned for cluster={cluster}, node_name={node_name}")
662+
logger.info(f"Query result status: {result.get('status')}")
663+
664+
return self.format_report_data("S002", settings_data, node_name, postgres_version=self._get_postgres_version_info(cluster, node_name))
665+
599666
def generate_h001_invalid_indexes_report(self, cluster: str = "local", node_name: str = "node-01") -> Dict[
600667
str, Any]:
601668
"""
@@ -3761,6 +3828,7 @@ def get_check_title(self, check_id: str) -> str:
37613828
"L002": "Data types being used",
37623829
"L003": "Integer out-of-range risks in PKs",
37633830
"L004": "Tables without PK/UK",
3831+
"S002": "SSL/TLS settings",
37643832
}
37653833
return check_titles.get(check_id, f"Check {check_id}")
37663834

@@ -3970,6 +4038,7 @@ def generate_all_reports(self, cluster: str = "local", node_name: str = None, co
39704038
('A003', self.generate_a003_settings_report),
39714039
('A004', self.generate_a004_cluster_report),
39724040
('A007', self.generate_a007_altered_settings_report),
4041+
('S002', self.generate_s002_ssl_tls_report),
39734042
('F004', self.generate_f004_heap_bloat_report),
39744043
('F005', self.generate_f005_btree_bloat_report),
39754044
('H001', self.generate_h001_invalid_indexes_report),
@@ -4834,7 +4903,8 @@ def main():
48344903
help='Disable combining primary and replica reports into single report')
48354904
parser.add_argument('--check-id',
48364905
choices=['A002', 'A003', 'A004', 'A007', 'D004', 'F001', 'F004', 'F005', 'G001', 'H001', 'H002',
4837-
'H004', 'K001', 'K003', 'K004', 'K005', 'K006', 'K007', 'K008', 'M001', 'M002', 'M003', 'N001', 'ALL'],
4906+
'H004', 'K001', 'K003', 'K004', 'K005', 'K006', 'K007', 'K008', 'M001', 'M002', 'M003', 'N001',
4907+
'S002', 'ALL'],
48384908
help='Specific check ID to generate (default: ALL)')
48394909
parser.add_argument('--output', default='-',
48404910
help='Output file (default: stdout)')
@@ -4960,6 +5030,8 @@ def main():
49605030
report = generator.generate_a004_cluster_report(cluster, args.node_name)
49615031
elif args.check_id == 'A007':
49625032
report = generator.generate_a007_altered_settings_report(cluster, args.node_name)
5033+
elif args.check_id == 'S002':
5034+
report = generator.generate_s002_ssl_tls_report(cluster, args.node_name)
49635035
elif args.check_id == 'D004':
49645036
if a003_report:
49655037
report = generator.generate_d004_from_a003(a003_report, cluster, args.node_name)

reporter/schemas/S002.schema.json

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"title": "S002 report schema",
4+
"type": "object",
5+
"additionalProperties": false,
6+
"required": ["checkId", "checkTitle", "timestamptz", "nodes", "results"],
7+
"properties": {
8+
"version": { "type": ["string", "null"] },
9+
"build_ts": { "type": ["string", "null"] },
10+
"generation_mode": { "type": ["string", "null"] },
11+
"checkId": { "const": "S002" },
12+
"checkTitle": { "const": "SSL/TLS settings" },
13+
"timestamptz": { "type": "string" },
14+
"nodes": { "$ref": "#/$defs/nodes" },
15+
"results": {
16+
"type": "object",
17+
"minProperties": 1,
18+
"additionalProperties": { "$ref": "#/$defs/nodeResult" }
19+
}
20+
},
21+
"$defs": {
22+
"nodes": {
23+
"type": "object",
24+
"additionalProperties": false,
25+
"required": ["primary", "standbys"],
26+
"properties": {
27+
"primary": { "type": "string" },
28+
"standbys": { "type": "array", "items": { "type": "string" } }
29+
}
30+
},
31+
"postgresVersion": {
32+
"type": "object",
33+
"additionalProperties": false,
34+
"required": ["version", "server_version_num", "server_major_ver", "server_minor_ver"],
35+
"properties": {
36+
"version": { "type": "string" },
37+
"server_version_num": { "type": "string" },
38+
"server_major_ver": { "type": "string" },
39+
"server_minor_ver": { "type": "string" }
40+
}
41+
},
42+
"setting": {
43+
"type": "object",
44+
"additionalProperties": false,
45+
"required": ["setting", "unit", "category", "context", "vartype", "pretty_value"],
46+
"properties": {
47+
"setting": { "type": "string" },
48+
"unit": { "type": "string" },
49+
"category": { "type": "string" },
50+
"context": { "type": "string" },
51+
"vartype": { "type": "string" },
52+
"pretty_value": { "type": "string" }
53+
}
54+
},
55+
"data": {
56+
"type": "object",
57+
"additionalProperties": { "$ref": "#/$defs/setting" }
58+
},
59+
"nodeResult": {
60+
"type": "object",
61+
"additionalProperties": false,
62+
"required": ["data"],
63+
"properties": {
64+
"data": { "$ref": "#/$defs/data" },
65+
"postgres_version": { "$ref": "#/$defs/postgresVersion" }
66+
}
67+
}
68+
}
69+
}

tests/reporter/test_report_schemas.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,44 @@ def test_schema_a007(
127127
validate_report(report)
128128

129129

130+
@pytest.mark.unit
131+
def test_schema_s002(
132+
monkeypatch: pytest.MonkeyPatch,
133+
generator: PostgresReportGenerator,
134+
fixed_pg_version,
135+
prom_result,
136+
) -> None:
137+
monkeypatch.setattr(generator, "_get_postgres_version_info", lambda *args, **kwargs: fixed_pg_version)
138+
resp = prom_result(
139+
[
140+
{
141+
"metric": {
142+
"setting_name": "ssl",
143+
"setting_value": "on",
144+
"unit": "",
145+
"category": "Connections and Authentication / SSL",
146+
"context": "sighup",
147+
"vartype": "bool",
148+
}
149+
},
150+
{
151+
"metric": {
152+
"setting_name": "ssl_min_protocol_version",
153+
"setting_value": "TLSv1.2",
154+
"unit": "",
155+
"category": "Connections and Authentication / SSL",
156+
"context": "sighup",
157+
"vartype": "enum",
158+
}
159+
},
160+
]
161+
)
162+
monkeypatch.setattr(generator, "query_instant", lambda query: resp)
163+
164+
report = generator.generate_s002_ssl_tls_report("local", "node-1")
165+
validate_report(report)
166+
167+
130168
@pytest.mark.unit
131169
def test_schema_d004(
132170
monkeypatch: pytest.MonkeyPatch,

0 commit comments

Comments
 (0)