mirror of
https://github.com/itflow-org/itflow
synced 2026-09-01 12:25:11 +00:00
Feature: Added network IPs section to document network IP and their hostnames
This commit is contained in:
27
admin/database_updates/2.7.2.php
Normal file
27
admin/database_updates/2.7.2.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.7.2 (from 2.7.1)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// Documented IP addresses, hanging off the network (subnet) they belong to.
|
||||
// Addresses are stored canonicalised (inet_ntop form) so the UNIQUE key
|
||||
// below actually catches the same address entered two different ways.
|
||||
//
|
||||
// The foreign key means deleting a network takes its addresses with it -
|
||||
// agent/post/network.php's delete handler needs no change.
|
||||
mysqli_query($mysqli, "CREATE TABLE IF NOT EXISTS `network_ips` (
|
||||
`ip_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`ip_address` varchar(45) NOT NULL,
|
||||
`ip_hostname` varchar(200) DEFAULT NULL,
|
||||
`ip_description` varchar(200) DEFAULT NULL,
|
||||
`ip_created_at` datetime NOT NULL DEFAULT current_timestamp(),
|
||||
`ip_updated_at` datetime DEFAULT NULL ON UPDATE current_timestamp(),
|
||||
`ip_network_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`ip_id`),
|
||||
UNIQUE KEY `ip_network_address` (`ip_network_id`,`ip_address`),
|
||||
CONSTRAINT `network_ips_ibfk_1` FOREIGN KEY (`ip_network_id`) REFERENCES `networks` (`network_id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
|
||||
65
agent/modals/network/network_ip_add.php
Normal file
65
agent/modals/network/network_ip_add.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
require_once '../../../includes/modal_header.php';
|
||||
|
||||
enforceUserPermission('module_support', 2);
|
||||
|
||||
$network_id = intval($_GET['network_id'] ?? 0);
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT network, network_client_id, network_name FROM networks WHERE network_id = $network_id LIMIT 1");
|
||||
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
$network_name = escapeHtml($row['network_name']);
|
||||
$network = escapeHtml($row['network']);
|
||||
$client_id = intval($row['network_client_id']);
|
||||
|
||||
enforceClientAccess();
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title"><i class="fa fa-fw fa-map-pin me-2"></i>New IP Address</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form action="post.php" method="post" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
<input type="hidden" name="network_id" value="<?= $network_id ?>">
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<div class="mb-3">
|
||||
<label>IP Address <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-map-pin"></i></span>
|
||||
<input type="text" class="form-control font-monospace" name="ip_address" placeholder="Must be inside <?= $network ?>" maxlength="45" required autofocus>
|
||||
</div>
|
||||
<small class="text-secondary"><?= $network_name ?> — <span class="font-monospace"><?= $network ?></span></small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Hostname</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-server"></i></span>
|
||||
<input type="text" class="form-control" name="hostname" placeholder="dc01, printer-hr, ap-lobby" maxlength="200">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Description</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-fw fa-align-left"></i></span>
|
||||
<input type="text" class="form-control" name="description" placeholder="Domain controller, HR floor printer" maxlength="200">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="add_network_ip" class="btn btn-primary text-bold"><i class="fa fa-check me-2"></i>Create</button>
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fa fa-times me-2"></i>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
70
agent/modals/network/network_ip_edit.php
Normal file
70
agent/modals/network/network_ip_edit.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
require_once '../../../includes/modal_header.php';
|
||||
|
||||
enforceUserPermission('module_support', 2);
|
||||
|
||||
$ip_id = intval($_GET['id']);
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT ip_address, ip_description, ip_hostname, ip_network_id, network, network_client_id, network_name FROM network_ips
|
||||
LEFT JOIN networks ON network_id = ip_network_id
|
||||
WHERE ip_id = $ip_id LIMIT 1");
|
||||
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
$ip_address = escapeHtml($row['ip_address']);
|
||||
$ip_hostname = escapeHtml($row['ip_hostname']);
|
||||
$ip_description = escapeHtml($row['ip_description']);
|
||||
$network_name = escapeHtml($row['network_name']);
|
||||
$network = escapeHtml($row['network']);
|
||||
$client_id = intval($row['network_client_id']);
|
||||
|
||||
enforceClientAccess();
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title"><i class="fa fa-fw fa-map-pin me-2"></i>Editing <span class="font-monospace"><?= $ip_address ?></span></h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form action="post.php" method="post" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
<input type="hidden" name="ip_id" value="<?= $ip_id ?>">
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<div class="mb-3">
|
||||
<label>IP Address <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-map-pin"></i></span>
|
||||
<input type="text" class="form-control font-monospace" name="ip_address" value="<?= $ip_address ?>" maxlength="45" required>
|
||||
</div>
|
||||
<small class="text-secondary"><?= $network_name ?> — <span class="font-monospace"><?= $network ?></span></small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Hostname</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-server"></i></span>
|
||||
<input type="text" class="form-control" name="hostname" value="<?= $ip_hostname ?>" maxlength="200">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Description</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-fw fa-align-left"></i></span>
|
||||
<input type="text" class="form-control" name="description" value="<?= $ip_description ?>" maxlength="200">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="edit_network_ip" class="btn btn-primary text-bold"><i class="fa fa-check me-2"></i>Save</button>
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fa fa-times me-2"></i>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
38
agent/modals/network/network_ip_export.php
Normal file
38
agent/modals/network/network_ip_export.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
require_once '../../../includes/modal_header.php';
|
||||
|
||||
enforceUserPermission('module_support');
|
||||
|
||||
// Scoped to one network, so there are no page filters to inherit - column
|
||||
// selection only, same as the asset interfaces export
|
||||
$network_id = intval($_GET['network_id'] ?? 0);
|
||||
|
||||
$client_id = intval(getFieldById('networks', $network_id, 'network_client_id'));
|
||||
|
||||
enforceClientAccess();
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title"><i class="fas fa-fw fa-download me-2"></i>Export IP Addresses</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form action="post.php" method="post" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
<input type="hidden" name="network_id" value="<?= $network_id ?>">
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<?php renderExportColumnPicker('network_ips'); ?>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<?php renderExportButtons('export_network_ips'); ?>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
47
agent/modals/network/network_ip_import.php
Normal file
47
agent/modals/network/network_ip_import.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
require_once '../../../includes/modal_header.php';
|
||||
|
||||
enforceUserPermission('module_support', 2);
|
||||
|
||||
$network_id = intval($_GET['network_id'] ?? 0);
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT network, network_client_id, network_name FROM networks WHERE network_id = $network_id LIMIT 1");
|
||||
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
$network_name = escapeHtml($row['network_name']);
|
||||
$network = escapeHtml($row['network']);
|
||||
$client_id = intval($row['network_client_id']);
|
||||
|
||||
enforceClientAccess();
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title"><i class="fas fa-fw fa-upload me-2"></i>Import IP Addresses</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form action="post.php" method="post" enctype="multipart/form-data" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
<input type="hidden" name="network_id" value="<?= $network_id ?>">
|
||||
|
||||
<div class="modal-body">
|
||||
<p><strong>Format csv file with headings & data:</strong><br>IP Address, Hostname, Description</p>
|
||||
<p class="text-secondary mb-0">Importing into <strong><?= $network_name ?></strong> (<span class="font-monospace"><?= $network ?></span>). Rows that aren't a valid address, fall outside the subnet, or are already documented here are skipped.</p>
|
||||
<hr>
|
||||
<div class="mb-3 my-4">
|
||||
<input type="file" class="form-control" name="file" accept=".csv" required>
|
||||
</div>
|
||||
<hr>
|
||||
<div>Download: <a class="text-bold" href="post.php?download_network_ips_csv_template=<?= $network_id ?>">sample csv template</a></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="import_network_ips_csv" class="btn btn-primary text-bold"><i class="fa fa-upload me-2"></i>Import</button>
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fa fa-times me-2"></i>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
291
agent/network.php
Normal file
291
agent/network.php
Normal file
@@ -0,0 +1,291 @@
|
||||
<?php
|
||||
|
||||
// Default Column Sortby Filter
|
||||
$sort = "ip_address";
|
||||
$order = "ASC";
|
||||
|
||||
// If client_id is in URI then show client Side Bar and client header
|
||||
if (isset($_GET['client_id'])) {
|
||||
require_once "includes/inc_all_client.php";
|
||||
$client_query = "AND network_client_id = $client_id";
|
||||
$client_url = "client_id=$client_id&";
|
||||
} else {
|
||||
require_once "includes/inc_client_overview_all.php";
|
||||
$client_query = '';
|
||||
$client_url = '';
|
||||
}
|
||||
|
||||
// Perms
|
||||
enforceUserPermission('module_support');
|
||||
|
||||
$network_id = intval($_GET['network_id'] ?? 0);
|
||||
|
||||
$sql = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT client_id, client_name, location_name, network, network_client_id, network_description,
|
||||
network_dhcp_range, network_gateway, network_id, network_name, network_notes,
|
||||
network_primary_dns, network_secondary_dns, network_vlan FROM networks
|
||||
LEFT JOIN clients ON client_id = network_client_id
|
||||
LEFT JOIN locations ON location_id = network_location_id
|
||||
WHERE network_id = $network_id
|
||||
" . clientScopeSql('network_client_id') . "
|
||||
$client_query
|
||||
LIMIT 1"
|
||||
);
|
||||
|
||||
if (mysqli_num_rows($sql) == 0) {
|
||||
|
||||
echo "<center><h1 class='text-secondary mt-5'>Nothing to see here</h1><a class='btn btn-lg btn-secondary mt-3' href='javascript:history.back()'><i class='fa fa-fw fa-arrow-left'></i> Go Back</a></center>";
|
||||
|
||||
} else {
|
||||
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
$client_id = intval($row['network_client_id']);
|
||||
$client_name = escapeHtml($row['client_name']);
|
||||
$network_name = escapeHtml($row['network_name']);
|
||||
$network_description = escapeHtml($row['network_description']);
|
||||
$network_vlan = intval($row['network_vlan']);
|
||||
$network = escapeHtml($row['network']);
|
||||
$network_gateway = escapeHtml($row['network_gateway']);
|
||||
$network_primary_dns = escapeHtml($row['network_primary_dns']);
|
||||
$network_secondary_dns = escapeHtml($row['network_secondary_dns']);
|
||||
$network_dhcp_range = escapeHtml($row['network_dhcp_range']);
|
||||
$network_notes = escapeHtml($row['network_notes']);
|
||||
$location_name = escapeHtml($row['location_name']);
|
||||
|
||||
// Belt and braces - the query above is already scoped, but the no-client_id
|
||||
// entry point relies entirely on clientScopeSql()
|
||||
enforceClientAccess();
|
||||
|
||||
// Sorting - the search box posts back through filter_header.php, which will
|
||||
// accept any lowercase column name, so keep this to columns that exist
|
||||
if (!in_array($sort, ['ip_address', 'ip_hostname', 'ip_description'], true)) {
|
||||
$sort = 'ip_address';
|
||||
}
|
||||
|
||||
// Addresses sort by value, not as text - .9 belongs before .10, and this is
|
||||
// the one ordering that works for IPv4 and IPv6 in the same column
|
||||
$order_by = ($sort === 'ip_address') ? "INET6_ATON(ip_address)" : $sort;
|
||||
|
||||
$sql_ips = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT SQL_CALC_FOUND_ROWS ip_address, ip_description, ip_hostname, ip_id FROM network_ips
|
||||
WHERE ip_network_id = $network_id
|
||||
AND (ip_address LIKE '%$q%' OR ip_hostname LIKE '%$q%' OR ip_description LIKE '%$q%')
|
||||
ORDER BY $order_by $order LIMIT $record_from, $record_to"
|
||||
);
|
||||
|
||||
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
|
||||
?>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-3">
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<button type="button" class="btn btn-light float-end ajax-modal"
|
||||
data-modal-url="modals/network/network_edit.php?id=<?= $network_id ?>">
|
||||
<i class="fas fa-fw fa-edit"></i>
|
||||
</button>
|
||||
<h4 class="text-bold"><i class="fa fa-fw text-secondary fa-network-wired me-2"></i><?= $network_name ?></h4>
|
||||
<?php if ($network_description) { ?>
|
||||
<div class="text-secondary"><?= $network_description ?></div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div><i class="fa fa-fw fa-network-wired text-secondary me-2"></i><span class="font-monospace"><?= $network ?></span></div>
|
||||
<?php if ($network_vlan) { ?>
|
||||
<div class="mt-2"><i class="fa fa-fw fa-layer-group text-secondary me-2"></i>VLAN <span class="font-monospace"><?= $network_vlan ?></span></div>
|
||||
<?php }
|
||||
if ($network_gateway) { ?>
|
||||
<div class="mt-2"><i class="fa fa-fw fa-route text-secondary me-2"></i><span class="font-monospace"><?= $network_gateway ?></span></div>
|
||||
<?php }
|
||||
if ($network_dhcp_range) { ?>
|
||||
<div class="mt-2"><i class="fa fa-fw fa-arrows-alt-h text-secondary me-2"></i><span class="font-monospace"><?= $network_dhcp_range ?></span></div>
|
||||
<?php }
|
||||
if ($network_primary_dns) { ?>
|
||||
<div class="mt-2"><i class="fa fa-fw fa-globe text-secondary me-2"></i><span class="font-monospace"><?= $network_primary_dns ?></span></div>
|
||||
<?php }
|
||||
if ($network_secondary_dns) { ?>
|
||||
<div class="mt-2"><i class="fa fa-fw fa-globe text-secondary me-2"></i><span class="font-monospace"><?= $network_secondary_dns ?></span></div>
|
||||
<?php }
|
||||
if ($location_name) { ?>
|
||||
<div class="mt-2"><i class="fa fa-fw fa-map-marker-alt text-secondary me-2"></i><?= $location_name ?></div>
|
||||
<?php } ?>
|
||||
<div class="mt-3">
|
||||
<a class="btn btn-light btn-sm" href="networks.php?<?= $client_url ?>">
|
||||
<i class="fa fa-fw fa-arrow-left me-2"></i>All Networks
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($network_notes) { ?>
|
||||
<div class="card card-dark mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title">Notes</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?= nl2br($network_notes) ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-md-9">
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header bg-dark py-2">
|
||||
<h3 class="card-title mt-2"><i class="fas fa-fw fa-map-pin me-2"></i>IP Addresses</h3>
|
||||
<div class="card-tools">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/network/network_ip_add.php?network_id=<?= $network_id ?>">
|
||||
<i class="fas fa-plus me-2"></i>New IP
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown"></button>
|
||||
<div class="dropdown-menu">
|
||||
<?php if ($num_rows[0] > 0) { ?>
|
||||
<a class="dropdown-item text-dark ajax-modal" href="#"
|
||||
data-modal-url="modals/network/network_ip_export.php?network_id=<?= $network_id ?>">
|
||||
<i class="fa fa-fw fa-download me-2"></i>Export
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<?php } ?>
|
||||
<a class="dropdown-item text-dark ajax-modal" href="#"
|
||||
data-modal-url="modals/network/network_ip_import.php?network_id=<?= $network_id ?>">
|
||||
<i class="fa fa-fw fa-upload me-2"></i>Import
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-header py-3">
|
||||
<form autocomplete="off">
|
||||
<?php if ($client_url) { ?>
|
||||
<input type="hidden" name="client_id" value="<?= $client_id ?>">
|
||||
<?php } ?>
|
||||
<input type="hidden" name="network_id" value="<?= $network_id ?>">
|
||||
<div class="row g-2 align-items-center">
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="input-group">
|
||||
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(escapeHtml($q)); } ?>" placeholder="Search IP, hostname or description">
|
||||
<button class="btn btn-dark"><i class="fa fa-search"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="btn-group float-end">
|
||||
<div class="dropdown" id="bulkActionButton" hidden>
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-fw fa-layer-group me-2"></i>Bulk Action (<span id="selectedCount">0</span>)
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<button class="dropdown-item text-danger text-bold confirm-link"
|
||||
type="submit" form="bulkActions" name="bulk_delete_network_ips">
|
||||
<i class="fas fa-fw fa-trash me-2"></i>Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
|
||||
<form id="bulkActions" action="post.php" method="post">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
|
||||
<table class="table table-striped table-borderless table-hover mb-0">
|
||||
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
|
||||
<tr>
|
||||
<td class="checkbox-column">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" id="selectAllCheckbox" type="checkbox" onclick="checkAll(this)">
|
||||
</div>
|
||||
</td>
|
||||
<th>
|
||||
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=ip_address&order=<?= $disp ?>">
|
||||
IP Address <?php if ($sort == 'ip_address') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=ip_hostname&order=<?= $disp ?>">
|
||||
Hostname <?php if ($sort == 'ip_hostname') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=ip_description&order=<?= $disp ?>">
|
||||
Description <?php if ($sort == 'ip_description') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th class="text-center">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
|
||||
while ($row = mysqli_fetch_assoc($sql_ips)) {
|
||||
$ip_id = intval($row['ip_id']);
|
||||
$ip_address = escapeHtml($row['ip_address']);
|
||||
$ip_hostname = escapeHtml($row['ip_hostname']);
|
||||
$ip_description = escapeHtml($row['ip_description']);
|
||||
|
||||
?>
|
||||
<tr>
|
||||
<td class="checkbox-column">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input bulk-select" type="checkbox" name="network_ip_ids[]" value="<?= $ip_id ?>">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<a class="text-dark font-monospace ajax-modal" href="#" data-modal-url="modals/network/network_ip_edit.php?id=<?= $ip_id ?>">
|
||||
<?= $ip_address ?>
|
||||
</a>
|
||||
</td>
|
||||
<td><?= $ip_hostname ?></td>
|
||||
<td><?= $ip_description ?></td>
|
||||
<td>
|
||||
<div class="dropdown dropstart text-center">
|
||||
<button class="btn btn-secondary btn-sm" type="button" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-ellipsis-h"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/network/network_ip_edit.php?id=<?= $ip_id ?>">
|
||||
<i class="fas fa-fw fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_network_ip=<?= $ip_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-trash me-2"></i>Delete
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<?php } ?>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
<?php require_once "../includes/filter_footer.php"; ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="../js/bulk_actions.js"></script>
|
||||
|
||||
<?php
|
||||
|
||||
}
|
||||
|
||||
require_once "../includes/footer.php";
|
||||
@@ -60,7 +60,8 @@ $sql = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT SQL_CALC_FOUND_ROWS client_id, client_name, location_name, network, network_archived_at, network_description,
|
||||
network_dhcp_range, network_gateway, network_id, network_location_id, network_name,
|
||||
network_primary_dns, network_secondary_dns, network_vlan FROM networks
|
||||
network_primary_dns, network_secondary_dns, network_vlan,
|
||||
(SELECT COUNT(ip_id) FROM network_ips WHERE ip_network_id = network_id) AS ip_count FROM networks
|
||||
LEFT JOIN clients ON client_id = network_client_id
|
||||
LEFT JOIN locations ON location_id = network_location_id
|
||||
WHERE $archive_query
|
||||
@@ -289,6 +290,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
$location_name_display = $location_name;
|
||||
}
|
||||
$network_archived_at = escapeHtml($row['network_archived_at']);
|
||||
$ip_count = intval($row['ip_count']);
|
||||
|
||||
?>
|
||||
<tr>
|
||||
@@ -298,11 +300,16 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<a class="text-dark ajax-modal" href="#" data-modal-url="modals/network/network_edit.php?id=<?= $network_id ?>">
|
||||
<a class="text-dark" href="network.php?client_id=<?= $client_id ?>&network_id=<?= $network_id ?>">
|
||||
<div class="d-flex">
|
||||
<i class="fa fa-fw fa-2x fa-network-wired me-2"></i>
|
||||
<div class="flex-grow-1">
|
||||
<div><?= $network_name ?></div>
|
||||
<div>
|
||||
<?= $network_name ?>
|
||||
<?php if ($ip_count) { ?>
|
||||
<span class="badge bg-secondary ms-1" title="Documented IP addresses"><?= $ip_count ?></span>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<div><small class="text-secondary"><?= $network_description ?></small></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -323,6 +330,10 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
<i class="fas fa-ellipsis-h"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="network.php?client_id=<?= $client_id ?>&network_id=<?= $network_id ?>">
|
||||
<i class="fas fa-fw fa-map-pin me-2"></i>IP Addresses
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/network/network_edit.php?id=<?= $network_id ?>">
|
||||
<i class="fas fa-fw fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
@@ -338,7 +349,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
</a>
|
||||
<?php } else { ?>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-danger confirm-link" href="post.php?archive_network=<?= $network_id ?>">
|
||||
<a class="dropdown-item text-danger confirm-link" href="post.php?archive_network=<?= $network_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-archive me-2"></i>Archive
|
||||
</a>
|
||||
<?php } ?>
|
||||
|
||||
341
agent/post/network_ip.php
Normal file
341
agent/post/network_ip.php
Normal file
@@ -0,0 +1,341 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - GET/POST request handler for documented IP addresses
|
||||
*
|
||||
* IPs hang off a network (subnet). The two safeguards - valid + inside the
|
||||
* subnet, and not already documented on that network - live in
|
||||
* checkIpForNetwork() in functions/network.php so add, edit and CSV import all
|
||||
* apply exactly the same rules. There is a UNIQUE(ip_network_id, ip_address)
|
||||
* index behind them as a backstop.
|
||||
*/
|
||||
|
||||
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
|
||||
|
||||
if (isset($_POST['add_network_ip'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceUserPermission('module_support', 2);
|
||||
|
||||
require_once 'network_ip_model.php';
|
||||
|
||||
$network_id = intval($_POST['network_id']);
|
||||
|
||||
$client_id = intval(getFieldById('networks', $network_id, 'network_client_id'));
|
||||
|
||||
enforceClientAccess();
|
||||
|
||||
$ip_error = checkIpForNetwork($ip_address, $network_id);
|
||||
|
||||
if ($ip_error) {
|
||||
flashAlert($ip_error, 'error');
|
||||
redirect();
|
||||
}
|
||||
|
||||
$ip_address_escaped = escapeSql($ip_address);
|
||||
|
||||
mysqli_query($mysqli, "INSERT INTO network_ips SET ip_address = '$ip_address_escaped', ip_hostname = '$hostname', ip_description = '$description', ip_network_id = $network_id");
|
||||
|
||||
$ip_id = mysqli_insert_id($mysqli);
|
||||
|
||||
$network_name = escapeSql(getFieldById('networks', $network_id, 'network_name'));
|
||||
|
||||
logAudit("IP Address", "Create", "$session_name documented IP $ip_address_escaped on network $network_name", $client_id, $ip_id);
|
||||
|
||||
flashAlert("IP <strong>$ip_address</strong> documented");
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['edit_network_ip'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceUserPermission('module_support', 2);
|
||||
|
||||
require_once 'network_ip_model.php';
|
||||
|
||||
$ip_id = intval($_POST['ip_id']);
|
||||
|
||||
$network_id = intval(getFieldById('network_ips', $ip_id, 'ip_network_id'));
|
||||
|
||||
$client_id = intval(getFieldById('networks', $network_id, 'network_client_id'));
|
||||
|
||||
enforceClientAccess();
|
||||
|
||||
// The row's own id is excluded so saving without changing the address
|
||||
// doesn't trip the duplicate check against itself
|
||||
$ip_error = checkIpForNetwork($ip_address, $network_id, $ip_id);
|
||||
|
||||
if ($ip_error) {
|
||||
flashAlert($ip_error, 'error');
|
||||
redirect();
|
||||
}
|
||||
|
||||
$ip_address_escaped = escapeSql($ip_address);
|
||||
|
||||
mysqli_query($mysqli, "UPDATE network_ips SET ip_address = '$ip_address_escaped', ip_hostname = '$hostname', ip_description = '$description' WHERE ip_id = $ip_id");
|
||||
|
||||
$network_name = escapeSql(getFieldById('networks', $network_id, 'network_name'));
|
||||
|
||||
logAudit("IP Address", "Edit", "$session_name edited IP $ip_address_escaped on network $network_name", $client_id, $ip_id);
|
||||
|
||||
flashAlert("IP <strong>$ip_address</strong> updated");
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_GET['delete_network_ip'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceUserPermission('module_support', 2);
|
||||
|
||||
$ip_id = intval($_GET['delete_network_ip']);
|
||||
|
||||
// Get IP and Client ID for logging and alert message
|
||||
$sql = mysqli_query($mysqli, "SELECT ip_address, network_client_id FROM network_ips LEFT JOIN networks ON network_id = ip_network_id WHERE ip_id = $ip_id");
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
$ip_address = escapeSql($row['ip_address']);
|
||||
$client_id = intval($row['network_client_id']);
|
||||
|
||||
enforceClientAccess();
|
||||
|
||||
mysqli_query($mysqli, "DELETE FROM network_ips WHERE ip_id = $ip_id");
|
||||
|
||||
logAudit("IP Address", "Delete", "$session_name deleted IP $ip_address", $client_id);
|
||||
|
||||
flashAlert("IP <strong>$ip_address</strong> deleted", 'error');
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['bulk_delete_network_ips'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceUserPermission('module_support', 2);
|
||||
|
||||
if (isset($_POST['network_ip_ids'])) {
|
||||
|
||||
// Get Selected Count
|
||||
$count = count($_POST['network_ip_ids']);
|
||||
|
||||
$client_id = 0;
|
||||
|
||||
// Cycle through array and delete each IP
|
||||
foreach ($_POST['network_ip_ids'] as $ip_id) {
|
||||
|
||||
$ip_id = intval($ip_id);
|
||||
|
||||
// Get IP and Client ID for logging and the access check
|
||||
$sql = mysqli_query($mysqli, "SELECT ip_address, network_client_id FROM network_ips LEFT JOIN networks ON network_id = ip_network_id WHERE ip_id = $ip_id");
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
$ip_address = escapeSql($row['ip_address']);
|
||||
$client_id = intval($row['network_client_id']);
|
||||
|
||||
enforceClientAccess();
|
||||
|
||||
mysqli_query($mysqli, "DELETE FROM network_ips WHERE ip_id = $ip_id");
|
||||
|
||||
logAudit("IP Address", "Delete", "$session_name deleted IP $ip_address", $client_id);
|
||||
|
||||
}
|
||||
|
||||
logAudit("IP Address", "Bulk Delete", "$session_name deleted $count IP(s)", $client_id);
|
||||
|
||||
flashAlert("Deleted <strong>$count</strong> IP(s)", 'error');
|
||||
|
||||
}
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_GET['download_network_ips_csv_template'])) {
|
||||
|
||||
enforceUserPermission('module_support');
|
||||
|
||||
$network_id = intval($_GET['download_network_ips_csv_template']);
|
||||
|
||||
$client_id = intval(getFieldById('networks', $network_id, 'network_client_id'));
|
||||
|
||||
enforceClientAccess();
|
||||
|
||||
$network_name = getFieldById('networks', $network_id, 'network_name');
|
||||
|
||||
$delimiter = ",";
|
||||
$enclosure = '"';
|
||||
$escape = '\\';
|
||||
$filename = toAlphanumeric($network_name) . "-IPs-Template.csv";
|
||||
|
||||
// Create a file pointer
|
||||
$f = fopen('php://memory', 'w');
|
||||
|
||||
// Set column headers
|
||||
$fields = array('IP Address', 'Hostname', 'Description');
|
||||
fputcsv($f, $fields, $delimiter, $enclosure, $escape);
|
||||
|
||||
// Move back to beginning of file
|
||||
fseek($f, 0);
|
||||
|
||||
// Set headers to download file rather than displayed
|
||||
header('Content-Type: text/csv');
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '";');
|
||||
|
||||
// Output all remaining data on a file pointer
|
||||
fpassthru($f);
|
||||
exit;
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['import_network_ips_csv'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceUserPermission('module_support', 2);
|
||||
|
||||
$network_id = intval($_POST['network_id']);
|
||||
|
||||
$client_id = intval(getFieldById('networks', $network_id, 'network_client_id'));
|
||||
|
||||
enforceClientAccess();
|
||||
|
||||
$network_name = escapeSql(getFieldById('networks', $network_id, 'network_name'));
|
||||
|
||||
$error = false;
|
||||
|
||||
// File provided?
|
||||
if (!empty($_FILES['file']['tmp_name'])) {
|
||||
$file_name = $_FILES['file']['tmp_name'];
|
||||
} else {
|
||||
flashAlert("Please select a file to upload.", 'error');
|
||||
redirect();
|
||||
}
|
||||
|
||||
// Check file is CSV
|
||||
$file_parts = explode('.', $_FILES['file']['name']);
|
||||
$file_extension = strtolower(end($file_parts));
|
||||
if ($file_extension !== 'csv') {
|
||||
$error = true;
|
||||
flashAlert("Bad file extension - only .csv files are accepted.", 'error');
|
||||
}
|
||||
|
||||
// Check file isn't empty
|
||||
elseif ($_FILES['file']['size'] < 1) {
|
||||
$error = true;
|
||||
flashAlert("Bad file size (empty file?).", 'error');
|
||||
}
|
||||
|
||||
// Check column count (IP Address, Hostname, Description)
|
||||
else {
|
||||
$f = fopen($file_name, 'r');
|
||||
$f_columns = fgetcsv($f, 1000, ',');
|
||||
fclose($f);
|
||||
|
||||
if (count($f_columns) !== 3) {
|
||||
$error = true;
|
||||
flashAlert("Bad column count - expected 3 columns: IP Address, Hostname, Description.", 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Parse and insert
|
||||
if (!$error) {
|
||||
|
||||
$file = fopen($file_name, 'r');
|
||||
fgetcsv($file, 1000, ','); // Skip header row
|
||||
|
||||
$row_count = 0;
|
||||
$skipped_count = 0;
|
||||
|
||||
while (($column = fgetcsv($file, 1000, ',')) !== false) {
|
||||
|
||||
$ip_address = trim($column[0] ?? '');
|
||||
$hostname = escapeSql($column[1] ?? '');
|
||||
$description = escapeSql($column[2] ?? '');
|
||||
|
||||
// Skip blank lines
|
||||
if ($ip_address === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Same safeguards as the add form - invalid, out of subnet or
|
||||
// already documented all mean skip. Rows inserted earlier in this
|
||||
// same run are in the table already, so a file that repeats an
|
||||
// address imports it once
|
||||
if (checkIpForNetwork($ip_address, $network_id) !== '') {
|
||||
$skipped_count++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$ip_address_escaped = escapeSql($ip_address);
|
||||
|
||||
mysqli_query($mysqli, "INSERT INTO network_ips SET ip_address = '$ip_address_escaped', ip_hostname = '$hostname', ip_description = '$description', ip_network_id = $network_id");
|
||||
|
||||
$row_count++;
|
||||
|
||||
}
|
||||
|
||||
fclose($file);
|
||||
|
||||
logAudit("IP Address", "Import", "$session_name imported $row_count IP(s) to network $network_name. $skipped_count row(s) skipped", $client_id);
|
||||
|
||||
flashAlert("<strong>$row_count</strong> IP(s) imported, <strong>$skipped_count</strong> skipped (invalid, outside the subnet or already documented)");
|
||||
|
||||
}
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isExportRequest('export_network_ips')) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
// Exports are reads - see CONTRIBUTING.md
|
||||
enforceUserPermission('module_support');
|
||||
|
||||
$format = resolveExportFormat($_POST['export_network_ips']);
|
||||
|
||||
$network_id = intval($_POST['network_id']);
|
||||
|
||||
$client_id = intval(getFieldById('networks', $network_id, 'network_client_id'));
|
||||
|
||||
enforceClientAccess();
|
||||
|
||||
$network_name = getFieldById('networks', $network_id, 'network_name');
|
||||
|
||||
// Scoped to the one network, so no page filters apply
|
||||
$sql = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT ip_address, ip_hostname, ip_description, network_name, client_name FROM network_ips
|
||||
LEFT JOIN networks ON network_id = ip_network_id
|
||||
LEFT JOIN clients ON client_id = network_client_id
|
||||
WHERE ip_network_id = $network_id
|
||||
ORDER BY INET6_ATON(ip_address) ASC"
|
||||
);
|
||||
|
||||
$num_rows = mysqli_num_rows($sql);
|
||||
|
||||
if ($num_rows > 0) {
|
||||
|
||||
guardExportPdfRowCount($format, $num_rows);
|
||||
|
||||
$export = beginExport('network_ips', $format, toAlphanumeric($network_name) . '-IPs', "$network_name - IP Addresses", '');
|
||||
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
addExportRow($export, $row);
|
||||
}
|
||||
|
||||
finishExport($export);
|
||||
}
|
||||
|
||||
logAudit("IP Address", "Export", "$session_name exported $num_rows IP(s) to a " . strtoupper($format) . " file", $client_id);
|
||||
|
||||
exit;
|
||||
|
||||
}
|
||||
9
agent/post/network_ip_model.php
Normal file
9
agent/post/network_ip_model.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
|
||||
|
||||
// $ip_address is deliberately NOT escaped here - checkIpForNetwork() in
|
||||
// functions/network.php normalises it first and the handler escapes the
|
||||
// canonical form it hands back.
|
||||
$ip_address = trim($_POST['ip_address'] ?? '');
|
||||
$hostname = escapeSql($_POST['hostname'] ?? '');
|
||||
$description = escapeSql($_POST['description'] ?? '');
|
||||
23
db.sql
23
db.sql
@@ -1501,6 +1501,27 @@ CREATE TABLE `modules` (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `network_ips`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `network_ips`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `network_ips` (
|
||||
`ip_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`ip_address` varchar(45) NOT NULL,
|
||||
`ip_hostname` varchar(200) DEFAULT NULL,
|
||||
`ip_description` varchar(200) DEFAULT NULL,
|
||||
`ip_created_at` datetime NOT NULL DEFAULT current_timestamp(),
|
||||
`ip_updated_at` datetime DEFAULT NULL ON UPDATE current_timestamp(),
|
||||
`ip_network_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`ip_id`),
|
||||
UNIQUE KEY `ip_network_address` (`ip_network_id`,`ip_address`),
|
||||
CONSTRAINT `network_ips_ibfk_1` FOREIGN KEY (`ip_network_id`) REFERENCES `networks` (`network_id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `networks`
|
||||
--
|
||||
@@ -3172,4 +3193,4 @@ CREATE TABLE `vendors` (
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||
|
||||
-- Dump completed on 2026-08-15 20:35:12
|
||||
-- Dump completed on 2026-08-25 12:18:16
|
||||
|
||||
@@ -17,6 +17,7 @@ require_once __DIR__ . '/functions/format.php';
|
||||
require_once __DIR__ . '/functions/request.php';
|
||||
require_once __DIR__ . '/functions/files.php';
|
||||
require_once __DIR__ . '/functions/domain.php';
|
||||
require_once __DIR__ . '/functions/network.php';
|
||||
require_once __DIR__ . '/functions/auth.php';
|
||||
require_once __DIR__ . '/functions/logging.php';
|
||||
require_once __DIR__ . '/functions/app.php';
|
||||
|
||||
@@ -144,6 +144,16 @@ function getExportColumns($export_type) {
|
||||
'network_secondary_dns' => ['label' => 'Secondary DNS'],
|
||||
],
|
||||
|
||||
'network_ips' => [
|
||||
'ip_address' => ['label' => 'IP Address'],
|
||||
'ip_hostname' => ['label' => 'Hostname', 'weight' => 2],
|
||||
'ip_description' => ['label' => 'Description', 'weight' => 3],
|
||||
// Available from the handler's join but not exported by default -
|
||||
// the export is always scoped to one network of one client
|
||||
'network_name' => ['label' => 'Network', 'default' => false],
|
||||
'client_name' => ['label' => 'Client', 'default' => false],
|
||||
],
|
||||
|
||||
'certificates' => [
|
||||
'certificate_name' => ['label' => 'Name', 'weight' => 2],
|
||||
'certificate_description' => ['label' => 'Description', 'weight' => 3],
|
||||
|
||||
227
functions/network.php
Normal file
227
functions/network.php
Normal file
@@ -0,0 +1,227 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - IP address and subnet helpers
|
||||
*
|
||||
* Backs the per-network IP address list (agent/network.php). Everything here
|
||||
* works on both IPv4 and IPv6 by comparing the packed binary form, so there is
|
||||
* no ip2long/32-bit path to go wrong.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Canonicalises an address so two spellings of the same thing compare equal -
|
||||
* 2001:DB8:0000::0001 and 2001:db8::1 both store as 2001:db8::1. This is what
|
||||
* makes the duplicate check (and the UNIQUE index behind it) reliable.
|
||||
*
|
||||
* Returns false when the value is not an IP address at all. Note that
|
||||
* inet_pton() rejects leading-zero IPv4 octets (192.168.001.005), which is
|
||||
* deliberate - that notation is read as octal by some resolvers.
|
||||
*/
|
||||
function normalizeIpAddress($ip) {
|
||||
|
||||
$ip = trim($ip);
|
||||
|
||||
if ($ip === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* inet_pton() rejects leading-zero IPv4 octets outright (192.168.010.05),
|
||||
* because that notation reads as octal to some resolvers. Nothing here
|
||||
* resolves anything - these are documentation records, and a switch or
|
||||
* router config screen is where people copy them from, so the zeros get
|
||||
* stripped and the row is kept rather than failing the import.
|
||||
*/
|
||||
if (preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/', $ip)) {
|
||||
$octets = explode('.', $ip);
|
||||
foreach ($octets as $index => $octet) {
|
||||
$octets[$index] = ltrim($octet, '0');
|
||||
if ($octets[$index] === '') {
|
||||
$octets[$index] = '0';
|
||||
}
|
||||
}
|
||||
$ip = implode('.', $octets);
|
||||
}
|
||||
|
||||
$bin = @inet_pton($ip);
|
||||
|
||||
if ($bin === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return inet_ntop($bin);
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Parses a subnet into its binary network address and prefix length.
|
||||
*
|
||||
* Accepts CIDR (192.168.1.0/24, 2001:db8::/64) and the dotted-mask form
|
||||
* (192.168.1.0/255.255.255.0) that hand-entered records sometimes carry.
|
||||
*
|
||||
* Returns ['network' => <masked binary>, 'prefix' => int, 'bytes' => 4|16],
|
||||
* or false when the value isn't something we can reason about.
|
||||
*/
|
||||
function parseSubnet($subnet) {
|
||||
|
||||
$subnet = trim($subnet);
|
||||
|
||||
if (strpos($subnet, '/') === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
list($address, $prefix) = explode('/', $subnet, 2);
|
||||
|
||||
$bin = @inet_pton(trim($address));
|
||||
|
||||
if ($bin === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$bytes = strlen($bin);
|
||||
$prefix = trim($prefix);
|
||||
|
||||
// Dotted subnet mask (IPv4 only) - count the leading 1 bits
|
||||
if (strpos($prefix, '.') !== false) {
|
||||
|
||||
$mask_bin = @inet_pton($prefix);
|
||||
|
||||
if ($mask_bin === false || strlen($mask_bin) !== 4 || $bytes !== 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$mask_bits = '';
|
||||
for ($i = 0; $i < 4; $i++) {
|
||||
$mask_bits .= str_pad(decbin(ord($mask_bin[$i])), 8, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
// Must be contiguous - 255.255.254.0 is a mask, 255.0.255.0 is a typo
|
||||
if (!preg_match('/^(1*)0*$/', $mask_bits, $match)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$prefix = strlen($match[1]);
|
||||
|
||||
} elseif (!ctype_digit($prefix)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$prefix = intval($prefix);
|
||||
|
||||
if ($prefix < 0 || $prefix > $bytes * 8) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return [
|
||||
'network' => applyIpMask($bin, $prefix),
|
||||
'prefix' => $prefix,
|
||||
'bytes' => $bytes,
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Zeroes every bit past the prefix length, giving the network address.
|
||||
* Operates on the packed binary form from inet_pton().
|
||||
*/
|
||||
function applyIpMask($bin, $prefix) {
|
||||
|
||||
$bytes = strlen($bin);
|
||||
|
||||
for ($i = 0; $i < $bytes; $i++) {
|
||||
|
||||
$bits_left = $prefix - ($i * 8);
|
||||
|
||||
if ($bits_left >= 8) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($bits_left <= 0) {
|
||||
$bin[$i] = chr(0);
|
||||
} else {
|
||||
$bin[$i] = chr(ord($bin[$i]) & ((0xFF << (8 - $bits_left)) & 0xFF));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $bin;
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* True when the address falls inside the subnet. Families must match - an IPv4
|
||||
* address is never inside an IPv6 subnet and vice versa.
|
||||
*
|
||||
* Returns TRUE when the subnet itself can't be parsed. Networks created before
|
||||
* the CIDR field was there can hold anything, and refusing to let someone
|
||||
* document addresses on those is worse than not checking them.
|
||||
*/
|
||||
function isIpInSubnet($ip, $subnet) {
|
||||
|
||||
$parsed = parseSubnet($subnet);
|
||||
|
||||
if ($parsed === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$bin = @inet_pton(trim($ip));
|
||||
|
||||
if ($bin === false || strlen($bin) !== $parsed['bytes']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return applyIpMask($bin, $parsed['prefix']) === $parsed['network'];
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* The two safeguards on a documented IP, in one place so add, edit and CSV
|
||||
* import can't drift apart: the address must be valid and inside its network's
|
||||
* subnet, and the network must not already have it.
|
||||
*
|
||||
* $ip is rewritten by reference to its canonical form, so callers store what
|
||||
* was checked rather than what was typed.
|
||||
*
|
||||
* Returns an error message for the user, or an empty string when it's good.
|
||||
* Pass the row's own id as $ignore_ip_id when editing so a row doesn't collide
|
||||
* with itself.
|
||||
*/
|
||||
function checkIpForNetwork(&$ip, $network_id, $ignore_ip_id = 0) {
|
||||
|
||||
global $mysqli;
|
||||
|
||||
$original = trim($ip);
|
||||
$normalized = normalizeIpAddress($original);
|
||||
|
||||
if ($normalized === false) {
|
||||
return "<strong>$original</strong> is not a valid IP address";
|
||||
}
|
||||
|
||||
$ip = $normalized;
|
||||
|
||||
$network_id = intval($network_id);
|
||||
$subnet = getFieldById('networks', $network_id, 'network');
|
||||
|
||||
if (!isIpInSubnet($ip, $subnet)) {
|
||||
return "<strong>$ip</strong> is outside <strong>$subnet</strong>";
|
||||
}
|
||||
|
||||
$ip_escaped = escapeSql($ip);
|
||||
$ignore_ip_id = intval($ignore_ip_id);
|
||||
|
||||
$sql = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT ip_id FROM network_ips
|
||||
WHERE ip_network_id = $network_id
|
||||
AND ip_address = '$ip_escaped'
|
||||
AND ip_id != $ignore_ip_id
|
||||
LIMIT 1"
|
||||
);
|
||||
|
||||
if (mysqli_num_rows($sql) > 0) {
|
||||
return "<strong>$ip</strong> is already documented on this network";
|
||||
}
|
||||
|
||||
return '';
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user