2
0
mirror of https://gitlab.com/apparmor/apparmor synced 2025-08-22 10:07:12 +00:00
apparmor/binutils/aa_status.c
John Johansen ed62eeba75 aa-status: convert to using options instead of directly calling fns
In preparation for new options and controls, split detailed_output()
into detailed_profiles() and detailed_processes() and This switch the
output processing to one larger pass that uses option flags.

This doesn't change current behavior and is in not as clean as the
current code for the current option set. However it will allow us to
easily add new options in one place and have them apply correctly
every where.

Signed-off-by: John Johansen <john.johansen@canonical.com>
2023-05-02 02:38:43 -07:00

860 lines
22 KiB
C

/*
* Copyright (C) 2020 Canonical Ltd.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of version 2 of the GNU General Public
* License published by the Free Software Foundation.
*/
#define _GNU_SOURCE /* for asprintf() */
#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#include <errno.h>
#include <ctype.h>
#include <dirent.h>
#include <sys/apparmor.h>
#include <sys/apparmor_private.h>
#include "cJSON.h"
#define autofree __attribute((cleanup(_aa_autofree)))
#define autofclose __attribute((cleanup(_aa_autofclose)))
#define AA_EXIT_ENABLED 0
#define AA_EXIT_DISABLED 1
#define AA_EXIT_NO_POLICY 2
#define AA_EXIT_NO_CONTROL 3
#define AA_EXIT_NO_PERM 4
#define AA_EXIT_INTERNAL_ERROR 42
/* NOTE: Increment this whenever the JSON format changes */
static const unsigned char aa_status_json_version[] = "2";
#define ARRAY_SIZE(a) (sizeof(a) / sizeof(a[0]))
#define __unused __attribute__ ((__unused__))
struct profile {
char *name;
char *status;
};
static void free_profiles(struct profile *profiles, size_t n) {
if (!profiles)
return;
while (n > 0) {
n--;
free(profiles[n].name);
free(profiles[n].status);
}
free(profiles);
}
struct process {
char *pid;
char *profile;
char *exe;
char *mode;
};
static void free_processes(struct process *processes, size_t n) {
if (!processes)
return;
while (n > 0) {
n--;
free(processes[n].pid);
free(processes[n].profile);
free(processes[n].exe);
free(processes[n].mode);
}
free(processes);
}
#define SHOW_PROFILES 1
#define SHOW_PROCESSES 2
static int verbose = 0;
int opt_show = SHOW_PROFILES | SHOW_PROCESSES;
bool opt_json = false;
bool opt_pretty = false;
bool opt_count = false;
const char *opt_mode = NULL;
const char *profile_statuses[] = {"enforce", "complain", "kill", "unconfined"};
const char *process_statuses[] = {"enforce", "complain", "kill", "unconfined", "mixed"};
#define dprintf(...) \
do { \
if (verbose) \
printf(__VA_ARGS__); \
} while (0)
#define dfprintf(...) \
do { \
if (verbose) \
fprintf(__VA_ARGS__); \
} while (0)
static int open_profiles(FILE **fp)
{
autofree char *apparmorfs = NULL;
autofree char *apparmor_profiles = NULL;
struct stat st;
int ret;
ret = stat("/sys/module/apparmor", &st);
if (ret != 0) {
dfprintf(stderr, "apparmor not present.\n");
return AA_EXIT_DISABLED;
}
dprintf("apparmor module is loaded.\n");
ret = aa_find_mountpoint(&apparmorfs);
if (ret == -1) {
dfprintf(stderr, "apparmor filesystem is not mounted.\n");
return AA_EXIT_NO_CONTROL;
}
apparmor_profiles = malloc(strlen(apparmorfs) + 10); // /profiles\0
if (apparmor_profiles == NULL) {
return AA_EXIT_INTERNAL_ERROR;
}
sprintf(apparmor_profiles, "%s/profiles", apparmorfs);
*fp = fopen(apparmor_profiles, "r");
if (*fp == NULL) {
if (errno == EACCES) {
dfprintf(stderr, "You do not have enough privilege to read the profile set.\n");
} else {
dfprintf(stderr, "Could not open %s: %s", apparmor_profiles, strerror(errno));
}
return AA_EXIT_NO_PERM;
}
return 0;
}
/**
* get_profiles - get a listing of profiles on the system
* @fp: opened apparmor profiles file
* @profiles: return: list of profiles
* @n: return: number of elements in @profiles
*
* Return: 0 on success, shell error on failure
*/
static int get_profiles(FILE *fp, struct profile **profiles, size_t *n) {
autofree char *line = NULL;
size_t len = 0;
*profiles = NULL;
*n = 0;
while (getline(&line, &len, fp) != -1) {
struct profile *_profiles;
autofree char *status = NULL;
autofree char *name = NULL;
char *tmpname = aa_splitcon(line, &status);
if (!tmpname) {
dfprintf(stderr, "Error: failed profile name split of '%s'.\n", line);
// skip this entry and keep processing
// else would be AA_EXIT_INTERNAL_ERROR;
continue;
}
name = strdup(tmpname);
if (status)
status = strdup(status);
// give up if out of memory
if (name == NULL || status == NULL)
goto err;
_profiles = realloc(*profiles, (*n + 1) * sizeof(**profiles));
if (_profiles == NULL)
goto err;
// steal name and status
_profiles[*n].name = name;
_profiles[*n].status = status;
name = NULL;
status = NULL;
*n = *n + 1;
*profiles = _profiles;
}
return *n > 0 ? AA_EXIT_ENABLED : AA_EXIT_NO_POLICY;
err:
free_profiles(*profiles, *n);
*profiles = NULL;
*n = 0;
return AA_EXIT_INTERNAL_ERROR;
}
static int compare_profiles(const void *a, const void *b) {
return strcmp(((struct profile *)a)->name,
((struct profile *)b)->name);
}
/**
* filter_profiles - create a filtered profile list
* @profiles: list of profiles
* @n: number of elements in @profiles
* @filter: string to match against profile mode, if NULL no filter
* @filtered: return: new list of profiles that match the filter
* @nfiltered: return: number of elements in @filtered
*
* Return: 0 on success, shell error on failure
*/
static int filter_profiles(struct profile *profiles,
size_t n,
const char *filter,
struct profile **filtered,
size_t *nfiltered)
{
int ret = 0;
size_t i;
*filtered = NULL;
*nfiltered = 0;
for (i = 0; i < n; i++) {
if (filter == NULL || strcmp(profiles[i].status, filter) == 0) {
struct profile *_filtered = realloc(*filtered, (*nfiltered + 1) * sizeof(**filtered));
if (_filtered == NULL) {
free_profiles(*filtered, *nfiltered);
*filtered = NULL;
*nfiltered = 0;
ret = AA_EXIT_INTERNAL_ERROR;
break;
}
_filtered[*nfiltered].name = strdup(profiles[i].name);
_filtered[*nfiltered].status = strdup(profiles[i].status);
*filtered = _filtered;
*nfiltered = *nfiltered + 1;
}
}
if (*nfiltered != 0) {
qsort(*filtered, *nfiltered, sizeof(*profiles), compare_profiles);
}
return ret;
}
/**
* get_processes - get a list of processes that are confined
* @profiles: list of profiles, used to filter out unconfined processes
* @n: number of entries in @procfiles
* @processes: return: list of confined processes
* @nprocesses: return: number of entries in @processes
*
* Return: 0 on success, shell exit code on failure
*
* profiles is used to find prcesses that should be confined but aren't.
*/
static int get_processes(struct profile *profiles,
size_t n,
struct process **processes,
size_t *nprocesses)
{
DIR *dir = NULL;
struct dirent *entry = NULL;
int ret = 0;
*processes = NULL;
*nprocesses = 0;
dir = opendir("/proc");
if (dir == NULL) {
ret = AA_EXIT_INTERNAL_ERROR;
goto exit;
}
while ((entry = readdir(dir)) != NULL) {
size_t i;
int rc;
int ispid = 1;
autofree char *profile = NULL;
autofree char *mode = NULL; /* be careful */
autofree char *exe = NULL;
autofree char *real_exe = NULL;
autofclose FILE *fp = NULL;
autofree char *line = NULL;
// ignore non-pid entries
for (i = 0; ispid && i < strlen(entry->d_name); i++) {
ispid = (isdigit(entry->d_name[i]) ? 1 : 0);
}
if (!ispid) {
continue;
}
rc = aa_getprocattr(atoi(entry->d_name), "current", &profile, &mode);
if (rc == -1 && errno != ENOMEM) {
/* fail to access */
continue;
} else if (rc == -1 ||
asprintf(&exe, "/proc/%s/exe", entry->d_name) == -1) {
fprintf(stderr, "ERROR: Failed to allocate memory\n");
ret = AA_EXIT_INTERNAL_ERROR;
goto exit;
} else if (mode) {
/* TODO: make this not needed. Mode can now be autofreed */
mode = strdup(mode);
}
// get executable - readpath can allocate for us but seems
// to fail in some cases with errno 2 - no such file or
// directory - whereas readlink() can succeed in these
// cases - and readpath() seems to have the same behaviour
// as in python with better canonicalized results so try it
// first and fallack to readlink if it fails
// coverity[toctou]
real_exe = realpath(exe, NULL);
if (real_exe == NULL) {
int res;
// ensure enough space for NUL terminator
real_exe = calloc(PATH_MAX + 1, sizeof(char));
if (real_exe == NULL) {
fprintf(stderr, "ERROR: Failed to allocate memory\n");
ret = AA_EXIT_INTERNAL_ERROR;
goto exit;
}
res = readlink(exe, real_exe, PATH_MAX);
if (res == -1) {
continue;
}
real_exe[res] = '\0';
}
if (mode == NULL) {
// is unconfined so keep only if this has a
// matching profile. TODO: fix to use attachment
// ideally would walk process tree and apply
// according to x rules and attachments
for (i = 0; i < n; i++) {
if (strcmp(profiles[i].name, real_exe) == 0) {
profile = strdup(real_exe);
mode = strdup("unconfined");
break;
}
}
}
if (profile != NULL && mode != NULL) {
struct process *_processes = realloc(*processes,
(*nprocesses + 1) * sizeof(**processes));
if (_processes == NULL) {
free_processes(*processes, *nprocesses);
*processes = NULL;
*nprocesses = 0;
ret = AA_EXIT_INTERNAL_ERROR;
goto exit;
}
_processes[*nprocesses].pid = strdup(entry->d_name);
_processes[*nprocesses].profile = profile;
_processes[*nprocesses].exe = strdup(real_exe);
_processes[*nprocesses].mode = mode;
*processes = _processes;
*nprocesses = *nprocesses + 1;
profile = NULL;
mode = NULL;
ret = AA_EXIT_ENABLED;
}
}
exit:
if (dir != NULL) {
closedir(dir);
}
return ret;
}
/**
* filter_processes: create a new filtered process list by applying @filter
* @processes: list of processes to filter
* @n: number of entries in @processes
* @filter: mode string to filter @processes against, if NULL no filter
* @filtered: return: new list of processes matching filter
* @nfiltered: number of entries in @filtered
*
* Return: 0 on success, shell exit value on failure
*/
static int filter_processes(struct process *processes,
size_t n,
const char *filter,
struct process **filtered,
size_t *nfiltered)
{
size_t i;
int ret = 0;
*filtered = NULL;
*nfiltered = 0;
for (i = 0; i < n; i++) {
if (filter == NULL || strcmp(processes[i].mode, filter) == 0) {
struct process *_filtered = realloc(*filtered, (*nfiltered + 1) * sizeof(**filtered));
if (_filtered == NULL) {
free_processes(*filtered, *nfiltered);
*filtered = NULL;
*nfiltered = 0;
ret = AA_EXIT_INTERNAL_ERROR;
break;
}
_filtered[*nfiltered].pid = strdup(processes[i].pid);
_filtered[*nfiltered].profile = strdup(processes[i].profile);
_filtered[*nfiltered].exe = strdup(processes[i].exe);
_filtered[*nfiltered].mode = strdup(processes[i].mode);
*filtered = _filtered;
*nfiltered = *nfiltered + 1;
}
}
return ret;
}
/**
* simple_filtered_count - count the number of profiles with mode == filter
* @outf: output file destination
* @filter: mode string to filter profiles on
* @profiles: profiles list to filter
* @nprofiles: number of entries in @profiles
*
* Return: 0 on success, else shell error code
*/
static int simple_filtered_count(FILE *outf, const char *filter,
struct profile *profiles, size_t nprofiles)
{
struct profile *filtered = NULL;
size_t nfiltered;
int ret;
ret = filter_profiles(profiles, nprofiles, filter,
&filtered, &nfiltered);
fprintf(outf, "%zd\n", nfiltered);
free_profiles(filtered, nfiltered);
return ret;
}
/**
* simple_filtered_process_count - count processes with mode == filter
* @outf: output file destination
* @filter: mode string to filter processes on
* @processes: process list to filter
* @nprocesses: number of entries in @processes
*
* Return: 0 on success, else shell error code
*/
static int simple_filtered_process_count(FILE *outf, const char *filter,
struct process *processes, size_t nprocesses) {
struct process *filtered = NULL;
size_t nfiltered;
int ret;
ret = filter_processes(processes, nprocesses, filter, &filtered, &nfiltered);
fprintf(outf, "%zd\n", nfiltered);
free_processes(filtered, nfiltered);
return ret;
}
static int compare_processes_by_profile(const void *a, const void *b) {
return strcmp(((struct process *)a)->profile,
((struct process *)b)->profile);
}
static int compare_processes_by_executable(const void *a, const void *b) {
return strcmp(((struct process *)a)->exe,
((struct process *)b)->exe);
}
static void json_header(FILE *outf)
{
fprintf(outf, "{\"version\": \"%s\", ", aa_status_json_version);
}
static void json_footer(FILE *outf)
{
fprintf(outf, "}\n");
}
/**
* detailed_profiles - output a detailed listing of apparmor profile status
* @outf: output file
* @filter: mode filter
* @json: whether output should be in json format
* @profiles: list of profiles to output
* @nprofiles: number of profiles in @profiles
*
* Return: 0 on success, else shell error
*/
static int detailed_profiles(FILE *outf, const char *filter, bool json,
struct profile *profiles, size_t nprofiles) {
int ret;
size_t i;
if (json) {
fprintf(outf, "\"profiles\": {");
} else {
dfprintf(outf, "%zd profiles are loaded.\n", nprofiles);
}
for (i = 0; i < ARRAY_SIZE(profile_statuses); i++) {
size_t nfiltered = 0, j;
struct profile *filtered = NULL;
if (filter && strcmp(filter, profile_statuses[i]) != 0)
/* skip processing for entries that don't match filter*/
continue;
ret = filter_profiles(profiles, nprofiles, profile_statuses[i], &filtered, &nfiltered);
if (ret != 0) {
return ret;
}
if (!json) {
dfprintf(outf, "%zd profiles are in %s mode.\n", nfiltered, profile_statuses[i]);
}
for (j = 0; j < nfiltered; j++) {
if (json) {
fprintf(outf, "%s\"%s\": \"%s\"",
i == 0 && j == 0 ? "" : ", ", filtered[j].name, profile_statuses[i]);
} else {
dfprintf(outf, " %s\n", filtered[j].name);
}
}
free_profiles(filtered, nfiltered);
}
if (json)
fprintf(outf, "}, ");
return AA_EXIT_ENABLED;
}
/**
* detailed_processses - output a detailed listing of apparmor process status
* @outf: output file
* @filter: mode filter
* @json: whether output should be in json format
* @processes: list of processes to output
* @nprocesses: number of processes in @processes
*
* Return: 0 on success, else shell error
*/
static int detailed_processes(FILE *outf, const char *filter, bool json,
struct process *processes, size_t nprocesses) {
int ret;
size_t i;
if (json) {
fprintf(outf, "\"processes\": {");
} else {
dfprintf(outf, "%zd processes have profiles defined.\n", nprocesses);
}
for (i = 0; i < ARRAY_SIZE(process_statuses); i++) {
size_t nfiltered = 0, j;
struct process *filtered = NULL;
if (filter && strcmp(filter, process_statuses[i]) != 0)
/* skip processing for entries that don't match filter*/
continue;
ret = filter_processes(processes, nprocesses, process_statuses[i], &filtered, &nfiltered);
if (ret != 0)
goto exit;
if (!json) {
if (strcmp(process_statuses[i], "unconfined") == 0) {
dfprintf(outf, "%zd processes are unconfined but have a profile defined.\n", nfiltered);
} else {
dfprintf(outf, "%zd processes are in %s mode.\n", nfiltered, process_statuses[i]);
}
}
if (!json) {
qsort(filtered, nfiltered, sizeof(*filtered), compare_processes_by_profile);
for (j = 0; j < nfiltered; j++) {
dfprintf(outf, " %s (%s) %s\n", filtered[j].exe, filtered[j].pid,
// hide profile name if matches executable
(strcmp(filtered[j].profile, filtered[j].exe) == 0 ?
"" :
filtered[j].profile));
}
} else {
// json output requires processes to be grouped per executable
qsort(filtered, nfiltered, sizeof(*filtered), compare_processes_by_executable);
for (j = 0; j < nfiltered; j++) {
if (j > 0 && strcmp(filtered[j].exe, filtered[j - 1].exe) == 0) {
// same executable
fprintf(outf, ", {\"profile\": \"%s\", \"pid\": \"%s\", \"status\": \"%s\"}",
filtered[j].profile, filtered[j].pid, filtered[j].mode);
} else {
fprintf(outf, "%s\"%s\": [{\"profile\": \"%s\", \"pid\": \"%s\", \"status\": \"%s\"}",
// first element will be a unique executable
j == 0 ? "" : "], ",
filtered[j].exe, filtered[j].profile, filtered[j].pid, filtered[j].mode);
}
}
if (j > 0) {
fprintf(outf, "]");
}
}
free_processes(filtered, nfiltered);
}
if (json) {
fprintf(outf, "}}\n");
}
exit:
return ret;
}
static int print_usage(const char *command, bool error)
{
int status = EXIT_SUCCESS;
if (error) {
status = EXIT_FAILURE;
}
printf("Usage: %s [OPTIONS]\n"
"Displays various information about the currently loaded AppArmor policy.\n"
"OPTIONS (one only):\n"
" --enabled returns error code if AppArmor not enabled\n"
" --profiled prints the number of loaded policies\n"
" --enforced prints the number of loaded enforcing policies\n"
" --complaining prints the number of loaded non-enforcing policies\n"
" --kill prints the number of loaded enforcing policies that kill tasks on policy violations\n"
" --special-unconfined prints the number of loaded non-enforcing policies in the special unconfined mode\n"
" --process-mixed prints the number processes with mixed profile modes\n"
" --json displays multiple data points in machine-readable JSON format\n"
" --pretty-json same data as --json, formatted for human consumption as well\n"
" --verbose (default) displays multiple data points about loaded policy set\n"
" --help this message\n",
command);
exit(status);
return 0;
}
#define ARG_ENABLED 129
#define ARG_PROFILED 130
#define ARG_ENFORCED 131
#define ARG_COMPLAIN 132
#define ARG_KILL 133
#define ARG_UNCONFINED 134
#define ARG_PS_MIXED 135
#define ARG_JSON 136
#define ARG_PRETTY 137
#define ARG_VERBOSE 'v'
#define ARG_HELP 'h'
static char **parse_args(int argc, char **argv)
{
int opt;
struct option long_opts[] = {
{"enabled", no_argument, 0, ARG_ENABLED},
{"profiled", no_argument, 0, ARG_PROFILED},
{"enforced", no_argument, 0, ARG_ENFORCED},
{"complaining", no_argument, 0, ARG_COMPLAIN},
{"kill", no_argument, 0, ARG_KILL},
{"special-unconfined", no_argument, 0, ARG_UNCONFINED},
{"process-mixed", no_argument, 0, ARG_PS_MIXED},
{"json", no_argument, 0, ARG_JSON},
{"pretty-json", no_argument, 0, ARG_PRETTY},
{"verbose", no_argument, 0, ARG_VERBOSE},
{"help", no_argument, 0, ARG_HELP},
{NULL, 0, 0, 0},
};
// Using exit here is temporary
while ((opt = getopt_long(argc, argv, "+vh", long_opts, NULL)) != -1) {
switch (opt) {
case ARG_ENABLED:
exit(aa_is_enabled() == 1 ? 0 : AA_EXIT_DISABLED);
break;
case ARG_VERBOSE:
verbose = 1;
/* default opt_mode */
/* default opt_show */
break;
case ARG_HELP:
print_usage(argv[0], false);
break;
case ARG_PROFILED:
opt_count = true;
opt_show = SHOW_PROFILES;
/* default opt_mode */
break;
case ARG_ENFORCED:
opt_count = true;
opt_show = SHOW_PROFILES;
opt_mode = "enforce";
break;
case ARG_COMPLAIN:
opt_count = true;
opt_show = SHOW_PROFILES;
opt_mode = "complain";
break;
case ARG_UNCONFINED:
opt_count = true;
opt_show = SHOW_PROFILES;
opt_mode = "unconfined";
break;
case ARG_KILL:
opt_count = true;
opt_show = SHOW_PROFILES;
opt_mode = "kill";
break;
case ARG_PS_MIXED:
opt_count = true;
opt_show = SHOW_PROCESSES;
opt_mode = "mixed";
break;
case ARG_JSON:
opt_json = true;
/* default opt_show */
break;
case ARG_PRETTY:
opt_pretty = true;
opt_json = true;
/* default opt_show */
break;
default:
dfprintf(stderr, "Error: Invalid command.\n");
print_usage(argv[0], true);
break;
}
}
return argv + optind;
}
int main(int argc, char **argv)
{
autofree char *buffer = NULL; /* pretty print buffer */
size_t buffer_size;
autofclose FILE *fp = NULL;
size_t nprofiles = 0;
struct profile *profiles = NULL;
int ret = EXIT_SUCCESS;
const char *progname = argv[0];
FILE *outf = stdout, *outf_save = NULL;
if (argc > 2) {
dfprintf(stderr, "Error: Too many options.\n");
print_usage(progname, true);
} else if (argc == 2) {
argv = parse_args(argc, argv);
} else {
verbose = 1;
/* default opt_show */
/* default opt_mode */
/* default opt_json */
}
/* check apparmor is available and we have permissions */
ret = open_profiles(&fp);
if (ret != 0)
goto out;
if (opt_pretty) {
outf_save = outf;
outf = open_memstream(&buffer, &buffer_size);
if (!outf) {
dfprintf(stderr, "Failed to open memstream: %m\n");
return AA_EXIT_INTERNAL_ERROR;
}
}
/* always get policy even if not displayed because getting processes
* requires it to filter out unconfined tasks that don't or shouldn't
* have policy associated.
*/
ret = get_profiles(fp, &profiles, &nprofiles);
if (ret != 0) {
dfprintf(stderr, "Failed to get profiles: %d....\n", ret);
goto out;
}
if (opt_json)
json_header(outf);
if (opt_show & SHOW_PROFILES) {
if (opt_count) {
ret = simple_filtered_count(outf, opt_mode,
profiles, nprofiles);
} else {
ret = detailed_profiles(outf, opt_mode, opt_json,
profiles, nprofiles);
}
if (ret != 0)
goto out;
}
if (opt_show & SHOW_PROCESSES) {
struct process *processes = NULL;
size_t nprocesses = 0;
ret = get_processes(profiles, nprofiles, &processes, &nprocesses);
if (ret != 0) {
dfprintf(stderr, "Failed to get processes: %d....\n", ret);
} else if (opt_count) {
ret = simple_filtered_process_count(outf, opt_mode,
processes, nprocesses);
} else {
ret = detailed_processes(outf, opt_mode, opt_json,
processes, nprocesses);
}
free_processes(processes, nprocesses);
if (ret != 0)
goto out;
}
if (opt_json)
json_footer(outf);
if (opt_pretty) {
autofree char *pretty = NULL;
cJSON *json;
/* explicit close to sync */
fclose(outf);
outf = outf_save;
json = cJSON_Parse(buffer);
if (!json) {
dfprintf(stderr, "Failed to parse json output");
ret = AA_EXIT_INTERNAL_ERROR;
goto out;
}
pretty = cJSON_Print(json);
if (!pretty) {
dfprintf(stderr, "Failed to print pretty json");
ret = AA_EXIT_INTERNAL_ERROR;
goto out;
}
fprintf(outf, "%s\n", pretty);
ret = AA_EXIT_ENABLED;
}
out:
free_profiles(profiles, nprofiles);
exit(ret);
}