mirror of
https://github.com/itflow-org/itflow
synced 2026-11-11 07:01:16 +00:00
Audit logs stage 3: failed sign-ins say why and are logged against the account they were for - wrong password, no account with that email, disabled, archived or invited account, client archived, portal turned off, a wrong MFA or two-step code after the right password - with failed Microsoft 365 portal sign-ins logged too and marked Their account in the audit log; the IP lockout counts portal failures as well; the first sign-in from an IP address is badged New IP in the audit lists and quick look; new Admin > Sign-ins under Access with People and IP addresses views, search, date filter, sortable columns and paging; admins get a notification and an email on a run of failed sign-ins or turned-away API calls from one address or against one account, set in Settings > Security (10 to start, 5, 20, 50 or never), each in the audit log Security view and in Mail > Previews; API calls counted per key, day and endpoint, with a 30-day calls column and last IP on API Keys and a usage window, expired-key calls logged against the key, and usage deleted with the key and after the log retention; database 3.3.2 drops the unused auth_logs table and adds settings.config_login_alert_failures and api_key_usage; new functions/sign_in.php, admin/sign_ins.php and admin/modals/api/api_key_usage.php; the license template page no longer warns when view is missing
This commit is contained in:
@@ -155,3 +155,138 @@ function apiKeyAgentFieldHtml($selected = 0) {
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------- What each key is used for */
|
||||
|
||||
/** The endpoint a call went to, from its address - "/api/v1/clients/read.php" is "clients/read". */
|
||||
function apiKeyUsageEndpoint($uri) {
|
||||
$path = (string) parse_url((string) $uri, PHP_URL_PATH);
|
||||
$path = preg_replace('#^.*?/api/v1/#', '', $path);
|
||||
$path = preg_replace('/\.php$/', '', $path);
|
||||
return substr(preg_replace('#[^a-z0-9_/-]#', '', strtolower($path)), 0, 100);
|
||||
}
|
||||
|
||||
/*
|
||||
* Count a call with a key - to its day and endpoint, with when and from
|
||||
* where (api/v1/validate_api_key.php). Never in the way of the call: before
|
||||
* the update that adds the table, nothing is counted.
|
||||
*/
|
||||
function apiKeyUsageRecord($api_key_id, $endpoint, $ip) {
|
||||
global $mysqli;
|
||||
$api_key_id = intval($api_key_id);
|
||||
$endpoint = escapeSql(substr((string) $endpoint, 0, 100));
|
||||
$ip = escapeSql(substr((string) $ip, 0, 200));
|
||||
try {
|
||||
mysqli_query($mysqli, "INSERT INTO api_key_usage SET api_key_usage_key_id = $api_key_id, api_key_usage_date = CURDATE(), api_key_usage_endpoint = '$endpoint',
|
||||
api_key_usage_calls = 1, api_key_usage_last_at = NOW(), api_key_usage_last_ip = '$ip'
|
||||
ON DUPLICATE KEY UPDATE api_key_usage_calls = api_key_usage_calls + 1, api_key_usage_last_at = NOW(), api_key_usage_last_ip = '$ip'");
|
||||
} catch (Throwable $e) {
|
||||
error_log('ITFlow: API key use not counted - ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Each key's use over the last $days days - [api_key_id => ['calls',
|
||||
* 'endpoints', 'last_at', 'last_ip']], nothing for a key not used.
|
||||
*/
|
||||
function apiKeyUsageSummary(array $api_key_ids, $days = 30) {
|
||||
global $mysqli;
|
||||
$ids = array_values(array_filter(array_map('intval', $api_key_ids)));
|
||||
$usage = [];
|
||||
if (!$ids) {
|
||||
return $usage;
|
||||
}
|
||||
$days = max(1, intval($days));
|
||||
$sql = mysqli_query($mysqli, "SELECT api_key_usage_key_id, SUM(api_key_usage_calls) AS calls, COUNT(DISTINCT api_key_usage_endpoint) AS endpoints, MAX(api_key_usage_last_at) AS last_at
|
||||
FROM api_key_usage WHERE api_key_usage_key_id IN (" . implode(',', $ids) . ") AND api_key_usage_date > CURDATE() - INTERVAL $days DAY
|
||||
GROUP BY api_key_usage_key_id");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$usage[intval($row['api_key_usage_key_id'])] = ['calls' => intval($row['calls']), 'endpoints' => intval($row['endpoints']), 'last_at' => $row['last_at'], 'last_ip' => ''];
|
||||
}
|
||||
// Where the last call came from - the row it was counted on
|
||||
$sql = mysqli_query($mysqli, "SELECT u.api_key_usage_key_id, u.api_key_usage_last_ip FROM api_key_usage u
|
||||
JOIN (SELECT api_key_usage_key_id, MAX(api_key_usage_last_at) AS last_at FROM api_key_usage WHERE api_key_usage_key_id IN (" . implode(',', $ids) . ") GROUP BY api_key_usage_key_id) m
|
||||
ON m.api_key_usage_key_id = u.api_key_usage_key_id AND m.last_at = u.api_key_usage_last_at");
|
||||
while ($row = mysqli_fetch_row($sql)) {
|
||||
if (isset($usage[intval($row[0])])) {
|
||||
$usage[intval($row[0])]['last_ip'] = (string) $row[1];
|
||||
}
|
||||
}
|
||||
return $usage;
|
||||
}
|
||||
|
||||
/* A key's calls each day over the last $days days, oldest first - [Y-m-d => calls], every day there. */
|
||||
function apiKeyUsageDaily($api_key_id, $days = 30) {
|
||||
global $mysqli;
|
||||
$days = max(1, intval($days));
|
||||
$daily = [];
|
||||
for ($i = $days - 1; $i >= 0; $i--) {
|
||||
$daily[date('Y-m-d', strtotime("-$i days"))] = 0;
|
||||
}
|
||||
$sql = mysqli_query($mysqli, "SELECT api_key_usage_date, SUM(api_key_usage_calls) FROM api_key_usage
|
||||
WHERE api_key_usage_key_id = " . intval($api_key_id) . " AND api_key_usage_date > CURDATE() - INTERVAL $days DAY GROUP BY api_key_usage_date");
|
||||
while ($row = mysqli_fetch_row($sql)) {
|
||||
if (isset($daily[$row[0]])) {
|
||||
$daily[$row[0]] = intval($row[1]);
|
||||
}
|
||||
}
|
||||
return $daily;
|
||||
}
|
||||
|
||||
/* The endpoints a key called over the last $days days, busiest first - rows of endpoint, calls, last_at. */
|
||||
function apiKeyUsageEndpoints($api_key_id, $days = 30) {
|
||||
global $mysqli;
|
||||
$rows = [];
|
||||
$sql = mysqli_query($mysqli, "SELECT api_key_usage_endpoint AS endpoint, SUM(api_key_usage_calls) AS calls, MAX(api_key_usage_last_at) AS last_at FROM api_key_usage
|
||||
WHERE api_key_usage_key_id = " . intval($api_key_id) . " AND api_key_usage_date > CURDATE() - INTERVAL " . max(1, intval($days)) . " DAY
|
||||
GROUP BY api_key_usage_endpoint ORDER BY calls DESC, endpoint ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$rows[] = $row;
|
||||
}
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/* The IP addresses a key was last called from on each day, over the last $days days - rows of ip and last_at, latest first. */
|
||||
function apiKeyUsageIps($api_key_id, $days = 30) {
|
||||
global $mysqli;
|
||||
$rows = [];
|
||||
$sql = mysqli_query($mysqli, "SELECT api_key_usage_last_ip AS ip, MAX(api_key_usage_last_at) AS last_at FROM api_key_usage
|
||||
WHERE api_key_usage_key_id = " . intval($api_key_id) . " AND api_key_usage_date > CURDATE() - INTERVAL " . max(1, intval($days)) . " DAY AND api_key_usage_last_ip <> ''
|
||||
GROUP BY api_key_usage_last_ip ORDER BY last_at DESC LIMIT 10");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$rows[] = $row;
|
||||
}
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/* Calls turned away with a key after it expired, over the last $days days - ['count', 'last_at']. */
|
||||
function apiKeyTurnedAway($api_key_id, $days = 30) {
|
||||
global $mysqli;
|
||||
$row = mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*), MAX(log_created_at) FROM logs
|
||||
WHERE log_type = 'API' AND log_action = 'Failed' AND log_entity_id = " . intval($api_key_id) . " AND log_created_at > NOW() - INTERVAL " . max(1, intval($days)) . " DAY"));
|
||||
return ['count' => intval($row[0]), 'last_at' => $row[1]];
|
||||
}
|
||||
|
||||
/*
|
||||
* After an API call is turned away: when this IP address has just reached
|
||||
* the admins' number of them in SIGN_IN_WINDOW_MINUTES, tell them
|
||||
* (signInAlertAdmins()).
|
||||
*/
|
||||
function apiKeyWatch() {
|
||||
global $mysqli, $session_ip;
|
||||
$limit = signInAlertLimit();
|
||||
$ip = escapeSql((string) $session_ip);
|
||||
if ($limit <= 0 || $ip === '') {
|
||||
return;
|
||||
}
|
||||
$count = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*) FROM logs WHERE log_type = 'API' AND log_action = 'Failed' AND log_ip = '$ip'
|
||||
AND log_created_at > NOW() - INTERVAL " . SIGN_IN_WINDOW_MINUTES . " MINUTE"))[0]);
|
||||
if ($count === $limit) {
|
||||
signInAlertAdmins(
|
||||
"$limit API calls turned away from $session_ip",
|
||||
"$limit API calls in " . SIGN_IN_WINDOW_MINUTES . " minutes from $session_ip were turned away - with a key that is wrong or has expired.",
|
||||
['IP address' => (string) $session_ip],
|
||||
'/admin/audit_logs.php?view=security&q=' . rawurlencode((string) $session_ip)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user