master
c 505 lines 12.9 KB
Raw
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include <unistd.h>
5 #include <stdbool.h>
6
7 #define MAX_SEARCH 3
8 #define MAX_PARAMETERS 128
9 #define ERROR_BUFFER_SIZE 1024
10
11 struct command {
12 const char *name;
13 const char *params;
14 const char *search[MAX_SEARCH];
15 } allowed_commands[] = {
16 {
17 .name = "ethtool-module-info",
18 .params = "-m {{devname}}",
19 .search =
20 {
21 [0] = "ethtool",
22 [1] = NULL,
23 },
24 },
25 {
26 .name = "chronyc-serverstats",
27 .params = "serverstats",
28 .search =
29 {
30 [0] = "chronyc",
31 [1] = NULL,
32 },
33 },
34 {
35 .name = "varnishadm-backend-list",
36 .params = "backend.list",
37 .search =
38 {
39 [0] = "varnishadm",
40 [1] = NULL,
41 },
42 },
43 {
44 .name = "varnishstat-stats",
45 .params = "-1 -t off -n {{instanceName}}",
46 .search =
47 {
48 [0] = "varnishstat",
49 [1] = NULL,
50 },
51 },
52 {
53 .name = "smbstatus-profile",
54 .params = "-P",
55 .search =
56 {
57 [0] = "smbstatus",
58 [1] = NULL,
59 },
60 },
61 {
62 .name = "exim-bpc",
63 .params = "-bpc",
64 .search =
65 {
66 [0] = "exim",
67 [1] = NULL,
68 },
69 },
70 {
71 .name = "nsd-control-stats",
72 .params = "stats_noreset",
73 .search = {
74 [0] = "nsd-control",
75 [1] = NULL,
76 },
77 },
78 {
79 .name = "chronyc-serverstats",
80 .params = "serverstats",
81 .search = {
82 [0] = "chronyc",
83 [1] = NULL,
84 },
85 },
86 {
87 .name = "dmsetup-status-cache",
88 .params = "status --target cache --noflush",
89 .search = {
90 [0] = "dmsetup",
91 [1] = NULL,
92 },
93 },
94 {
95 .name = "ssacli-controllers-info",
96 .params = "ctrl all show config detail",
97 .search = {
98 [0] = "ssacli",
99 [1] = NULL,
100 },
101 },
102 {
103 .name = "smartctl-json-scan",
104 .params = "--json --scan",
105 .search = {
106 [0] = "smartctl",
107 [1] = NULL,
108 },
109 },
110 {
111 .name = "smartctl-json-scan-open",
112 .params = "--json --scan-open",
113 .search = {
114 [0] = "smartctl",
115 [1] = NULL,
116 },
117 },
118 {
119 .name = "smartctl-json-device-info",
120 .params = "--json --all {{deviceName}} --device {{deviceType}} --nocheck {{powerMode}}",
121 .search = {
122 [0] = "smartctl",
123 [1] = NULL,
124 },
125 },
126 {
127 .name = "fail2ban-client-status",
128 .params = "status",
129 .search = {
130 [0] = "fail2ban-client",
131 [1] = NULL,
132 },
133 },
134 {
135 .name = "fail2ban-client-status-socket",
136 .params = "-s {{socket_path}} status",
137 .search = {
138 [0] = "fail2ban-client",
139 [1] = NULL,
140 },
141 },
142 {
143 .name = "fail2ban-client-status-jail",
144 .params = "status {{jail}}",
145 .search = {
146 [0] = "fail2ban-client",
147 [1] = NULL,
148 },
149 },
150 {
151 .name = "fail2ban-client-status-jail-socket",
152 .params = "-s {{socket_path}} status {{jail}}",
153 .search = {
154 [0] = "fail2ban-client",
155 [1] = NULL,
156 },
157 },
158 {
159 .name = "storcli-controllers-info",
160 .params = "/cALL show all J nolog",
161 .search = {
162 [0] = "storcli",
163 [1] = NULL,
164 },
165 },
166 {
167 .name = "storcli-drives-info",
168 .params = "/cALL/eALL/sALL show all J nolog",
169 .search = {
170 [0] = "storcli",
171 [1] = NULL,
172 },
173 },
174 {
175 .name = "lvs-report-json",
176 .params = "--reportformat json --units b --nosuffix -o {{options}}",
177 .search = {
178 [0] = "lvs",
179 [1] = NULL,
180 },
181 },
182 {
183 .name = "igt-list-gpus",
184 .params = "-L",
185 .search = {
186 [0] = "intel_gpu_top",
187 [1] = NULL,
188 },
189 },
190 {
191 .name = "igt-device-json",
192 .params = "-d {{device}} -J -s {{interval}}",
193 .search = {
194 [0] = "intel_gpu_top",
195 [1] = NULL,
196 },
197 },
198 {
199 .name = "igt-json",
200 .params = "-J -s {{interval}}",
201 .search = {
202 [0] = "intel_gpu_top",
203 [1] = NULL,
204 },
205 },
206 {
207 .name = "nvme-list",
208 .params = "list --output-format=json",
209 .search = {
210 [0] = "nvme",
211 [1] = NULL,
212 },
213 },
214 {
215 .name = "nvme-smart-log",
216 .params = "smart-log {{device}} --output-format=json",
217 .search = {
218 [0] = "nvme",
219 [1] = NULL,
220 },
221 },
222 {
223 .name = "megacli-disk-info",
224 .params = "-LDPDInfo -aAll -NoLog",
225 .search = {
226 [0] = "megacli",
227 [1] = "MegaCli",
228 [2] = "MegaCli64",
229 },
230 },
231 {
232 .name = "megacli-battery-info",
233 .params = "-AdpBbuCmd -aAll -NoLog",
234 .search = {
235 [0] = "megacli",
236 [1] = "MegaCli",
237 [2] = "MegaCli64",
238 },
239 },
240 {
241 .name = "arcconf-ld-info",
242 .params = "GETCONFIG 1 LD",
243 .search = {
244 [0] = "arcconf",
245 [1] = NULL,
246 },
247 },
248 {
249 .name = "arcconf-pd-info",
250 .params = "GETCONFIG 1 PD",
251 .search = {
252 [0] = "arcconf",
253 [1] = NULL,
254 },
255 }
256 };
257
258 bool command_exists_in_dir(const char *dir, const char *cmd, char *dst, size_t dst_size) {
259 snprintf(dst, dst_size, "%s/%s", dir, cmd);
260 return access(dst, X_OK) == 0;
261 }
262
263 bool command_exists_in_PATH(const char *cmd, char *dst, size_t dst_size) {
264 if(!dst || !dst_size)
265 return false;
266
267 char *path = getenv("PATH");
268 if(!path)
269 return false;
270
271 char *path_copy = strdup(path);
272 if (!path_copy)
273 return false;
274
275 char *dir;
276 bool found = false;
277 dir = strtok(path_copy, ":");
278 while(dir && !found) {
279 found = command_exists_in_dir(dir, cmd, dst, dst_size);
280 dir = strtok(NULL, ":");
281 }
282
283 free(path_copy);
284 return found;
285 }
286
287 struct command *find_command(const char *cmd) {
288 size_t size = sizeof(allowed_commands) / sizeof(allowed_commands[0]);
289 for(size_t i = 0; i < size ;i++) {
290 if(strcmp(cmd, allowed_commands[i].name) == 0)
291 return &allowed_commands[i];
292 }
293
294 return NULL;
295 }
296
297 bool check_string(const char *str, size_t index, char *err, size_t err_size) {
298 const char *s = str;
299 while(*s) {
300 char c = *s++;
301 if(!((c >= 'A' && c <= 'Z') ||
302 (c >= 'a' && c <= 'z') ||
303 (c >= '0' && c <= '9') ||
304 c == ' ' || c == '_' || c == '-' || c == '/' ||
305 c == '.' || c == ',' || c == ':' || c == '=')) {
306 snprintf(err, err_size, "command line argument No %zu includes invalid character '%c'", index, c);
307 return false;
308 }
309 }
310
311 return true;
312 }
313
314 bool check_params(int argc, char **argv, char *err, size_t err_size) {
315 for(int i = 0 ; i < argc ;i++)
316 if(!check_string(argv[i], i, err, err_size))
317 return false;
318
319 return true;
320 }
321
322 char *find_variable_in_argv(const char *variable, int argc, char **argv, char *err, size_t err_size) {
323 for (int i = 1; i < argc - 1; i++) {
324 if (strcmp(argv[i], variable) == 0)
325 return strdup(argv[i + 1]);
326 }
327
328 snprintf(err, err_size, "variable '%s' is required, but was not provided in the command line parameters", variable);
329
330 return NULL;
331 }
332
333 bool search_and_replace_params(struct command *cmd, char **params, size_t max_params, const char *filename, int argc, char **argv, char *err, size_t err_size) {
334 if (!cmd || !params || !max_params) {
335 snprintf(err, err_size, "search_and_replace_params() internal error");
336 return false;
337 }
338
339 const char *delim = " ";
340 char *token;
341 char *temp_params = strdup(cmd->params);
342 if (!temp_params) {
343 snprintf(err, err_size, "search_and_replace_params() cannot allocate memory");
344 return false;
345 }
346
347 size_t param_count = 0;
348 params[param_count++] = strdup(filename);
349
350 token = strtok(temp_params, delim);
351 while (token && param_count < max_params - 1) {
352 size_t len = strlen(token);
353
354 char *value = NULL;
355
356 if (strncmp(token, "{{", 2) == 0 && strncmp(token + len - 2, "}}", 2) == 0) {
357 token[0] = '-';
358 token[1] = '-';
359 token[len - 2] = '\0';
360
361 value = find_variable_in_argv(token, argc, argv, err, err_size);
362 }
363 else
364 value = strdup(token);
365
366 if(!value)
367 goto cleanup;
368
369 params[param_count++] = value;
370 token = strtok(NULL, delim);
371 }
372
373 params[param_count] = NULL; // Null-terminate the params array
374 free(temp_params);
375 return true;
376
377 cleanup:
378 if(!err[0])
379 snprintf(err, err_size, "memory allocation failure");
380
381 free(temp_params);
382 for (size_t i = 0; i < param_count; ++i) {
383 free(params[i]);
384 params[i] = NULL;
385 }
386 return false;
387 }
388
389 void show_help() {
390 fprintf(stdout, "\n");
391 fprintf(stdout, "ndsudo\n");
392 fprintf(stdout, "\n");
393 fprintf(stdout, "Copyright 2018-2025 Netdata Inc.\n");
394 fprintf(stdout, "\n");
395 fprintf(stdout, "A helper to allow Netdata run privileged commands.\n");
396 fprintf(stdout, "\n");
397 fprintf(stdout, " --test\n");
398 fprintf(stdout, " print the generated command that will be run, without running it.\n");
399 fprintf(stdout, "\n");
400 fprintf(stdout, " --help\n");
401 fprintf(stdout, " print this message.\n");
402 fprintf(stdout, "\n");
403
404 fprintf(stdout, "The following commands are supported:\n\n");
405
406 size_t size = sizeof(allowed_commands) / sizeof(allowed_commands[0]);
407 for(size_t i = 0; i < size ;i++) {
408 fprintf(stdout, "- Command : %s\n", allowed_commands[i].name);
409 fprintf(stdout, " Executables: ");
410 for(size_t j = 0; j < MAX_SEARCH && allowed_commands[i].search[j] ;j++) {
411 fprintf(stdout, "%s ", allowed_commands[i].search[j]);
412 }
413 fprintf(stdout, "\n");
414 fprintf(stdout, " Parameters : %s\n\n", allowed_commands[i].params);
415 }
416
417 fprintf(stdout, "The program searches for executables in the system path.\n");
418 fprintf(stdout, "\n");
419 fprintf(stdout, "Variables given as {{variable}} are expected on the command line as:\n");
420 fprintf(stdout, " --variable VALUE\n");
421 fprintf(stdout, "\n");
422 fprintf(stdout, "VALUE can include space, A-Z, a-z, 0-9, _, -, /, and .\n");
423 fprintf(stdout, "\n");
424 }
425
426 int main(int argc, char *argv[]) {
427 char error_buffer[ERROR_BUFFER_SIZE] = "";
428
429 if (argc < 2) {
430 fprintf(stderr, "at least 2 parameters are needed, but %d were given.\n", argc);
431 return 1;
432 }
433
434 if(!check_params(argc, argv, error_buffer, sizeof(error_buffer))) {
435 fprintf(stderr, "invalid characters in parameters: %s\n", error_buffer);
436 return 2;
437 }
438
439 bool test = false;
440 const char *cmd = argv[1];
441 if(strcmp(cmd, "--help") == 0 || strcmp(cmd, "-h") == 0) {
442 show_help();
443 exit(0);
444 }
445 else if(strcmp(cmd, "--test") == 0) {
446 cmd = argv[2];
447 test = true;
448 }
449
450 struct command *command = find_command(cmd);
451 if(!command) {
452 fprintf(stderr, "command not recognized: %s\n", cmd);
453 return 3;
454 }
455
456 char new_path[] = "PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin";
457 putenv(new_path);
458
459 setuid(0);
460 setgid(0);
461 setegid(0);
462
463 bool found = false;
464 char filename[FILENAME_MAX];
465
466 for(size_t i = 0; i < MAX_SEARCH && !found ;i++) {
467 if(command->search[i]) {
468 found = command_exists_in_PATH(command->search[i], filename, sizeof(filename));
469 if(!found) {
470 size_t len = strlen(error_buffer);
471 snprintf(&error_buffer[len], sizeof(error_buffer) - len, "%s ", command->search[i]);
472 }
473 }
474 }
475
476 if(!found) {
477 fprintf(stderr, "%s: not available in PATH.\n", error_buffer);
478 return 4;
479 }
480 else
481 error_buffer[0] = '\0';
482
483 char *params[MAX_PARAMETERS];
484 if(!search_and_replace_params(command, params, MAX_PARAMETERS, filename, argc, argv, error_buffer, sizeof(error_buffer))) {
485 fprintf(stderr, "command line parameters are not satisfied: %s\n", error_buffer);
486 return 5;
487 }
488
489 if(test) {
490 fprintf(stderr, "Command to run: \n");
491
492 for(size_t i = 0; i < MAX_PARAMETERS && params[i] ;i++)
493 fprintf(stderr, "'%s' ", params[i]);
494
495 fprintf(stderr, "\n");
496
497 exit(0);
498 }
499 else {
500 char *clean_env[] = {NULL};
501 execve(filename, params, clean_env);
502 perror("execve"); // execve only returns on error
503 return 6;
504 }
505 }