Skip to content

Commit ccdf7bf

Browse files
committed
Merge branch 'develop'
2 parents a9170d7 + c212ddc commit ccdf7bf

File tree

3 files changed

+76
-34
lines changed

3 files changed

+76
-34
lines changed

glances/config.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,19 @@
1717
from glances.globals import BSD, LINUX, MACOS, SUNOS, WINDOWS, ConfigParser, NoOptionError, NoSectionError, system_exec
1818
from glances.logger import logger
1919

20+
# Sections entirely blocked from the secure view
21+
_SECURE_BLOCKED_SECTIONS = frozenset(
22+
{
23+
"passwords",
24+
}
25+
)
26+
27+
# Key name patterns redacted in any section
28+
_SECURE_SENSITIVE_KEY_RE = re.compile(
29+
r"password|token|secret|api_key|apikey|ssl_keyfile",
30+
re.IGNORECASE,
31+
)
32+
2033

2134
def user_config_dir():
2235
r"""Return a list of per-user config dir (full path).
@@ -286,6 +299,22 @@ def as_dict(self):
286299
dictionary[section][option] = self.parser.get(section, option)
287300
return dictionary
288301

302+
def as_dict_secure(self):
303+
"""Return a sanitised copy of the configuration dict.
304+
305+
Intended for unauthenticated API access.
306+
- Blocked sections are omitted entirely.
307+
- Sensitive keys in remaining sections are replaced by '********'.
308+
"""
309+
sanitized = {}
310+
for section, options in self.as_dict().items():
311+
if section in _SECURE_BLOCKED_SECTIONS:
312+
continue
313+
sanitized[section] = {
314+
key: "********" if _SECURE_SENSITIVE_KEY_RE.search(key) else value for key, value in options.items()
315+
}
316+
return sanitized
317+
289318
def sections(self):
290319
"""Return a list of all sections."""
291320
return self.parser.sections()

glances/exports/glances_timescaledb/__init__.py

Lines changed: 44 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010

1111
import sys
1212
import time
13+
from datetime import datetime, timezone
1314
from platform import node
1415

1516
import psycopg
17+
from psycopg import sql
1618

1719
from glances.exports.export import GlancesExport
1820
from glances.logger import logger
@@ -77,20 +79,13 @@ def init(self):
7779
return db
7880

7981
def normalize(self, value):
80-
"""Normalize the value to be exportable to TimescaleDB."""
81-
if value is None:
82-
return 'NULL'
83-
if isinstance(value, bool):
84-
return str(value).upper()
82+
"""Normalize the value for use in a parameterized psycopg query (returns raw Python value)."""
8583
if isinstance(value, (list, tuple)):
8684
# Special case for list of one boolean
8785
if len(value) == 1 and isinstance(value[0], bool):
88-
return str(value[0]).upper()
89-
return ', '.join([f"'{v}'" for v in value])
90-
if isinstance(value, str):
91-
return f"'{value}'"
92-
93-
return f"{value}"
86+
return value[0]
87+
return ', '.join(str(v) for v in value)
88+
return value # None → NULL, bool/str/int/float handled natively by psycopg
9489

9590
def update(self, stats):
9691
"""Update the TimescaleDB export module."""
@@ -137,8 +132,8 @@ def update(self, stats):
137132
segmented_by.extend(['hostname_id']) # Segment by hostname
138133
for key, value in plugin_stats.items():
139134
creation_list.append(f"{key} {convert_types[type(value).__name__]} NULL")
140-
values_list.append('NOW()') # Add the current time (insertion time)
141-
values_list.append(f"'{self.hostname}'") # Add the hostname
135+
values_list.append(datetime.now(timezone.utc)) # Add the current time (insertion time)
136+
values_list.append(self.hostname) # Add the hostname
142137
values_list.extend([self.normalize(value) for value in plugin_stats.values()])
143138
values_list = [values_list]
144139
elif isinstance(plugin_stats, list) and len(plugin_stats) > 0 and 'key' in plugin_stats[0]:
@@ -153,9 +148,9 @@ def update(self, stats):
153148
# Create the values list (it is a list of list to have a single datamodel for all the plugins)
154149
for plugin_item in plugin_stats:
155150
item_list = []
156-
item_list.append('NOW()') # Add the current time (insertion time)
157-
item_list.append(f"'{self.hostname}'") # Add the hostname
158-
item_list.append(f"'{plugin_item.get('key')}'")
151+
item_list.append(datetime.now(timezone.utc)) # Add the current time (insertion time)
152+
item_list.append(self.hostname) # Add the hostname
153+
item_list.append(plugin_item.get('key'))
159154
item_list.extend([self.normalize(value) for value in plugin_item.values()])
160155
values_list.append(item_list[:-1])
161156
else:
@@ -175,34 +170,52 @@ def export(self, plugin, creation_list, segmented_by, values_list):
175170

176171
with self.client.cursor() as cur:
177172
# Is the table exists?
178-
cur.execute(f"select exists(select * from information_schema.tables where table_name='{plugin}')")
173+
cur.execute(
174+
"SELECT EXISTS(SELECT * FROM information_schema.tables WHERE table_name=%s)",
175+
[plugin],
176+
)
179177
if not cur.fetchone()[0]:
180178
# Create the table if it does not exist
181179
# https://github.com/timescale/timescaledb/blob/main/README.md#create-a-hypertable
182-
# Execute the create table query
183-
create_query = f"""
184-
CREATE TABLE {plugin} (
185-
{', '.join(creation_list)}
186-
)
187-
WITH (
188-
timescaledb.hypertable,
189-
timescaledb.partition_column='time',
190-
timescaledb.segmentby = '{", ".join(segmented_by)}'
191-
);"""
180+
# Build CREATE TABLE using sql.Identifier for column names (prevents injection)
181+
# Each item in creation_list is "colname TYPE [NULL|NOT NULL]"
182+
fields = sql.SQL(', ').join(
183+
sql.SQL("{} {}").format(
184+
sql.Identifier(item.split(' ')[0]),
185+
sql.SQL(' '.join(item.split(' ')[1:]))
186+
)
187+
for item in creation_list
188+
)
189+
create_query = sql.SQL(
190+
"CREATE TABLE {table} ({fields}) WITH ("
191+
"timescaledb.hypertable, "
192+
"timescaledb.partition_column='time', "
193+
"timescaledb.segmentby = {segmentby});"
194+
).format(
195+
table=sql.Identifier(plugin),
196+
fields=fields,
197+
segmentby=sql.Literal(', '.join(segmented_by)),
198+
)
192199
logger.debug(f"Create table: {create_query}")
193200
try:
194201
cur.execute(create_query)
195202
except Exception as e:
196203
logger.error(f"Cannot create table {plugin}: {e}")
197204
return
198205

199-
# Insert the data
206+
# Insert the data using parameterized queries (prevents injection)
200207
# https://github.com/timescale/timescaledb/blob/main/README.md#insert-and-query-data
201-
insert_list = [f"({','.join(i)})" for i in values_list]
202-
insert_query = f"INSERT INTO {plugin} VALUES {','.join(insert_list)};"
208+
col_names = [item.split(' ')[0] for item in creation_list]
209+
cols = sql.SQL(', ').join(sql.Identifier(c) for c in col_names)
210+
placeholders = sql.SQL(', ').join(sql.Placeholder() for _ in col_names)
211+
insert_query = sql.SQL("INSERT INTO {table} ({cols}) VALUES ({vals})").format(
212+
table=sql.Identifier(plugin),
213+
cols=cols,
214+
vals=placeholders,
215+
)
203216
logger.debug(f"Insert data into table: {insert_query}")
204217
try:
205-
cur.execute(insert_query)
218+
cur.executemany(insert_query, values_list)
206219
except Exception as e:
207220
logger.error(f"Cannot insert data into table {plugin}: {e}")
208221
return

glances/outputs/glances_restful_api.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1165,7 +1165,7 @@ def _api_config(self):
11651165
"""
11661166
try:
11671167
# Get the RAW value of the config' dict
1168-
args_json = self.config.as_dict()
1168+
args_json = self.config.as_dict() if self.args.password else self.config.as_dict_secure()
11691169
except Exception as e:
11701170
raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get config ({str(e)})")
11711171
else:
@@ -1179,7 +1179,7 @@ def _api_config_section(self, section: str):
11791179
HTTP/400 if item is not found
11801180
HTTP/404 if others error
11811181
"""
1182-
config_dict = self.config.as_dict()
1182+
config_dict = self.config.as_dict() if self.args.password else self.config.as_dict_secure()
11831183
if section not in config_dict:
11841184
raise HTTPException(status.HTTP_400_BAD_REQUEST, f"Unknown configuration item {section}")
11851185

@@ -1199,7 +1199,7 @@ def _api_config_section_item(self, section: str, item: str):
11991199
HTTP/400 if item is not found
12001200
HTTP/404 if others error
12011201
"""
1202-
config_dict = self.config.as_dict()
1202+
config_dict = self.config.as_dict() if self.args.password else self.config.as_dict_secure()
12031203
if section not in config_dict:
12041204
raise HTTPException(status.HTTP_400_BAD_REQUEST, f"Unknown configuration item {section}")
12051205

0 commit comments

Comments
 (0)