Merge branch 'swima'
[strongswan.git] / src / libimcv / swima / swima_collector.c
1 /*
2 * Copyright (C) 2017 Andreas Steffen
3 * HSR Hochschule fuer Technik Rapperswil
4 *
5 * This program is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License as published by the
7 * Free Software Foundation; either version 2 of the License, or (at your
8 * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
9 *
10 * This program is distributed in the hope that it will be useful, but
11 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
13 * for more details.
14 */
15
16 #include "swima_collector.h"
17
18 #include <collections/linked_list.h>
19 #include <bio/bio_writer.h>
20 #include <utils/debug.h>
21
22 #include <stdio.h>
23 #include <fcntl.h>
24 #include <unistd.h>
25 #include <sys/stat.h>
26 #include <libgen.h>
27 #include <errno.h>
28
29 #define SOURCE_ID_GENERATOR 1
30 #define SOURCE_ID_COLLECTOR 2
31
32 #define SWID_GENERATOR "/usr/local/bin/swid_generator"
33
34 /**
35 * Directories to be skipped by collector
36 */
37 static const char* skip_directories[] = {
38 "/usr/share/doc",
39 "/usr/share/help",
40 "/usr/share/icons",
41 "/usr/share/gnome/help"
42 };
43
44 typedef struct private_swima_collector_t private_swima_collector_t;
45
46 /**
47 * Private data of a swima_collector_t object.
48 *
49 */
50 struct private_swima_collector_t {
51
52 /**
53 * Public swima_collector_t interface.
54 */
55 swima_collector_t public;
56
57 /**
58 * Collect Software Identifiers only
59 */
60 bool sw_id_only;
61
62 /**
63 * Software Collector Database [if it exists]
64 */
65 database_t *db;
66
67 /**
68 * List of Software [Identifier] records
69 */
70 swima_inventory_t *inventory;
71
72 /**
73 * List of Software [Identifier] events
74 */
75 swima_events_t *events;
76
77 };
78
79 /**
80 * Extract Software Identifier from SWID tag
81 */
82 static status_t extract_sw_id(chunk_t swid_tag, chunk_t *sw_id)
83 {
84 char *pos, *tag, *tagid, *regid;
85 size_t len, tagid_len, regid_len;
86 status_t status = NOT_FOUND;
87
88 /* Copy at most 1023 bytes of the SWID tag and null-terminate it */
89 len = min(1023, swid_tag.len);
90 pos = tag = strndup(swid_tag.ptr, len);
91
92 tagid= strstr(pos, "tagId=\"");
93 if (tagid == NULL)
94 {
95 goto end;
96 }
97 tagid += 7;
98 len -= tagid - pos - 7;
99
100 pos = strchr(tagid, '"');
101 if (pos == NULL)
102 {
103 goto end;
104 }
105 tagid_len = pos - tagid;
106
107 regid= strstr(pos, "regid=\"");
108 if (regid == NULL)
109 {
110 goto end;
111 }
112 regid += 7;
113 len -= regid - pos - 7;
114
115 pos = strchr(regid, '"');
116 if (pos == NULL)
117 {
118 goto end;
119 }
120 regid_len = pos - regid;
121
122 *sw_id = chunk_cat("ccc", chunk_create(regid, regid_len),
123 chunk_from_chars('_','_'),
124 chunk_create(tagid, tagid_len));
125 status = SUCCESS;
126 end:
127 free(tag);
128
129 return status;
130 }
131
132 /**
133 * Read SWID tags issued by the swid_generator tool
134 */
135 static status_t read_swid_tags(private_swima_collector_t *this, FILE *file)
136 {
137 swima_record_t *sw_record;
138 bio_writer_t *writer;
139 chunk_t sw_id, swid_tag;
140 bool more_tags = TRUE, last_newline;
141 char line[8192];
142 size_t len;
143 status_t status;
144
145 while (more_tags)
146 {
147 last_newline = TRUE;
148 writer = bio_writer_create(512);
149 while (TRUE)
150 {
151 if (!fgets(line, sizeof(line), file))
152 {
153 more_tags = FALSE;
154 break;
155 }
156 len = strlen(line);
157
158 if (last_newline && line[0] == '\n')
159 {
160 break;
161 }
162 else
163 {
164 last_newline = (line[len-1] == '\n');
165 writer->write_data(writer, chunk_create(line, len));
166 }
167 }
168 swid_tag = writer->get_buf(writer);
169
170 if (swid_tag.len > 1)
171 {
172 /* remove trailing newline if present */
173 if (swid_tag.ptr[swid_tag.len - 1] == '\n')
174 {
175 swid_tag.len--;
176 }
177 DBG3(DBG_IMC, " %.*s", swid_tag.len, swid_tag.ptr);
178
179 status = extract_sw_id(swid_tag, &sw_id);
180 if (status != SUCCESS)
181 {
182 DBG1(DBG_IMC, "software id could not be extracted from tag");
183 writer->destroy(writer);
184 return status;
185 }
186 sw_record = swima_record_create(0, sw_id, chunk_empty);
187 sw_record->set_source_id(sw_record, SOURCE_ID_GENERATOR);
188 sw_record->set_record(sw_record, swid_tag);
189 this->inventory->add(this->inventory, sw_record);
190 chunk_free(&sw_id);
191 }
192 writer->destroy(writer);
193 }
194
195 return SUCCESS;
196 }
197
198 /**
199 * Read Software Identifiers issued by the swid_generator tool
200 */
201 static status_t read_swid_tag_ids(private_swima_collector_t *this, FILE *file)
202 {
203 swima_record_t *sw_record;
204 chunk_t sw_id;
205 char line[BUF_LEN];
206 size_t len;
207
208 while (TRUE)
209 {
210 if (!fgets(line, sizeof(line), file))
211 {
212 return SUCCESS;
213 }
214 len = strlen(line);
215
216 /* remove trailing newline if present */
217 if (len > 0 && line[len - 1] == '\n')
218 {
219 len--;
220 }
221 DBG3(DBG_IMC, " %.*s", len, line);
222
223 sw_id = chunk_create(line, len);
224 sw_record = swima_record_create(0, sw_id, chunk_empty);
225 sw_record->set_source_id(sw_record, SOURCE_ID_GENERATOR);
226 this->inventory->add(this->inventory, sw_record);
227 }
228 }
229
230 static status_t retrieve_inventory(private_swima_collector_t *this,
231 swima_inventory_t *targets)
232 {
233 char *name;
234 uint32_t record_id, source;
235 swima_record_t *sw_record;
236 chunk_t sw_id;
237 enumerator_t *e;
238
239 /* Retrieve complete software identifier inventory */
240 e = this->db->query(this->db,
241 "SELECT id, name, source FROM sw_identifiers WHERE installed = 1 "
242 "ORDER BY name ASC", DB_UINT, DB_TEXT, DB_UINT);
243 if (!e)
244 {
245 DBG1(DBG_IMC, "database query for installed sw_identifiers failed");
246 return FAILED;
247 }
248 while (e->enumerate(e, &record_id, &name, &source))
249 {
250 sw_id = chunk_from_str(name);
251 sw_record = swima_record_create(record_id, sw_id, chunk_empty);
252 sw_record->set_source_id(sw_record, source);
253 this->inventory->add(this->inventory, sw_record);
254 }
255 e->destroy(e);
256
257 return SUCCESS;
258 }
259
260 static status_t retrieve_events(private_swima_collector_t *this,
261 swima_inventory_t *targets)
262 {
263 enumerator_t *e;
264 char *name, *timestamp;
265 uint32_t record_id, source, action, eid, earliest_eid;
266 chunk_t sw_id, ev_ts;
267 swima_record_t *sw_record;
268 swima_event_t *sw_event;
269
270 earliest_eid = targets->get_eid(targets, NULL);
271
272 /* Retrieve complete software identifier inventory */
273 e = this->db->query(this->db,
274 "SELECT e.id, e.timestamp, i.id, i.name, i.source, s.action "
275 "FROM sw_events as s JOIN events AS e ON s.eid = e.id "
276 "JOIN sw_identifiers as i ON s.sw_id = i.id WHERE s.eid >= ?"
277 "ORDER BY s.eid, i.name, s.action ASC", DB_UINT, earliest_eid,
278 DB_UINT, DB_TEXT, DB_UINT, DB_TEXT, DB_UINT, DB_UINT);
279 if (!e)
280 {
281 DBG1(DBG_IMC, "database query for sw_events failed");
282 return FAILED;
283 }
284 while (e->enumerate(e, &eid, &timestamp, &record_id, &name, &source, &action))
285 {
286 sw_id = chunk_from_str(name);
287 ev_ts = chunk_from_str(timestamp);
288 sw_record = swima_record_create(record_id, sw_id, chunk_empty);
289 sw_record->set_source_id(sw_record, source);
290 sw_event = swima_event_create(eid, ev_ts, action, sw_record);
291 this->events->add(this->events, sw_event);
292 }
293 e->destroy(e);
294
295 return SUCCESS;
296 }
297
298 static status_t generate_tags(private_swima_collector_t *this, char *generator,
299 swima_inventory_t *targets, bool pretty, bool full)
300 {
301 FILE *file;
302 char command[BUF_LEN];
303 char doc_separator[] = "'\n\n'";
304
305 status_t status = SUCCESS;
306
307 if (targets->get_count(targets) == 0)
308 {
309 /* Assemble the SWID generator command */
310 if (this->sw_id_only)
311 {
312 snprintf(command, BUF_LEN, "%s software-id", generator);
313 }
314 else
315 {
316 snprintf(command, BUF_LEN, "%s swid --doc-separator %s%s%s",
317 generator, doc_separator, pretty ? " --pretty" : "",
318 full ? " --full" : "");
319 }
320
321 /* Open a pipe stream for reading the SWID generator output */
322 file = popen(command, "r");
323 if (!file)
324 {
325 DBG1(DBG_IMC, "failed to run swid_generator command");
326 return NOT_SUPPORTED;
327 }
328
329 if (this->sw_id_only)
330 {
331 DBG2(DBG_IMC, "SWID tag ID generation by package manager");
332 status = read_swid_tag_ids(this, file);
333 }
334 else
335 {
336 DBG2(DBG_IMC, "SWID tag generation by package manager");
337 status = read_swid_tags(this, file);
338 }
339 pclose(file);
340 }
341 else if (!this->sw_id_only)
342 {
343 swima_record_t *target;
344 enumerator_t *enumerator;
345 chunk_t sw_id;
346
347 enumerator = targets->create_enumerator(targets);
348 while (enumerator->enumerate(enumerator, &target))
349 {
350 sw_id = target->get_sw_id(target, NULL);
351
352 /* Assemble the SWID generator command */
353 snprintf(command, BUF_LEN, "%s swid --software-id %.*s%s%s",
354 generator, sw_id.len, sw_id.ptr,
355 pretty ? " --pretty" : "", full ? " --full" : "");
356
357 /* Open a pipe stream for reading the SWID generator output */
358 file = popen(command, "r");
359 if (!file)
360 {
361 DBG1(DBG_IMC, "failed to run swid_generator command");
362 return NOT_SUPPORTED;
363 }
364 status = read_swid_tags(this, file);
365 pclose(file);
366
367 if (status != SUCCESS)
368 {
369 break;
370 }
371 }
372 enumerator->destroy(enumerator);
373 }
374
375 return status;
376 }
377
378 static bool collect_tags(private_swima_collector_t *this, char *pathname,
379 swima_inventory_t *targets, bool is_swidtag_dir)
380 {
381 char *rel_name, *abs_name, *suffix, *pos;
382 chunk_t *swid_tag, sw_id, sw_locator;
383 swima_record_t *sw_record;
384 struct stat st;
385 bool success = FALSE, skip, is_new_swidtag_dir;
386 enumerator_t *enumerator;
387 int i;
388
389 enumerator = enumerator_create_directory(pathname);
390 if (!enumerator)
391 {
392 DBG1(DBG_IMC, "directory '%s' can not be opened, %s",
393 pathname, strerror(errno));
394 return FALSE;
395 }
396
397 while (enumerator->enumerate(enumerator, &rel_name, &abs_name, &st))
398 {
399 if (S_ISDIR(st.st_mode))
400 {
401 skip = FALSE;
402
403 for (i = 0; i < countof(skip_directories); i++)
404 {
405 if (streq(abs_name, skip_directories[i]))
406 {
407 skip = TRUE;
408 break;
409 }
410 }
411
412 if (skip)
413 {
414 continue;
415 }
416
417 is_new_swidtag_dir = streq(rel_name, "swidtag");
418 if (is_new_swidtag_dir)
419 {
420 DBG2(DBG_IMC, "entering %s", pathname);
421 }
422 if (!collect_tags(this, abs_name, targets, is_swidtag_dir ||
423 is_new_swidtag_dir))
424 {
425 goto end;
426 }
427 if (is_new_swidtag_dir)
428 {
429 DBG2(DBG_IMC, "leaving %s", pathname);
430 }
431 }
432
433 if (!is_swidtag_dir)
434 {
435 continue;
436 }
437
438 /* found a swidtag file? */
439 suffix = strstr(rel_name, ".swidtag");
440 if (!suffix)
441 {
442 continue;
443 }
444
445 /* load the swidtag file */
446 swid_tag = chunk_map(abs_name, FALSE);
447 if (!swid_tag)
448 {
449 DBG1(DBG_IMC, " opening '%s' failed: %s", abs_name,
450 strerror(errno));
451 goto end;
452 }
453
454 /* extract software identity from SWID tag */
455 if (extract_sw_id(*swid_tag, &sw_id) != SUCCESS)
456 {
457 DBG1(DBG_IMC, "software id could not be extracted from SWID tag");
458 chunk_unmap(swid_tag);
459 goto end;
460 }
461
462 /* In case of a targeted request */
463 if (targets->get_count(targets))
464 {
465 enumerator_t *target_enumerator;
466 swima_record_t *target;
467 bool match = FALSE;
468
469 target_enumerator = targets->create_enumerator(targets);
470 while (target_enumerator->enumerate(target_enumerator, &target))
471 {
472 if (chunk_equals(target->get_sw_id(target, NULL), sw_id))
473 {
474 match = TRUE;
475 break;
476 }
477 }
478 target_enumerator->destroy(target_enumerator);
479
480 if (!match)
481 {
482 chunk_unmap(swid_tag);
483 chunk_free(&sw_id);
484 continue;
485 }
486 }
487 DBG2(DBG_IMC, " %s", rel_name);
488
489 pos = strstr(pathname, "/swidtag");
490 sw_locator = pos ? chunk_create(pathname, pos - pathname) : chunk_empty;
491 sw_record = swima_record_create(0, sw_id, sw_locator);
492 sw_record->set_source_id(sw_record, SOURCE_ID_COLLECTOR);
493 if (!this->sw_id_only)
494 {
495 sw_record->set_record(sw_record, *swid_tag);
496 }
497 this->inventory->add(this->inventory, sw_record);
498 chunk_unmap(swid_tag);
499 chunk_free(&sw_id);
500 }
501 success = TRUE;
502
503 end:
504 enumerator->destroy(enumerator);
505
506 return success;
507 }
508
509 METHOD(swima_collector_t, collect_inventory, swima_inventory_t*,
510 private_swima_collector_t *this, bool sw_id_only, swima_inventory_t *targets)
511 {
512 char *directory, *generator;
513 bool pretty, full;
514 status_t status;
515
516 directory = lib->settings->get_str(lib->settings,
517 "%s.plugins.imc-swima.swid_directory",
518 SWID_DIRECTORY, lib->ns);
519 generator = lib->settings->get_str(lib->settings,
520 "%s.plugins.imc-swima.swid_generator",
521 SWID_GENERATOR, lib->ns);
522 pretty = lib->settings->get_bool(lib->settings,
523 "%s.plugins.imc-swima.swid_pretty",
524 FALSE, lib->ns);
525 full = lib->settings->get_bool(lib->settings,
526 "%s.plugins.imc-swima.swid_full",
527 FALSE, lib->ns);
528
529 /**
530 * Re-initialize collector
531 */
532 this->sw_id_only = sw_id_only;
533 this->inventory->clear(this->inventory);
534
535 /**
536 * Source 1: Tags are generated by a package manager
537 */
538 if (sw_id_only && this->db)
539 {
540 status = retrieve_inventory(this, targets);
541 }
542 else
543 {
544 status = generate_tags(this, generator, targets, pretty, full);
545 }
546
547 /**
548 * Source 2: Collect swidtag files by iteratively entering all
549 * directories in the tree under the "directory" path.
550 */
551 collect_tags(this, directory, targets, FALSE);
552
553 return status == SUCCESS ? this->inventory : NULL;
554 }
555
556 METHOD(swima_collector_t, collect_events, swima_events_t*,
557 private_swima_collector_t *this, bool sw_id_only, swima_inventory_t *targets)
558 {
559 if (!sw_id_only || !this->db)
560 {
561 return NULL;
562 }
563
564 /**
565 * Re-initialize collector
566 */
567 this->sw_id_only = sw_id_only;
568 this->events->clear(this->events);
569
570 return retrieve_events(this, targets) == SUCCESS ? this->events : NULL;
571 }
572
573 METHOD(swima_collector_t, destroy, void,
574 private_swima_collector_t *this)
575 {
576 DESTROY_IF(this->db);
577 this->inventory->destroy(this->inventory);
578 this->events->destroy(this->events);
579 free(this);
580 }
581
582 /**
583 * See header
584 */
585 swima_collector_t *swima_collector_create(void)
586 {
587 private_swima_collector_t *this;
588 char *database;
589 uint32_t last_eid = 1, eid_epoch = 0x11223344;
590
591 INIT(this,
592 .public = {
593 .collect_inventory = _collect_inventory,
594 .collect_events = _collect_events,
595 .destroy = _destroy,
596 },
597 .inventory = swima_inventory_create(),
598 .events = swima_events_create(),
599 );
600
601 database = lib->settings->get_str(lib->settings,
602 "%s.plugins.imc-swima.swid_database", NULL, lib->ns);
603
604 /* If we have an URI, try to connect to sw_collector database */
605 if (database)
606 {
607 database_t *db = lib->db->create(lib->db, database);
608
609 if (db)
610 {
611 enumerator_t *e;
612
613 /* Get last event ID and corresponding epoch */
614 e = db->query(db,
615 "SELECT id, epoch FROM events ORDER BY timestamp DESC",
616 DB_UINT, DB_UINT);
617 if (!e || !e->enumerate(e, &last_eid, &eid_epoch))
618 {
619 DBG1(DBG_IMC, "database query for last event failed");
620 DESTROY_IF(e);
621 db->destroy(db);
622 }
623 else
624 {
625 /* The query worked, attach collector database permanently */
626 e->destroy(e);
627 this->db = db;
628 }
629 }
630 else
631 {
632 DBG1(DBG_IMC, "opening sw-collector database URI '%s' failed",
633 database);
634 }
635 }
636 if (!this->db)
637 {
638 /* Set the event ID epoch and last event ID smanually */
639 eid_epoch = lib->settings->get_int(lib->settings,
640 "%s.plugins.imc-swima.eid_epoch",
641 eid_epoch, lib->ns);
642 }
643 this->inventory->set_eid(this->inventory, last_eid, eid_epoch);
644 this->events->set_eid(this->events, last_eid, eid_epoch);
645
646 return &this->public;
647 }