') {
return version_compare($version1, $version2, $operator);
}
}
if (!function_exists('db_fetch_cell_prepared')) {
function db_fetch_cell_prepared($sql, $params = [], $log = true) {
return '';
}
}
if (!function_exists('db_fetch_cell')) {
function db_fetch_cell($sql, $col = '', $log = true) {
return '';
}
}
if (!function_exists('db_fetch_row_prepared')) {
function db_fetch_row_prepared($sql, $params = [], $log = true) {
return [];
}
}
require_once CACTI_PATH_LIBRARY . '/import.php';
/* Pull every data_source_name, rrd_minimum and rrd_maximum out of the packages
Cacti ships, so the rules are measured against real template content. */
function shipped_data_source_item_values() : array {
static $values = null;
if ($values !== null) {
return $values;
}
$values = ['data_source_name' => [], 'rrd_minimum' => [], 'rrd_maximum' => []];
foreach (glob(CACTI_PATH_BASE . '/install/templates/*.xml.gz') as $package) {
$blob = file_get_contents($package);
if ($blob === false) {
continue;
}
$raw = gzdecode($blob);
if ($raw === false || !preg_match_all('#([A-Za-z0-9+/=\s]*)#s', $raw, $blocks)) {
continue;
}
foreach ($blocks[1] as $encoded) {
/* the capture allows the wrapping whitespace that strict decoding refuses */
$xml = base64_decode(preg_replace('/\s+/', '', $encoded), true);
if ($xml === false || strpos($xml, '') === false) {
continue;
}
foreach (array_keys($values) as $field) {
if (preg_match_all('#<' . $field . '>(.*?)' . $field . '>#s', $xml, $found)) {
foreach ($found[1] as $value) {
$values[$field][$value] = basename($package);
}
}
}
}
}
return $values;
}
test('every data source name in the shipped packages is accepted', function () {
$names = shipped_data_source_item_values();
expect(count($names['data_source_name']))->toBeGreaterThan(500);
foreach ($names['data_source_name'] as $name => $package) {
expect(import_validate_data_source_item('data_source_name', (string) $name))
->toBeTrue('rejected ' . $name . ' from ' . $package);
}
});
test('every minimum and maximum in the shipped packages is accepted', function () {
$values = shipped_data_source_item_values();
/* |query_ifSpeed| is a real rrd_maximum in ArubaOS_switch, so the rule
cannot be a plain numeric check. */
expect($values['rrd_maximum'])->toHaveKey('|query_ifSpeed|');
expect($values['rrd_maximum'])->toHaveKey('U');
foreach (['rrd_minimum', 'rrd_maximum'] as $field) {
foreach ($values[$field] as $value => $package) {
expect(import_validate_data_source_item($field, (string) $value))
->toBeTrue('rejected ' . $field . ' ' . $value . ' from ' . $package);
}
}
});
test('the remaining item fields are left to their column types', function () {
expect(import_validate_data_source_item('data_source_type_id', '1'))->toBeTrue();
expect(import_validate_data_source_item('rrd_heartbeat', '600'))->toBeTrue();
expect(import_validate_data_source_item('data_input_field_id', '17'))->toBeTrue();
});
test('shell metacharacters in a data source name are refused', function () {
expect(import_validate_data_source_item('data_source_name', 'ds;id'))->toBeFalse();
expect(import_validate_data_source_item('data_source_name', 'ds`id`'))->toBeFalse();
expect(import_validate_data_source_item('data_source_name', 'ds$(id)'))->toBeFalse();
expect(import_validate_data_source_item('data_source_name', 'ds|id'))->toBeFalse();
expect(import_validate_data_source_item('data_source_name', 'ds id'))->toBeFalse();
expect(import_validate_data_source_item('data_source_name', ''))->toBeFalse();
/* varchar(19) truncation would otherwise silently keep the leading bytes */
expect(import_validate_data_source_item('data_source_name', str_repeat('a', 20)))->toBeFalse();
});
test('shell metacharacters in a minimum or maximum are refused', function () {
expect(import_validate_data_source_item('rrd_maximum', '0;id;'))->toBeFalse();
expect(import_validate_data_source_item('rrd_maximum', '100 -x'))->toBeFalse();
expect(import_validate_data_source_item('rrd_maximum', '$(id)'))->toBeFalse();
expect(import_validate_data_source_item('rrd_maximum', '|query_evil|'))->toBeFalse();
expect(import_validate_data_source_item('rrd_minimum', '0`id`'))->toBeFalse();
expect(import_validate_data_source_item('rrd_minimum', 'U;id'))->toBeFalse();
});
test('a trailing newline does not slip past the anchors', function () {
expect(import_validate_data_source_item('data_source_name', "traffic_in\n"))->toBeFalse();
expect(import_validate_data_source_item('data_source_name', "traffic_in\nid"))->toBeFalse();
expect(import_validate_data_source_item('rrd_maximum', "U\n"))->toBeFalse();
expect(import_validate_data_source_item('rrd_maximum', "100\nid"))->toBeFalse();
expect(import_validate_data_source_item('rrd_minimum', "0\n"))->toBeFalse();
});
test('the data template import calls the validator before it saves', function () {
$src = file_get_contents(CACTI_PATH_LIBRARY . '/import.php');
$decode = strpos($src, '$save[$field_name] = xml_character_decode($item_array[$field_name]);');
$guard = strpos($src, 'if (!import_validate_data_source_item($field_name, $save[$field_name])) {');
$write = strpos($src, "sql_save(\$save, 'data_template_rrd')");
expect($decode)->not->toBeFalse();
expect($guard)->not->toBeFalse();
expect($write)->not->toBeFalse();
expect($guard)->toBeGreaterThan($decode);
expect($guard)->toBeLessThan($write);
});
test('a rejected data template aborts the import instead of adding false to the hash cache', function () {
$src = file_get_contents(CACTI_PATH_LIBRARY . '/import.php');
expect($src)->toContain('$cache_add = xml_to_data_template(');
expect($src)->not->toContain('$hash_cache += xml_to_data_template(');
});
/* Drive the real xml_to_data_template() item loop with the database stubbed out. */
function import_one_data_template(array $item) : mixed {
global $struct_data_source, $struct_data_source_item, $preview_only, $legacy_template;
global $hash_type_codes, $cacti_version_codes, $import_messages, $import_debug_info, $config;
/* keeps the rejection's cacti_log() call off the database */
$config['is_web'] = false;
$config['config_options_array'] = [
'log_verbosity' => POLLER_VERBOSITY_NONE,
'log_destination' => 1,
'selective_debug' => '',
'selective_plugin_debug' => ''
];
$preview_only = true;
$legacy_template = false;
$import_messages = [];
$import_debug_info = [];
$hash_type_codes = ['data_template_item' => '08'];
$cacti_version_codes = [CACTI_VERSION => '0400'];
$struct_data_source = [];
$struct_data_source_item = [
'data_source_name' => [],
'rrd_minimum' => [],
'rrd_maximum' => [],
'data_source_type_id' => [],
'rrd_heartbeat' => [],
'data_input_field_id' => []
];
$item_hash = 'hash_080400' . str_repeat('a', 32);
$xml_array = ['name' => 'Test Template', 'ds' => [], 'items' => [$item_hash => $item]];
$hash_cache = [];
return xml_to_data_template(str_repeat('b', 32), $xml_array, $hash_cache, true, 1);
}
function import_item(array $overrides) : array {
return $overrides + [
'data_source_name' => 'traffic_in',
'rrd_minimum' => '0',
'rrd_maximum' => 'U',
'data_source_type_id' => '2',
'rrd_heartbeat' => '600'
];
}
test('the driven field list still matches global_form.php', function () {
$form = file_get_contents(CACTI_PATH_INCLUDE . '/global_form.php');
$item = substr($form, strpos($form, '$struct_data_source_item = ['));
$item = substr($item, 0, strpos($item, "\n];"));
foreach (['data_source_name', 'rrd_minimum', 'rrd_maximum', 'data_source_type_id', 'rrd_heartbeat', 'data_input_field_id'] as $field) {
expect($item)->toContain("'" . $field . "' => [");
}
});
test('a clean data source item is accepted by the item loop', function () {
$result = import_one_data_template(import_item(['rrd_maximum' => '|query_ifSpeed|']));
expect($result)->toBeArray();
expect($result)->toHaveKey('data_template_item');
expect($GLOBALS['import_messages'])->toBe([]);
});
test('an injected data source name aborts the item loop', function () {
$result = import_one_data_template(import_item(['data_source_name' => 'ds;id;']));
expect($result)->toBeFalse();
expect($GLOBALS['import_messages'])->toContain(45);
});
test('an injected maximum aborts the item loop', function () {
$result = import_one_data_template(import_item(['rrd_maximum' => "U\n; touch /tmp/pwned"]));
expect($result)->toBeFalse();
expect($GLOBALS['import_messages'])->toContain(45);
});
test('an injected minimum aborts the item loop', function () {
$result = import_one_data_template(import_item(['rrd_minimum' => '0`id`']));
expect($result)->toBeFalse();
expect($GLOBALS['import_messages'])->toContain(45);
});