mirror of
https://github.com/Cacti/cacti.git
synced 2026-10-23 03:27:02 +09:00
hardening(rendering): enforce changed-file guardrails (#7255)
* hardening(rendering): enforce changed-file guardrails * fix(ci): align CodeQL autobuild action * fix(rendering): address guard review feedback
This commit is contained in:
1 parent
d151a3875b
commit
8dc1f677dd
8 files changed
+395
-54
No files matched your search
@@ -40,7 +40,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v3
|
uses: github/codeql-action/autobuild@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v3
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v3
|
uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v3
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Install PHP ${{ matrix.php }}
|
- name: Install PHP ${{ matrix.php }}
|
||||||
uses: shivammathur/setup-php@v2
|
uses: shivammathur/setup-php@v2
|
||||||
@@ -113,6 +115,11 @@ jobs:
|
|||||||
run: composer run-script lint
|
run: composer run-script lint
|
||||||
working-directory: .
|
working-directory: .
|
||||||
|
|
||||||
|
- name: Run hardening pattern guard
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
run: composer run-script hardening-patterns -- --base="${{ github.event.pull_request.base.sha }}" --head=HEAD
|
||||||
|
working-directory: .
|
||||||
|
|
||||||
- name: Checking coding standards on base code
|
- name: Checking coding standards on base code
|
||||||
run: composer run-script phpcsfixer
|
run: composer run-script phpcsfixer
|
||||||
working-directory: .
|
working-directory: .
|
||||||
|
|||||||
@@ -74,6 +74,7 @@
|
|||||||
"phpstan": "phpstan analyse --level 6 ",
|
"phpstan": "phpstan analyse --level 6 ",
|
||||||
"php-cs-fixer": "php-cs-fixer fix --allow-unsupported-php-version=yes --dry-run --diff --config=./.php-cs-fixer.php ",
|
"php-cs-fixer": "php-cs-fixer fix --allow-unsupported-php-version=yes --dry-run --diff --config=./.php-cs-fixer.php ",
|
||||||
"phpcsfixer": "@php-cs-fixer",
|
"phpcsfixer": "@php-cs-fixer",
|
||||||
|
"hardening-patterns": "php tests/tools/check_hardening_patterns.php",
|
||||||
"php-cs-fixit": "php-cs-fixer fix ",
|
"php-cs-fixit": "php-cs-fixer fix ",
|
||||||
"install-hooks": "bash -c 'install -m 755 tests/tools/pre-commit \"$(git rev-parse --git-path hooks/pre-commit)\"'",
|
"install-hooks": "bash -c 'install -m 755 tests/tools/pre-commit \"$(git rev-parse --git-path hooks/pre-commit)\"'",
|
||||||
"test": "include/vendor/bin/pest --display-warnings"
|
"test": "include/vendor/bin/pest --display-warnings"
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
+-------------------------------------------------------------------------+
|
||||||
|
| Copyright (C) 2004-2026 The Cacti Group |
|
||||||
|
| |
|
||||||
|
| This program is free software; you can redistribute it and/or |
|
||||||
|
| modify it under the terms of the GNU General Public License |
|
||||||
|
| as published by the Free Software Foundation; either version 2 |
|
||||||
|
| of the License, or (at your option) any later version. |
|
||||||
|
+-------------------------------------------------------------------------+
|
||||||
|
| Cacti: The Complete RRDtool-based Graphing Solution |
|
||||||
|
+-------------------------------------------------------------------------+
|
||||||
|
*/
|
||||||
|
|
||||||
|
final class PackageListFilter {
|
||||||
|
private string $filter;
|
||||||
|
private int $rows;
|
||||||
|
private int $page;
|
||||||
|
private string $sortColumn;
|
||||||
|
private string $sortDirection;
|
||||||
|
|
||||||
|
private function __construct(string $filter, int $rows, int $page, string $sortColumn, string $sortDirection) {
|
||||||
|
$this->filter = $filter;
|
||||||
|
$this->rows = $rows;
|
||||||
|
$this->page = max(1, $page);
|
||||||
|
$this->sortColumn = $sortColumn;
|
||||||
|
$this->sortDirection = $sortDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function fromRequest(int $defaultRows) : self {
|
||||||
|
$rows = (string) grv('rows');
|
||||||
|
$rows = $rows === '-1' ? $defaultRows : (int) $rows;
|
||||||
|
|
||||||
|
if ($rows < 1) {
|
||||||
|
$rows = $defaultRows;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new self(
|
||||||
|
(string) grv('filter'),
|
||||||
|
$rows,
|
||||||
|
(int) grv('page'),
|
||||||
|
(string) grv('sort_column'),
|
||||||
|
(string) grv('sort_direction')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function filter() : string {
|
||||||
|
return $this->filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasFilter() : bool {
|
||||||
|
return $this->filter !== '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rows() : int {
|
||||||
|
return $this->rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function page() : int {
|
||||||
|
return $this->page;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sortColumn() : string {
|
||||||
|
return $this->sortColumn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sortDirection() : string {
|
||||||
|
return $this->sortDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function offset() : int {
|
||||||
|
return $this->rows * ($this->page - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function paginationUrl(string $path) : string {
|
||||||
|
if ($this->filter === '') {
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $path . '?' . http_build_query(['filter' => $this->filter], '', '&', PHP_QUERY_RFC3986);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function selectedIdsFromPost() : array {
|
||||||
|
return self::selectedIdsFromInput($_POST);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function selectedIdsFromInput(array $input) : array {
|
||||||
|
$selected = [];
|
||||||
|
|
||||||
|
foreach ($input as $var => $value) {
|
||||||
|
if (preg_match('/^chk_([0-9]+)$/', (string) $var, $matches)) {
|
||||||
|
$selected[] = (int) $matches[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function encodeSelectedIds(array $ids) : string {
|
||||||
|
return json_encode(array_values(array_map('intval', $ids)), JSON_THROW_ON_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function decodeSelectedIds(mixed $ids) : array|false {
|
||||||
|
if (!is_string($ids) || $ids === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$decoded = json_decode(stripslashes($ids), true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
} catch (JsonException) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_array($decoded)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$selected = [];
|
||||||
|
|
||||||
|
foreach ($decoded as $id) {
|
||||||
|
if (!is_int($id) && !(is_string($id) && preg_match('/^[0-9]+$/', $id))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$selected[] = (int) $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $selected;
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
-27
@@ -23,6 +23,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
require('./include/auth.php');
|
require('./include/auth.php');
|
||||||
|
require_once(CACTI_PATH_LIBRARY . '/PackageListFilter.php');
|
||||||
require_once(CACTI_PATH_LIBRARY . '/poller.php');
|
require_once(CACTI_PATH_LIBRARY . '/poller.php');
|
||||||
require_once(CACTI_PATH_LIBRARY . '/utility.php');
|
require_once(CACTI_PATH_LIBRARY . '/utility.php');
|
||||||
|
|
||||||
@@ -53,7 +54,7 @@ function form_actions() : void {
|
|||||||
|
|
||||||
// if we are to save this form, instead of display it
|
// if we are to save this form, instead of display it
|
||||||
if (isrv('selected_items')) {
|
if (isrv('selected_items')) {
|
||||||
$selected_items = sanitize_unserialize_selected_items(gnrv('selected_items'));
|
$selected_items = PackageListFilter::decodeSelectedIds(gnrv('selected_items'));
|
||||||
|
|
||||||
if ($selected_items != false) {
|
if ($selected_items != false) {
|
||||||
if (gnrv('drp_action') == '1') { // delete
|
if (gnrv('drp_action') == '1') { // delete
|
||||||
@@ -72,16 +73,9 @@ function form_actions() : void {
|
|||||||
$p_list = '';
|
$p_list = '';
|
||||||
$p_array = [];
|
$p_array = [];
|
||||||
|
|
||||||
// loop through each of the data queries and process them
|
foreach (PackageListFilter::selectedIdsFromPost() as $id) {
|
||||||
foreach ($_POST as $var => $val) {
|
$p_list .= '<li>' . htmle(db_fetch_cell_prepared('SELECT author FROM package_public_keys WHERE id = ?', [$id])) . '</li>';
|
||||||
if (preg_match('/^chk_([0-9]+)$/', $var, $matches)) {
|
$p_array[] = $id;
|
||||||
// ================= input validation =================
|
|
||||||
input_validate_input_number($matches[1]);
|
|
||||||
// ====================================================
|
|
||||||
|
|
||||||
$p_list .= '<li>' . htmle(db_fetch_cell_prepared('SELECT author FROM package_public_keys WHERE id = ?', [$matches[1]])) . '</li>';
|
|
||||||
$p_array[] = $matches[1];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
top_header();
|
top_header();
|
||||||
@@ -113,7 +107,7 @@ function form_actions() : void {
|
|||||||
print "<tr>
|
print "<tr>
|
||||||
<td class='saveRow'>
|
<td class='saveRow'>
|
||||||
<input type='hidden' name='action' value='actions'>
|
<input type='hidden' name='action' value='actions'>
|
||||||
<input type='hidden' name='selected_items' value='" . serialize($p_array) . "'>
|
<input type='hidden' name='selected_items' value='" . htmle(PackageListFilter::encodeSelectedIds($p_array)) . "'>
|
||||||
<input type='hidden' name='drp_action' value='" . htmle(gnrv('drp_action')) . "'>
|
<input type='hidden' name='drp_action' value='" . htmle(gnrv('drp_action')) . "'>
|
||||||
$save_html
|
$save_html
|
||||||
</td>
|
</td>
|
||||||
@@ -140,27 +134,23 @@ function public_keys() : void {
|
|||||||
$pageFilter->set_sort_array('author', 'ASC');
|
$pageFilter->set_sort_array('author', 'ASC');
|
||||||
$pageFilter->render();
|
$pageFilter->render();
|
||||||
|
|
||||||
if (grv('rows') == '-1') {
|
$filter = PackageListFilter::fromRequest((int) read_config_option('num_rows_table'));
|
||||||
$rows = read_config_option('num_rows_table');
|
|
||||||
} else {
|
|
||||||
$rows = grv('rows');
|
|
||||||
}
|
|
||||||
|
|
||||||
$sql_where = '';
|
$sql_where = '';
|
||||||
$sql_params = [];
|
$sql_params = [];
|
||||||
|
|
||||||
// form the 'where' clause for our main sql query
|
// form the 'where' clause for our main sql query
|
||||||
if (grv('filter') != '') {
|
if ($filter->hasFilter()) {
|
||||||
$sql_where = ($sql_where != '' ? ' AND ' : 'WHERE ') .
|
$sql_where = ($sql_where != '' ? ' AND ' : 'WHERE ') .
|
||||||
'(author LIKE ? OR homepage LIKE ? OR email_address LIKE ?)';
|
'(author LIKE ? OR homepage LIKE ? OR email_address LIKE ?)';
|
||||||
|
|
||||||
$sql_params[] = '%' . grv('filter') . '%';
|
$sql_params[] = '%' . $filter->filter() . '%';
|
||||||
$sql_params[] = '%' . grv('filter') . '%';
|
$sql_params[] = '%' . $filter->filter() . '%';
|
||||||
$sql_params[] = '%' . grv('filter') . '%';
|
$sql_params[] = '%' . $filter->filter() . '%';
|
||||||
}
|
}
|
||||||
|
|
||||||
$sql_order = get_order_string();
|
$sql_order = get_order_string();
|
||||||
$sql_limit = ' LIMIT ' . ($rows * (grv('page') - 1)) . ',' . $rows;
|
$sql_limit = ' LIMIT ' . $filter->offset() . ',' . $filter->rows();
|
||||||
|
|
||||||
$total_rows = db_fetch_cell_prepared("SELECT COUNT(*)
|
$total_rows = db_fetch_cell_prepared("SELECT COUNT(*)
|
||||||
FROM package_public_keys
|
FROM package_public_keys
|
||||||
@@ -197,7 +187,7 @@ function public_keys() : void {
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
$nav = html_nav_bar('package_keys.php?filter=' . grv('filter'), MAX_DISPLAY_PAGES, grv('page'), $rows, $total_rows, sizeof($display_text) + 1, __('Package Public Keys'), 'page', 'main');
|
$nav = html_nav_bar($filter->paginationUrl('package_keys.php'), MAX_DISPLAY_PAGES, $filter->page(), $filter->rows(), $total_rows, cacti_sizeof($display_text) + 1, __('Package Public Keys'), 'page', 'main');
|
||||||
|
|
||||||
form_start('package_keys.php', 'chk');
|
form_start('package_keys.php', 'chk');
|
||||||
|
|
||||||
@@ -205,7 +195,7 @@ function public_keys() : void {
|
|||||||
|
|
||||||
html_start_box('', '100%', false, 3, 'center', '');
|
html_start_box('', '100%', false, 3, 'center', '');
|
||||||
|
|
||||||
html_header_sort_checkbox($display_text, grv('sort_column'), grv('sort_direction'), false);
|
html_header_sort_checkbox($display_text, $filter->sortColumn(), $filter->sortDirection(), false);
|
||||||
|
|
||||||
$i = 0;
|
$i = 0;
|
||||||
|
|
||||||
@@ -215,10 +205,10 @@ function public_keys() : void {
|
|||||||
|
|
||||||
$pkey = $key['public_key'];
|
$pkey = $key['public_key'];
|
||||||
|
|
||||||
form_selectable_cell(filter_value($key['author'], grv('filter')), $key['id']);
|
form_selectable_cell(filter_value($key['author'], $filter->filter()), $key['id']);
|
||||||
form_selectable_cell($key['id'], $key['id']);
|
form_selectable_cell($key['id'], $key['id']);
|
||||||
form_selectable_cell(filter_value($key['homepage'], grv('filter')), $key['id']);
|
form_selectable_cell(filter_value($key['homepage'], $filter->filter()), $key['id']);
|
||||||
form_selectable_cell(filter_value($key['email_address'], grv('filter')), $key['id']);
|
form_selectable_cell(filter_value($key['email_address'], $filter->filter()), $key['id']);
|
||||||
form_selectable_cell(strlen($pkey) < 200 ? 'SHA1' : 'SHA256', $key['id']);
|
form_selectable_cell(strlen($pkey) < 200 ? 'SHA1' : 'SHA256', $key['id']);
|
||||||
|
|
||||||
form_checkbox_cell($key['author'], $key['id']);
|
form_checkbox_cell($key['author'], $key['id']);
|
||||||
|
|||||||
+16
-26
@@ -23,6 +23,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
require('./include/auth.php');
|
require('./include/auth.php');
|
||||||
|
require_once(CACTI_PATH_LIBRARY . '/PackageListFilter.php');
|
||||||
require_once(CACTI_PATH_LIBRARY . '/poller.php');
|
require_once(CACTI_PATH_LIBRARY . '/poller.php');
|
||||||
require_once(CACTI_PATH_LIBRARY . '/utility.php');
|
require_once(CACTI_PATH_LIBRARY . '/utility.php');
|
||||||
|
|
||||||
@@ -164,7 +165,7 @@ function form_actions() : void {
|
|||||||
|
|
||||||
// if we are to save this form, instead of display it
|
// if we are to save this form, instead of display it
|
||||||
if (isrv('selected_items')) {
|
if (isrv('selected_items')) {
|
||||||
$selected_items = sanitize_unserialize_selected_items(gnrv('selected_items'));
|
$selected_items = PackageListFilter::decodeSelectedIds(gnrv('selected_items'));
|
||||||
|
|
||||||
if ($selected_items != false) {
|
if ($selected_items != false) {
|
||||||
if (gnrv('drp_action') == '1') { // delete
|
if (gnrv('drp_action') == '1') { // delete
|
||||||
@@ -199,16 +200,9 @@ function form_actions() : void {
|
|||||||
$p_list = '';
|
$p_list = '';
|
||||||
$p_array = [];
|
$p_array = [];
|
||||||
|
|
||||||
// loop through each of the data queries and process them
|
foreach (PackageListFilter::selectedIdsFromPost() as $id) {
|
||||||
foreach ($_POST as $var => $val) {
|
$p_list .= '<li>' . htmle(db_fetch_cell_prepared('SELECT name FROM package_repositories WHERE id = ?', [$id])) . '</li>';
|
||||||
if (preg_match('/^chk_([0-9]+)$/', $var, $matches)) {
|
$p_array[] = $id;
|
||||||
// ================= input validation =================
|
|
||||||
input_validate_input_number($matches[1]);
|
|
||||||
// ====================================================
|
|
||||||
|
|
||||||
$p_list .= '<li>' . htmle(db_fetch_cell_prepared('SELECT name FROM package_repositories WHERE id = ?', [$matches[1]])) . '</li>';
|
|
||||||
$p_array[] = $matches[1];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
top_header();
|
top_header();
|
||||||
@@ -271,7 +265,7 @@ function form_actions() : void {
|
|||||||
print "<tr>
|
print "<tr>
|
||||||
<td class='saveRow'>
|
<td class='saveRow'>
|
||||||
<input type='hidden' name='action' value='actions'>
|
<input type='hidden' name='action' value='actions'>
|
||||||
<input type='hidden' name='selected_items' value='" . serialize($p_array) . "'>
|
<input type='hidden' name='selected_items' value='" . htmle(PackageListFilter::encodeSelectedIds($p_array)) . "'>
|
||||||
<input type='hidden' name='drp_action' value='" . htmle(gnrv('drp_action')) . "'>
|
<input type='hidden' name='drp_action' value='" . htmle(gnrv('drp_action')) . "'>
|
||||||
$save_html
|
$save_html
|
||||||
</td>
|
</td>
|
||||||
@@ -438,27 +432,23 @@ function repos() : void {
|
|||||||
$pageFilter->rows_label = __('Repos');
|
$pageFilter->rows_label = __('Repos');
|
||||||
$pageFilter->render();
|
$pageFilter->render();
|
||||||
|
|
||||||
if (grv('rows') == '-1') {
|
$filter = PackageListFilter::fromRequest((int) read_config_option('num_rows_table'));
|
||||||
$rows = read_config_option('num_rows_table');
|
|
||||||
} else {
|
|
||||||
$rows = grv('rows');
|
|
||||||
}
|
|
||||||
|
|
||||||
$sql_where = '';
|
$sql_where = '';
|
||||||
$sql_params = [];
|
$sql_params = [];
|
||||||
|
|
||||||
// form the 'where' clause for our main sql query
|
// form the 'where' clause for our main sql query
|
||||||
if (grv('filter') != '') {
|
if ($filter->hasFilter()) {
|
||||||
$sql_where = ($sql_where != '' ? ' AND ' : 'WHERE ') .
|
$sql_where = ($sql_where != '' ? ' AND ' : 'WHERE ') .
|
||||||
'(name LIKE ? OR repo_branch LIKE ? OR repo_location LIKE ?)';
|
'(name LIKE ? OR repo_branch LIKE ? OR repo_location LIKE ?)';
|
||||||
|
|
||||||
$sql_params[] = '%' . grv('filter') . '%';
|
$sql_params[] = '%' . $filter->filter() . '%';
|
||||||
$sql_params[] = '%' . grv('filter') . '%';
|
$sql_params[] = '%' . $filter->filter() . '%';
|
||||||
$sql_params[] = '%' . grv('filter') . '%';
|
$sql_params[] = '%' . $filter->filter() . '%';
|
||||||
}
|
}
|
||||||
|
|
||||||
$sql_order = get_order_string();
|
$sql_order = get_order_string();
|
||||||
$sql_limit = ' LIMIT ' . ($rows * (grv('page') - 1)) . ',' . $rows;
|
$sql_limit = ' LIMIT ' . $filter->offset() . ',' . $filter->rows();
|
||||||
|
|
||||||
$total_rows = db_fetch_cell_prepared("SELECT COUNT(*)
|
$total_rows = db_fetch_cell_prepared("SELECT COUNT(*)
|
||||||
FROM package_repositories
|
FROM package_repositories
|
||||||
@@ -502,7 +492,7 @@ function repos() : void {
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
$nav = html_nav_bar('package_repos.php?filter=' . grv('filter'), MAX_DISPLAY_PAGES, grv('page'), $rows, $total_rows, sizeof($display_text) + 1, __('Package Repositories'), 'page', 'main');
|
$nav = html_nav_bar($filter->paginationUrl('package_repos.php'), MAX_DISPLAY_PAGES, $filter->page(), $filter->rows(), $total_rows, cacti_sizeof($display_text) + 1, __('Package Repositories'), 'page', 'main');
|
||||||
|
|
||||||
form_start('package_repos.php', 'chk');
|
form_start('package_repos.php', 'chk');
|
||||||
|
|
||||||
@@ -510,15 +500,15 @@ function repos() : void {
|
|||||||
|
|
||||||
html_start_box('', '100%', false, 3, 'center', '');
|
html_start_box('', '100%', false, 3, 'center', '');
|
||||||
|
|
||||||
html_header_sort_checkbox($display_text, grv('sort_column'), grv('sort_direction'), false);
|
html_header_sort_checkbox($display_text, $filter->sortColumn(), $filter->sortDirection(), false);
|
||||||
|
|
||||||
if (cacti_sizeof($repos)) {
|
if (cacti_sizeof($repos)) {
|
||||||
foreach ($repos as $repo) {
|
foreach ($repos as $repo) {
|
||||||
form_alternate_row('line' . $repo['id'], true);
|
form_alternate_row('line' . $repo['id'], true);
|
||||||
|
|
||||||
form_selectable_cell(filter_value($repo['name'], grv('filter'), 'package_repos.php?action=edit&id=' . $repo['id']), $repo['id']);
|
form_selectable_cell(filter_value($repo['name'], $filter->filter(), 'package_repos.php?action=edit&id=' . $repo['id']), $repo['id']);
|
||||||
form_selectable_cell($types[$repo['repo_type']], $repo['id']);
|
form_selectable_cell($types[$repo['repo_type']], $repo['id']);
|
||||||
form_selectable_cell(filter_value($repo['repo_location'], grv('filter')), $repo['id']);
|
form_selectable_cell(filter_value($repo['repo_location'], $filter->filter()), $repo['id']);
|
||||||
form_selectable_ecell($repo['repo_type'] == 0 ? ($repo['repo_branch'] != '' ? $repo['repo_branch'] : __('default')) : __('N/A'), $repo['id'], '', 'center');
|
form_selectable_ecell($repo['repo_type'] == 0 ? ($repo['repo_branch'] != '' ? $repo['repo_branch'] : __('default')) : __('N/A'), $repo['id'], '', 'center');
|
||||||
form_selectable_cell($repo['enabled'] == 'on' ? __('Yes') : __('No'), $repo['id'], '', 'center');
|
form_selectable_cell($repo['enabled'] == 'on' ? __('Yes') : __('No'), $repo['id'], '', 'center');
|
||||||
form_selectable_cell($repo['default'] == 'on' ? __('Yes') : __('No'), $repo['id'], '', 'center');
|
form_selectable_cell($repo['default'] == 'on' ? __('Yes') : __('No'), $repo['id'], '', 'center');
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
+-------------------------------------------------------------------------+
|
||||||
|
| Copyright (C) 2004-2026 The Cacti Group |
|
||||||
|
| |
|
||||||
|
| This program is free software; you can redistribute it and/or |
|
||||||
|
| modify it under the terms of the GNU General Public License |
|
||||||
|
| as published by the Free Software Foundation; either version 2 |
|
||||||
|
| of the License, or (at your option) any later version. |
|
||||||
|
+-------------------------------------------------------------------------+
|
||||||
|
| Cacti: The Complete RRDtool-based Graphing Solution |
|
||||||
|
+-------------------------------------------------------------------------+
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once dirname(__DIR__) . '/Helpers/CactiStubs.php';
|
||||||
|
require_once dirname(__DIR__, 2) . '/include/global.php';
|
||||||
|
require_once dirname(__DIR__, 2) . '/lib/PackageListFilter.php';
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
global $_CACTI_REQUEST;
|
||||||
|
|
||||||
|
$_REQUEST = [];
|
||||||
|
$_GET = [];
|
||||||
|
$_POST = [];
|
||||||
|
$_CACTI_REQUEST = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
test('package list filter uses default row count when rows are defaulted', function () {
|
||||||
|
set_request_var('filter', 'alpha');
|
||||||
|
set_request_var('rows', '-1');
|
||||||
|
set_request_var('page', '2');
|
||||||
|
set_request_var('sort_column', 'name');
|
||||||
|
set_request_var('sort_direction', 'ASC');
|
||||||
|
|
||||||
|
$filter = PackageListFilter::fromRequest(25);
|
||||||
|
|
||||||
|
expect($filter->filter())->toBe('alpha')
|
||||||
|
->and($filter->rows())->toBe(25)
|
||||||
|
->and($filter->page())->toBe(2)
|
||||||
|
->and($filter->offset())->toBe(25);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('package list filter clamps non-positive rows to the default row count', function () {
|
||||||
|
set_request_var('filter', 'alpha');
|
||||||
|
set_request_var('rows', '0');
|
||||||
|
set_request_var('page', '2');
|
||||||
|
set_request_var('sort_column', 'name');
|
||||||
|
set_request_var('sort_direction', 'ASC');
|
||||||
|
|
||||||
|
$filter = PackageListFilter::fromRequest(25);
|
||||||
|
|
||||||
|
expect($filter->rows())->toBe(25)
|
||||||
|
->and($filter->offset())->toBe(25);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('package list filter encodes pagination query values', function () {
|
||||||
|
set_request_var('filter', 'a b&c');
|
||||||
|
set_request_var('rows', '10');
|
||||||
|
set_request_var('page', '1');
|
||||||
|
set_request_var('sort_column', 'name');
|
||||||
|
set_request_var('sort_direction', 'ASC');
|
||||||
|
|
||||||
|
$filter = PackageListFilter::fromRequest(25);
|
||||||
|
|
||||||
|
expect($filter->paginationUrl('package_repos.php'))->toBe('package_repos.php?filter=a%20b%26c');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('package list filter extracts selected checkbox ids', function () {
|
||||||
|
$selected = PackageListFilter::selectedIdsFromInput([
|
||||||
|
'chk_12' => 'on',
|
||||||
|
'chk_x' => 'on',
|
||||||
|
'other' => 'on',
|
||||||
|
'chk_34' => 'on',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($selected)->toBe([12, 34]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('package list filter decodes only numeric selected ids', function () {
|
||||||
|
$encoded = PackageListFilter::encodeSelectedIds([3, '7']);
|
||||||
|
|
||||||
|
expect(PackageListFilter::decodeSelectedIds($encoded))->toBe([3, 7])
|
||||||
|
->and(PackageListFilter::decodeSelectedIds('["3","bad"]'))->toBeFalse()
|
||||||
|
->and(PackageListFilter::decodeSelectedIds('not json'))->toBeFalse();
|
||||||
|
});
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
+-------------------------------------------------------------------------+
|
||||||
|
| Copyright (C) 2004-2026 The Cacti Group |
|
||||||
|
| |
|
||||||
|
| This program is free software; you can redistribute it and/or |
|
||||||
|
| modify it under the terms of the GNU General Public License |
|
||||||
|
| as published by the Free Software Foundation; either version 2 |
|
||||||
|
| of the License, or (at your option) any later version. |
|
||||||
|
+-------------------------------------------------------------------------+
|
||||||
|
| Cacti: The Complete RRDtool-based Graphing Solution |
|
||||||
|
+-------------------------------------------------------------------------+
|
||||||
|
*/
|
||||||
|
|
||||||
|
$options = getopt('', ['base:', 'head:']);
|
||||||
|
$base = $options['base'] ?? '';
|
||||||
|
$head = $options['head'] ?? 'HEAD';
|
||||||
|
$files = cli_file_args($argv);
|
||||||
|
|
||||||
|
if ($files === []) {
|
||||||
|
$files = changed_php_files($base, $head);
|
||||||
|
}
|
||||||
|
|
||||||
|
$patterns = [
|
||||||
|
'raw-superglobal-request' => [
|
||||||
|
'pattern' => '/\$_(?:GET|POST|REQUEST)\s*\[/',
|
||||||
|
'message' => 'Use Cacti request helpers or typed filter objects instead of raw request superglobals.',
|
||||||
|
'allow' => [
|
||||||
|
'lib/PackageListFilter.php',
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'manual-request-query-url' => [
|
||||||
|
'pattern' => '/[?&][A-Za-z0-9_-]+=\s*[\'"]?\s*\.\s*g(?:r|fr|nr)v\s*\(/',
|
||||||
|
'message' => 'Use a URL builder or typed page-state URL method instead of concatenating request values into query strings.'
|
||||||
|
],
|
||||||
|
'inline-js-php-string' => [
|
||||||
|
'pattern' => '/(?:var|let|const)\s+[A-Za-z0-9_$]+\s*=\s*[\'"][^\'"]*<\?php\s+print\s+/',
|
||||||
|
'message' => 'Use JSON encoding for PHP values injected into JavaScript.'
|
||||||
|
],
|
||||||
|
'raw-request-value-attribute' => [
|
||||||
|
'pattern' => '/value\s*=\s*[\'"][^\'"]*<\?php\s+print\s+g(?:r|fr|nr)v\s*\(/',
|
||||||
|
'message' => 'Use form rendering helpers for request-backed input values.'
|
||||||
|
],
|
||||||
|
'serialized-hidden-state' => [
|
||||||
|
'pattern' => '/(?:type\s*=\s*[\'"]hidden[\'"].*serialize\s*\(|html_hidden_input\s*\([^;]*serialize\s*\()/',
|
||||||
|
'message' => 'Do not round-trip serialized PHP state through hidden fields.'
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$violations = [];
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if (!is_file($file) || pathinfo($file, PATHINFO_EXTENSION) !== 'php') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = file($file, FILE_IGNORE_NEW_LINES);
|
||||||
|
|
||||||
|
if ($lines === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($lines as $index => $line) {
|
||||||
|
foreach ($patterns as $name => $rule) {
|
||||||
|
if (isset($rule['allow']) && in_array($file, $rule['allow'], true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match($rule['pattern'], $line) === 1) {
|
||||||
|
$violations[] = sprintf('%s:%d: %s: %s', $file, $index + 1, $name, $rule['message']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($violations !== []) {
|
||||||
|
fwrite(STDERR, "Hardening pattern guard found unsafe changed-file patterns:\n");
|
||||||
|
fwrite(STDERR, implode("\n", $violations) . "\n");
|
||||||
|
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
exit(0);
|
||||||
|
|
||||||
|
function changed_php_files(string $base, string $head) : array {
|
||||||
|
if ($base === '') {
|
||||||
|
$base = trim((string) shell_exec('git merge-base HEAD upstream/develop 2>/dev/null'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($base === '') {
|
||||||
|
$base = trim((string) shell_exec('git merge-base HEAD origin/develop 2>/dev/null'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($base === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$command = sprintf(
|
||||||
|
'git diff --name-only --diff-filter=ACMR %s...%s -- %s 2>/dev/null',
|
||||||
|
escapeshellarg($base),
|
||||||
|
escapeshellarg($head),
|
||||||
|
escapeshellarg('*.php')
|
||||||
|
);
|
||||||
|
|
||||||
|
$output = shell_exec($command);
|
||||||
|
|
||||||
|
if (!is_string($output) || trim($output) === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return preg_split('/\R/', trim($output)) ?: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function cli_file_args(array $argv) : array {
|
||||||
|
$files = [];
|
||||||
|
|
||||||
|
for ($i = 1; $i < count($argv); $i++) {
|
||||||
|
$arg = $argv[$i];
|
||||||
|
|
||||||
|
if ($arg === '--base' || $arg === '--head') {
|
||||||
|
$i++;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_starts_with($arg, '--base=') || str_starts_with($arg, '--head=')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_starts_with($arg, '--')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$files[] = $arg;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $files;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user