sca模块主函数wm_sca_main -> wm_sca_start
检查policy文件中的每一个项目wm_sca_check_policy
static int wm_sca_check_policy(const cJSON * const policy, const cJSON * const checks, OSHash *global_check_list)
{
if(!policy) {
return 1;
}
const cJSON * const id = cJSON_GetObjectItem(policy, "id");
if(!id) {
mwarn("Field 'id' not found in policy header.");
return 1;
}
if(!id->valuestring){
mwarn("Invalid format for field 'id'");
return 1;
}
char *coincident_policy_file;
if((coincident_policy_file = OSHash_Get(global_check_list,id->valuestring)), coincident_policy_file) {
mwarn("Found duplicated policy ID: %s. File '%s' contains the same ID.", id->valuestring, coincident_policy_file);
return 1;
}
const cJSON * const name = cJSON_GetObjectItem(policy, "name");
if(!name) {
mwarn("Field 'name' not found in policy header.");
return 1;
}
if(!name->valuestring){
mwarn("Invalid format for field 'name'");
return 1;
}
const cJSON * const file = cJSON_GetObjectItem(policy, "file");
if(!file) {
mwarn("Field 'file' not found in policy header.");
return 1;
}
if(!file->valuestring){
mwarn("Invalid format for field 'file'");
return 1;
}
const cJSON * const description = cJSON_GetObjectItem(policy, "description");
if(!description) {
mwarn("Field 'description' not found in policy header.");
return 1;
}
const cJSON * const regex_type = cJSON_GetObjectItem(policy, "regex_type");
if(!regex_type) {
mdebug1("Field 'regex_type' not found in policy header. The OS_REGEX engine shall be used.");
}
if(!description->valuestring) {
mwarn("Invalid format for field 'description'");
return 1;
}
// Check for policy rules with duplicated IDs */
if (!checks) {
mwarn("Section 'checks' not found.");
return 1;
}
int *read_id;
os_calloc(1, sizeof(int), read_id);
read_id[0] = 0;
const cJSON *check;
cJSON_ArrayForEach(check, checks) {
const cJSON * const check_id = cJSON_GetObjectItem(check, "id");
if (check_id == NULL) {
mwarn("Check ID not found.");
free(read_id);
return 1;
}
if (check_id->valueint <= 0) {
// Invalid ID
mwarn("Invalid check ID: %d", check_id->valueint);
free(read_id);
return 1;
}
char *coincident_policy;
char *key_id;
size_t key_length = snprintf(NULL, 0, "%d", check_id->valueint);
os_malloc(key_length + 1, key_id);
snprintf(key_id, key_length + 1, "%d", check_id->valueint);
if((coincident_policy = (char *)OSHash_Get(global_check_list, key_id)), coincident_policy){
// Invalid ID
mwarn("Found duplicated check ID: %d. First appearance at policy '%s'", check_id->valueint, coincident_policy);
os_free(key_id);
os_free(read_id);
return 1;
}
os_free(key_id);
int i;
for (i = 0; read_id[i] != 0; i++) {
if (check_id->valueint == read_id[i]) {
// Duplicated ID
mwarn("Found duplicated check ID: %d", check_id->valueint);
free(read_id);
return 1;
}
}
os_realloc(read_id, sizeof(int) * (i + 2), read_id);
read_id[i] = check_id->valueint;
read_id[i + 1] = 0;
const cJSON * const rules = cJSON_GetObjectItem(check, "rules");
if (rules == NULL) {
mwarn("Invalid check %d: no rules found.", check_id->valueint);
free(read_id);
return 1;
}
int rules_n = 0;
const cJSON *rule;
cJSON_ArrayForEach(rule, rules) {
if (!rule->valuestring) {
mwarn("Invalid check %d: Empty rule.", check_id->valueint);
free(read_id);
return 1;
}
char *valuestring_ref = rule->valuestring;
valuestring_ref += 4 * (!strncmp(valuestring_ref, "NOT ", 4) || !strncmp(valuestring_ref, "not ", 4));
switch (*valuestring_ref) {
#ifdef WIN32
case 'r':
#endif
case 'f':
case 'd':
case 'p':
case 'c':
break;
case '\0':
mwarn("Invalid check %d: Empty rule.", check_id->valueint);
free(read_id);
return 1;
default:
mwarn("Invalid check %d: Invalid rule format.", check_id->valueint);
free(read_id);
return 1;
}
rules_n++;
if (rules_n > 255) {
free(read_id);
mwarn("Invalid check %d: Maximum number of rules is 255.", check_id->valueint);
return 1;
}
}
if (rules_n == 0) {
mwarn("Invalid check %d: no rules found.", check_id->valueint);
free(read_id);
return 1;
}
}
char *policy_file = NULL;
os_strdup(file->valuestring, policy_file);
const int id_add_retval = OSHash_Add(global_check_list, id->valuestring, policy_file);
if (id_add_retval == 0){
os_free(policy_file);
os_free(read_id);
merror_exit("(1102): Could not acquire memory");
}
if (id_add_retval == 1){
merror("Error validating duplicated ID. Policy %s in file %s is duplicated", id->valuestring, policy_file);
os_free(policy_file);
os_free(read_id);
return 1;
}
int i;
for (i = 0; read_id[i] != 0; ++i) {
char *policy_id = NULL;
os_strdup(id->valuestring, policy_id);
const int check_add_retval = OSHash_Numeric_Add_ex(global_check_list, read_id[i], policy_id);
if (check_add_retval == 0){
os_free(policy_id);
os_free(read_id);
merror_exit("(1102): Could not acquire memory");
}
if (check_add_retval == 1){
merror("Error validating duplicated ID. Check %s in policy %s is duplicated", id->valuestring, policy_id);
os_free(policy_id);
os_free(read_id);
return 1;
}
}
os_free(read_id);
return 0;
}
policy文件中的具体rules项目,其中规则之一:
# 1.1.1.3 udf: filesystem
- id: 6002
title: "Ensure mounting of udf filesystems is disabled"
description: "The udf filesystem type is the universal disk format used to implement ISO/IEC 13346 and ECMA-167 specifications. This is an open vendor filesystem type for data storage on a broad range of media. This filesystem type is necessary to support writing DVDs and newer optical disc formats."
rationale: "Removing support for unneeded filesystem types reduces the local attack surface of the system. If this filesystem type is not needed, disable it."
remediation: "Edit or create the file /etc/modprobe.d/CIS.conf and add the following line: install udf /bin/true. Run the following command to unload the udf module: rmmod udf"
compliance:
- cis: ["1.1.1.3"]
- cis_csc: ["5.1"]
- pci_dss: ["2.2.5"]
- tsc: ["CC6.3"]
references:
- AJ Lewis, "LVM HOWTO", https://tldp.org/HOWTO/LVM-HOWTO/
condition: all
rules:
- 'c:modprobe -n -v udf -> r:install /bin/true|Module udf not found'
- 'not c:lsmod -> r:udf'
rules中的每一项是r (读取), f,d,p,c,not,NOT开头 "->"表示前一个动作之后的接着的下一个动作,或者条件
sca扫描核心函数
/*
Rules that match always return 1, and the other way arround.
Rule aggregators logic:
##########################################################
ALL:
r_1 -f -> r:123
...
r_n -f -> r:234
For an ALL to succeed, every rule shall return 1, in other words,
| = n -> ALL = RETURN_FOUND
SUM(r_i, 0, n) |
| != n -> ALL = RETURN_NOT_FOUND
##########################################################
ANY:
r_1 -f -> r:123
...
r_n -f -> r:234
For an ANY to succeed, a rule shall return 1, in other words,
| > 0 -> ANY = RETURN_FOUND
SUM(r_i, 0, n) |
| = 0 -> ANY = RETURN_NOT_FOUND
##########################################################
NONE:
r_1 -f -> r:123
...
r_n -f -> r:234
For a NONE to succeed, all rules shall return RETURN_NOT_FOUND, in other words,
| > 0 -> NONE = RETURN_NOT_FOUND
SUM(r_i, 0, n) |
| = 0 -> NONE = RETURN_FOUND
##########################################################
ANY and NONE aggregators are complementary.
*/
static int wm_sca_do_scan(cJSON * checks,
OSStore * vars,
wm_sca_t * data,
int id,
cJSON * policy,
int requirements_scan,
int cis_db_index,
unsigned int remote_policy,
int first_scan,
int * checks_number,
char ** sorted_variables,
char * policy_engine)
{
int type = 0;
char buf[OS_SIZE_1024 + 2];
char final_file[2048 + 1];
char *reason = NULL;
int ret_val = 0;
OSList *p_list = NULL;
/* Initialize variables */
memset(buf, '\0', sizeof(buf));
memset(final_file, '\0', sizeof(final_file));
int check_count = 0;
cJSON *check = NULL;
cJSON_ArrayForEach(check, checks) {
char _check_id_str[50];
if (requirements_scan) {
snprintf(_check_id_str, sizeof(_check_id_str), "Requirements check");
} else {
const cJSON * const c_id = cJSON_GetObjectItem(check, "id");
if (!c_id || !c_id->valueint) {
merror("Skipping check. Check ID is invalid. Offending check number: %d", check_count);
ret_val = 1;
continue;
}
snprintf(_check_id_str, sizeof(_check_id_str), "id: %d", c_id->valueint);
}
const cJSON * const c_title = cJSON_GetObjectItem(check, "title");
if (!c_title || !c_title->valuestring) {
merror("Skipping check with %s: Check name is invalid.", _check_id_str);
if (requirements_scan) {
ret_val = 1;
goto clean_return;
}
continue;
}
const cJSON * const c_condition = cJSON_GetObjectItem(check, "condition");
if (!c_condition || !c_condition->valuestring) {
merror("Skipping check '%s: %s': Check condition not found.", _check_id_str, c_title->valuestring);
if (requirements_scan) {
ret_val = 1;
goto clean_return;
}
continue;
}
int condition = 0;
wm_sca_set_condition(c_condition->valuestring, &condition);
if (condition == WM_SCA_COND_INV) {
merror("Skipping check '%s: %s': Check condition (%s) is invalid.",_check_id_str, c_title->valuestring, c_condition->valuestring);
if (requirements_scan) {
ret_val = 1;
goto clean_return;
}
continue;
}
int g_found = RETURN_NOT_FOUND;
if ((condition & WM_SCA_COND_ANY) || (condition & WM_SCA_COND_NON)) {
/* aggregators ANY and NONE break by matching, so they shall return NOT_FOUND if they never break */
g_found = RETURN_NOT_FOUND;
} else if (condition & WM_SCA_COND_ALL) {
/* aggregator ALL breaks the moment a rule does not match. If it doesn't break, all rules have matched */
g_found = RETURN_FOUND;
}
mdebug1("Beginning evaluation of check %s '%s'", _check_id_str, c_title->valuestring);
mdebug1("Rule aggregation strategy for this check is '%s'", c_condition->valuestring);
mdebug2("Initial rule-aggregator value por this type of rule is '%d'", g_found);
mdebug1("Beginning rules evaluation.");
const cJSON *const rules = cJSON_GetObjectItem(check, "rules");
if (!rules) {
merror("Skipping check %s '%s': No rules found.", _check_id_str, c_title->valuestring);
if (requirements_scan) {
ret_val = 1;
goto clean_return;
}
continue;
}
w_expression_t * regex_engine = NULL;
cJSON * engine = cJSON_GetObjectItem(check, "regex_type");
if (engine) {
if (strcmp(PCRE2_STR, cJSON_GetStringValue(engine)) == 0) {
w_calloc_expression_t(®ex_engine, EXP_TYPE_PCRE2);
} else {
w_calloc_expression_t(®ex_engine, EXP_TYPE_OSREGEX);
}
} else {
if(strcmp(PCRE2_STR, policy_engine) == 0) {
w_calloc_expression_t(®ex_engine, EXP_TYPE_PCRE2);
} else {
w_calloc_expression_t(®ex_engine, EXP_TYPE_OSREGEX);
}
}
mdebug1("SCA will use '%s' engine to check the rules.", w_expression_get_regex_type(regex_engine));
char *rule_cp = NULL;
const cJSON *rule_ref;
cJSON_ArrayForEach(rule_ref, rules) {
/* this free is responsible of freeing the copy of the previous rule if
the loop 'continues', i.e, does not reach the end of its block. */
os_free(rule_cp);
if(!rule_ref->valuestring) {
mdebug1("Field 'rule' must be a string.");
ret_val = 1;
os_free(regex_engine);
goto clean_return;
}
mdebug1("Considering rule: '%s'", rule_ref->valuestring);
os_strdup(rule_ref->valuestring, rule_cp);
char *rule_cp_ref = NULL;
#ifdef WIN32
char expanded_rule[2048] = {0};
ExpandEnvironmentStrings(rule_cp, expanded_rule, 2048);
rule_cp_ref = expanded_rule;
mdebug2("Rule after variable expansion: '%s'", rule_cp_ref);
#else
rule_cp_ref = rule_cp;
#endif
int rule_is_negated = 0;
if (rule_cp_ref &&
(strncmp(rule_cp_ref, "NOT ", 4) == 0 ||
strncmp(rule_cp_ref, "not ", 4) == 0))
{
mdebug2("Rule is negated.");
rule_is_negated = 1;
rule_cp_ref += 4;
}
/* Get value to look for. char *value is a reference
to rule_cp memory. Do not release value! */
char *value = wm_sca_get_value(rule_cp_ref, &type);
if (value == NULL) {
merror("Invalid rule: '%s'. Skipping policy.", rule_ref->valuestring);
os_free(rule_cp);
ret_val = 1;
os_free(regex_engine);
goto clean_return;
}
int found = RETURN_NOT_FOUND;
if (type == WM_SCA_TYPE_FILE) {
/* Check files */
char *pattern = wm_sca_get_pattern(value);
char *rule_location = NULL;
char *aux = NULL;
os_strdup(value, rule_location);
/* If any, replace the variables by their respective values */
if (sorted_variables) {
int i = 0;
for (i = 0; sorted_variables[i]; i++) {
if (strstr(rule_location, sorted_variables[i])) {
mdebug2("Variable '%s' found at rule '%s'. Replacing it.", sorted_variables[i], rule_location);
aux = wstr_replace(rule_location, sorted_variables[i], OSStore_Get(vars, sorted_variables[i]));
os_free(rule_location);
rule_location = aux;
if (!rule_location) {
merror("Invalid variable replacement: '%s'. Skipping check.", sorted_variables[i]);
break;
}
mdebug2("Variable replaced: '%s'", rule_location);
}
}
}
if (!rule_location) {
continue;
}
const int result = wm_sca_check_file_list(rule_location, pattern, &reason, regex_engine);
if (result == RETURN_FOUND || result == RETURN_INVALID) {
found = result;
}
char _b_msg[OS_SIZE_1024 + 1];
_b_msg[OS_SIZE_1024] = '\0';
snprintf(_b_msg, OS_SIZE_1024, " File: %s", rule_location);
append_msg_to_vm_scat(data, _b_msg);
os_free(rule_location);
} else if (type == WM_SCA_TYPE_COMMAND) {
/* Check command output */
char *pattern = wm_sca_get_pattern(value);
char *rule_location = NULL;
char *aux = NULL;
os_strdup(value, rule_location);
if (!data->remote_commands && remote_policy) {
mwarn("Ignoring check for policy '%s'. The internal option 'sca.remote_commands' is disabled.", cJSON_GetObjectItem(policy, "name")->valuestring);
if (reason == NULL) {
os_malloc(snprintf(NULL, 0, "Ignoring check for running command '%s'. The internal option 'sca.remote_commands' is disabled", rule_location) + 1, reason);
sprintf(reason, "Ignoring check for running command '%s'. The internal option 'sca.remote_commands' is disabled", rule_location);
}
found = RETURN_INVALID;
} else {
/* If any, replace the variables by their respective values */
if (sorted_variables) {
int i = 0;
for (i = 0; sorted_variables[i]; i++) {
if (strstr(rule_location, sorted_variables[i])) {
mdebug2("Variable '%s' found at rule '%s'. Replacing it.", sorted_variables[i], rule_location);
aux = wstr_replace(rule_location, sorted_variables[i], OSStore_Get(vars, sorted_variables[i]));
os_free(rule_location);
rule_location = aux;
if (!rule_location) {
merror("Invalid variable: '%s'. Skipping check.", sorted_variables[i]);
break;
}
mdebug2("Variable replaced: '%s'", rule_location);
}
}
}
if (!rule_location) {
continue;
}
mdebug2("Running command: '%s'", rule_location);
const int val = wm_sca_read_command(rule_location, pattern, data, &reason, regex_engine);
if (val == RETURN_FOUND) {
mdebug2("Command output matched.");
found = RETURN_FOUND;
} else if (val == RETURN_INVALID){
mdebug2("Command output did not match.");
found = RETURN_INVALID;
}
}
char _b_msg[OS_SIZE_1024 + 1];
_b_msg[OS_SIZE_1024] = '\0';
snprintf(_b_msg, OS_SIZE_1024, " Command: %s", rule_location);
append_msg_to_vm_scat(data, _b_msg);
os_free(rule_location);
} else if (type == WM_SCA_TYPE_DIR) {
/* Check directory */
mdebug2("Processing directory rule '%s'", value);
char * const file = wm_sca_get_pattern(value);
char *rule_location = NULL;
char *aux = NULL;
os_strdup(value, rule_location);
/* If any, replace the variables by their respective values */
if (sorted_variables) {
int i = 0;
for (i = 0; sorted_variables[i]; i++) {
if (strstr(rule_location, sorted_variables[i])) {
mdebug2("Variable '%s' found at rule '%s'. Replacing it.", sorted_variables[i], rule_location);
aux = wstr_replace(rule_location, sorted_variables[i], OSStore_Get(vars, sorted_variables[i]));
os_free(rule_location);
rule_location = aux;
if (!rule_location) {
merror("Invalid variable: '%s'. Skipping check.", sorted_variables[i]);
break;
}
mdebug2("Variable replaced: '%s'", rule_location);
}
}
}
if (!rule_location) {
continue;
}
char * const pattern = wm_sca_get_pattern(file);
found = wm_sca_check_dir_list(data, rule_location, file, pattern, &reason, regex_engine);
mdebug2("Check directory rule result: %d", found);
os_free(rule_location);
} else if (type == WM_SCA_TYPE_PROCESS) {
/* Check process existence */
if (!p_list) {
/* Lazy evaluation */
p_list = w_os_get_process_list();
}
mdebug2("Checking process: '%s'", value);
if (wm_sca_check_process_is_running(p_list, value, &reason, regex_engine)) {
mdebug2("Process found.");
found = RETURN_FOUND;
} else {
mdebug2("Process not found.");
}
char _b_msg[OS_SIZE_1024 + 1];
_b_msg[OS_SIZE_1024] = '\0';
snprintf(_b_msg, OS_SIZE_1024, " Process: %s", value);
append_msg_to_vm_scat(data, _b_msg);
}
#ifdef WIN32
else if (type == WM_SCA_TYPE_REGISTRY) {
/* Check windows registry */
char * const entry = wm_sca_get_pattern(value);
char * const pattern = wm_sca_get_pattern(entry);
found = wm_sca_is_registry(value, entry, pattern, &reason, regex_engine);
char _b_msg[OS_SIZE_1024 + 1];
_b_msg[OS_SIZE_1024] = '\0';
snprintf(_b_msg, OS_SIZE_1024, " Registry: %s", value);
append_msg_to_vm_scat(data, _b_msg);
}
#endif
/* Rule result processing */
if (found != RETURN_INVALID) {
found = rule_is_negated ^ found;
}
mdebug1("Result for rule '%s': %d", rule_ref->valuestring, found);
if (((condition & WM_SCA_COND_ALL) && found == RETURN_NOT_FOUND) ||
((condition & WM_SCA_COND_ANY) && found == RETURN_FOUND) ||
((condition & WM_SCA_COND_NON) && found == RETURN_FOUND))
{
g_found = found;
mdebug1("Breaking from rule aggregator '%s' with found = %d", c_condition->valuestring, g_found);
break;
}
if (found == RETURN_INVALID) {
/* Rules that agreggate by ANY are the only that can success after an INVALID
On the other hand ALL and NONE agregators can fail after an INVALID. */
g_found = found;
mdebug1("Rule evaluation returned INVALID. Continuing.");
}
}
if ((condition & WM_SCA_COND_NON) && g_found != RETURN_INVALID) {
g_found = !g_found;
}
mdebug1("Result for check %s '%s' -> %d", _check_id_str, c_title->valuestring, g_found);
if (g_found != RETURN_INVALID) {
os_free(reason);
}
/* if the loop breaks, rule_cp shall be released.
Also frees the the memory reserved on the last iteration */
os_free(rule_cp);
/* Determine if requirements are satisfied */
if (requirements_scan) {
/* return value for requirement scans is the inverse of the result,
unless the result is INVALID */
ret_val = g_found == RETURN_INVALID ? 1 : !g_found;
int i;
for (i=0; data->alert_msg[i]; i++){
free(data->alert_msg[i]);
data->alert_msg[i] = NULL;
}
w_free_expression_t(®ex_engine);
goto clean_return;
}
/* Event construction */
const char failed[] = "failed";
const char passed[] = "passed";
const char invalid[] = ""; //NOT AN ERROR!
const char *message_ref = NULL;
if (g_found == RETURN_NOT_FOUND) {
wm_sca_summary_increment_failed();
message_ref = failed;
} else if (g_found == RETURN_FOUND) {
wm_sca_summary_increment_passed();
message_ref = passed;
} else {
wm_sca_summary_increment_invalid();
message_ref = invalid;
if (reason == NULL) {
os_malloc(snprintf(NULL, 0, "Unknown reason") + 1, reason);
sprintf(reason, "Unknown reason");
mdebug1("A check returned INVALID for an unknown reason.");
}
}
cJSON *event = wm_sca_build_event(check, policy, data->alert_msg, id, message_ref, reason);
if (event) {
/* Alert if necessary */
if(!cis_db_for_hash[cis_db_index].elem[check_count]) {
os_realloc(cis_db_for_hash[cis_db_index].elem, sizeof(cis_db_info_t *) * (check_count + 2), cis_db_for_hash[cis_db_index].elem);
cis_db_for_hash[cis_db_index].elem[check_count] = NULL;
cis_db_for_hash[cis_db_index].elem[check_count + 1] = NULL;
}
if (wm_sca_check_hash(cis_db[cis_db_index], message_ref, check, event, check_count, cis_db_index) && !first_scan) {
wm_sca_send_event_check(data,event);
}
check_count++;
cJSON_Delete(event);
} else {
merror("Error constructing event for check: %s. Set debug mode for more information.", c_title->valuestring);
ret_val = 1;
}
int i;
for (i=0; data->alert_msg[i]; i++){
free(data->alert_msg[i]);
data->alert_msg[i] = NULL;
}
os_free(reason);
w_free_expression_t(®ex_engine);
}
*checks_number = check_count;
/* Clean up memory */
clean_return:
os_free(reason);
w_del_plist(p_list);
return ret_val;
}
迭代每一个检查项cJSON_ArrayForEach(check, checks)
/* Macro for iterating over an array or object */
#define cJSON_ArrayForEach(element, array) for(element = (array != NULL) ? (array)->child : NULL; element != NULL; element = element->next)