当前位置: 首页 > article >正文

wazuh-modules-sca-scan

 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(&regex_engine, EXP_TYPE_PCRE2);
            } else {
                w_calloc_expression_t(&regex_engine, EXP_TYPE_OSREGEX);
            }
        } else {
            if(strcmp(PCRE2_STR, policy_engine) == 0) {
                w_calloc_expression_t(&regex_engine, EXP_TYPE_PCRE2);
            } else {
                w_calloc_expression_t(&regex_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(&regex_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(&regex_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)


http://www.kler.cn/a/443092.html

相关文章:

  • 人工智能学习路线全链路解析
  • 美国大学的计算机科学专业排名
  • CompletableFuture // todo
  • Linux内核TTY子系统有什么(6)
  • OpenPCDet从环境配置到模型训练
  • 如何设计一个注册中心?以Zookeeper为例
  • 安装MetaMask钱包、创建新钱包、切换到以太坊主网、进行钱包充值以及转出以太资产
  • 一个开源的自托管虚拟浏览器项目,支持在安全、私密的环境中使用浏览器
  • 自动呼入机器人如何与人工客服进行无缝切换?
  • windows C#-本地函数
  • Java系统对接企业微信审批项目流程
  • jmeter连接mysql
  • fastAPI接口的请求与响应——基础
  • ArkTs组件的学习
  • Vue 2 中页面跳转方式的详细介绍
  • 如何在 Ubuntu 22.04 上使用 vnStat 监控网络流量
  • java 通过jdbc连接sql2000方法
  • JS 生成防篡改水印
  • OCR多模态大模型:视觉模型与LLM的结合之路
  • PDFMathTranslate 一个基于AI优秀的PDF论文翻译工具
  • 知识库管理系统可扩展性深度测评
  • 【论文笔记】Visual Prompt Tuning
  • 《自制编译器》--青木峰郎 -读书笔记 编译hello
  • 三维测量与建模笔记 - 7.2 点云滤波
  • mapper.xml传入参数为Map的正确做法
  • springboot使用scoket