diff --git a/CHANGELOG.md b/CHANGELOG.md index f8d3a6215..7363703b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,9 @@ This file documents all notable changes made to ITFlow. ### Upgrading to 26.11 -Update from Maintenance > Update — Queue Update hands the job to cron, which updates the files and applies this release's database changes (2.7.9 to 3.3.2) in the same pass. Nothing else is required. +Update from Maintenance > Update — Queue Update hands the job to cron, which updates the files and applies this release's database changes (2.7.9 to 3.3.4) in the same pass. Nothing else is required. -This is a big release, with fifty-three database steps in one go, so take a backup before you update. +This is a big release, with fifty-five database steps in one go, so take a backup before you update. The update also clears out data that deletes in older versions left behind, all of it pointing at something that no longer exists: @@ -51,6 +51,10 @@ Document templates keep their names, summaries, what they say and their images. License templates keep everything they hold. A license called exactly what one license template is called - as the New License form named it when started from one - counts as made from that template, so a template's use starts out counted; where two templates share a name, neither is given its licenses. Other licenses don't know a template, so they count from the update. No template sets seats, a renewal term, a vendor or a product until you give it one, and none is required of anyone until you set it up on its page. Maintenance > Starter Content has a new License Templates pack to add. +Vendor templates keep everything they hold, and the vendors made from them stay linked to them. Who made each and last changed it is taken from the audit log, where it has a record of it. + +The app logs are kept for as many days as the audit log, as before, and each can now be set on its own in Admin > Settings > Logs, which is where the log retention went from Settings > Security. A retention of 0 now keeps a log forever (see Breaking Changes, Logs). Nothing is sent to a syslog server or SIEM until you set one up there. + If you are coming from 26.08 or earlier, the database update still has to be run from the command line one time, exactly as described under Upgrading to 26.09 below. ### Breaking Changes and Notes @@ -217,6 +221,7 @@ If you are coming from 26.08 or earlier, the database update still has to be run - Admins are now told about a run of failed sign-ins - 10 in 10 minutes from one IP address or against one account - with a notification and an email each. Change or turn it off in Admin > Settings > Security. - A call to the API with a key that has expired now says which key, and is logged against it. Anything that reads the `logs` table for "Incorrect or expired key" needs the new wording. - The `auth_logs` table is removed. Nothing was ever written to it. +- Logs: log retention moved from Admin > Settings > Security to Admin > Settings > Logs, where the audit log (with API key use) and the app logs each keep their own number of days. A retention of 0 now keeps a log forever - it used to delete everything older than today each night. - Audit log: each thing done now has one name, in the entries already there as well as new ones. Unarchive and Restored are Restore, Deleted is Delete, and Closed, Resolved, Reopened, Merged and Sent are Close, Resolve, Reopen, Merge and Send. An agent turned away from a client they can't open is Denied, where it was Access. A saved card or payment provider deleted is Delete and a card saved is Create, where both were Update, and a contract template changed is Edit. A report or script that reads the `logs` table by the old names needs the new ones. ### New Features & Updates @@ -575,6 +580,13 @@ If you are coming from 26.08 or earlier, the database update still has to be run - Starter Content has a License Templates pack: Microsoft 365 Business Basic, Standard and Premium, Exchange Online, Google Workspace, Bitdefender GravityZone, ESET PROTECT, Proofpoint Essentials, Bitwarden Teams, a FortiGate security bundle, Veeam Backup & Replication, Veeam Backup for Microsoft 365, Acronis Cyber Protect Cloud, Synology Active Backup, Adobe Acrobat Pro and Creative Cloud, QuickBooks Online, Zoom and Windows Server with its CALs. Each comes with its type, how it is licensed, how often it renews, notes on how it is licensed and assigned, and its vendor, and is sold as the product of the same name once the Products & Services pack is here. Who should have each is yours to set. Update brings the ones you added up to the current version. - The New License Template button's menu has Import, Export, the report and the starter ones. - Reports > License Templates: for each template, the licenses made from it in the period against the period before, its current licenses and clients, their seats in use against bought - over and spare - the ones renewing in the next 30 days or expired, how many of the clients that should have one do and, for anyone who can see Sales, how many are billed and what that comes to a month. By client too: the licenses each should have, the ones missing, licenses over seats, renewals and the ones not billed. One client can be picked, and each can be exported. +- **Vendor templates** (rebuilt) + - Admin > Vendor Templates lists what each is, its support line, hours and SLA, its support site and email, how many clients have a vendor made from it - and whether it is one of your own - when one was last added and when it last changed, and by whom. Searched by name, what they are, support line, email, site, hours, SLA, account manager or notes, sorted from the column headings, a page at a time, with Active and Archived tabs. + - A template's page shows what a vendor made from it starts with, its support line a click to call, the clients with a vendor from it - each with its own account number, or marked when it has none yet - a click from their vendor, the license templates that take their vendor from it, who made it and last changed it, and its history. Add to clients is on the page and the list. + - The template window has the vendor window's tabs - Details, Support, Account and Notes - with their icons. Editing, any of its fields can be changed on the vendors made from it too, ticked one by one; what the window says replaces what each one had, and each vendor's history says so. The audit log says what changed on the template. + - Templates can be copied, archived - not offered on New Vendor or Add to Clients, while the vendors made from them stay - restored and deleted. Deleted, its vendors and the license templates that used it stay, no longer linked to it. + - Make a template from it, in the menu of a client's vendor for an admin, starts a template from that vendor - leaving its account number, PIN and account manager, which are the client's own - and links the vendor to it, so other clients can have the same one. + - A vendor made from a template links to it from its page, for an admin. - **Assets** - The asset list has: - type icons, colored status pills and OS icons @@ -937,7 +949,7 @@ If you are coming from 26.08 or earlier, the database update still has to be run - Revoke ends a key today. Delete is for expired keys, as before, one at a time or several from the bulk menu. - The audit log says who a new key acts as and when it ends, and what an edit changed: "renamed from RMM, expiring 2027-06-01 (was 2026-12-01), acting as Tess Tech (was Johnny Test)". - **Audit logs** (rebuilt) - - Admin > Audit Logs lists who did what, when and from where, with views for Changes, Deletions, Sign-ins, Security (failed and blocked sign-ins, an agent turned away from a client, viewing the portal as a contact, and changes to users, roles, API keys, the master key, settings and backups) and Data out (passwords looked at, exports, downloads and shared links), each counted for the filters you have picked. + - Admin > Audit Logs lists who did what, when and from where, with views for Changes, Deletions, Sign-ins, Security (failed and blocked sign-ins, an agent turned away from a client, viewing the portal as a contact, app logs cleared, and changes to users, roles, API keys, the master key, settings and backups) and Data out (passwords looked at, exports, downloads and shared links), each counted for the filters you have picked. - Search finds an entry by what it says, who did it, the client, the IP address or the browser, and it can be narrowed by the date, who did it - an agent, a client contact in the portal, or ITFlow itself for cron, the API and the email parser - the client, the type and, under More filters, the action. It sorts from the column headings, a page at a time. - Each entry shows when, who with their picture, the client, the kind of record with the action as a coloured label, what it says, and the IP address with the system and browser in words. A script calling the API shows as itself, for example python-requests. - An entry about a record links to it while it is still there - a client, contact, location, vendor, asset, network, rack, credential, certificate, domain, license, service, document, file, shared link, ticket, project, quote, invoice, recurring invoice, product, expense, recurring expense, trip or template - and says No longer here once it has been deleted. A credential, certificate, domain, file, expense or trip opens on its list with its quick look open. @@ -954,6 +966,13 @@ If you are coming from 26.08 or earlier, the database update still has to be run - Admin > Sign-ins, under Access: People - each agent and portal contact with their last sign in, its IP address and device, how many sign-ins and failed ones (the wrong MFA codes among them), how many IP addresses and, for a date range, how many of them were new - and IP addresses - each address with its sign-ins and failures, the accounts it tried and any emails with no account, when it was first and last seen, and whether it is being turned away now. Searched by name, email or address, for a date range, sorted from the column headings, a page at a time; each opens its sign-ins in the audit log. - Failed sign-in alerts: after a number of failed sign-ins in 10 minutes from one IP address or against one account - 10 to start with, or 5, 20, 50 or never, in Admin > Settings > Security - each admin gets a notification and an email saying who was tried and from where, with a link to look. A run is told about once. The same goes for API calls turned away from one address. Each alert is in the audit log's Security view. - Admin > API Keys counts each key's calls over the last 30 days and shows where the last one came from. Usage, on a key's menu or its count, shows its calls today, this week and this month, each day's as a bar, the endpoints it called and how often, the IP addresses the calls came from, and the calls turned away after it expired. Use is kept as long as the audit log. + - Admin > App Logs is rebuilt: what ITFlow itself does - cron runs, mail, payment providers, the API - by level, Errors, Warnings and Info (and Debug once there is any), each counted for the filters you have picked, with each entry's level as a coloured label and its category with an icon. Search finds an entry by its category or what it says, and it can be narrowed by category - each with how many entries it has - and by date. It sorts from the column headings, a page at a time. + - Each app log entry opens a quick look with all of it, to copy into a bug report, the other app log entries in the five minutes either side, and what was done in the audit log then - so a payment provider's error sits beside the payment. + - The app logs can be exported as a CSV or PDF, with the columns you choose, and cleared - the entries shown, with the search, level, category and dates you picked, or every one. Clearing is in the audit log, in its Security view, saying what went. The audit log itself can't be cleared. + - Admin > Settings > Logs, after Security: how many days the audit log, with API key use, and the app logs are each kept, or Keep forever, with how many entries each holds and when the oldest is from. + - Sending logs on: each audit log entry, and the app log entries from a level up (errors, warnings and errors - to start with - info and up, or everything), can go to a syslog server or SIEM as it is written - Graylog, Splunk, Wazuh, Microsoft Sentinel, QRadar and the like. Over UDP, TCP or TLS (with the server's certificate checked, or not for one on your own network), as JSON, CEF or plain text, as standard syslog messages. Save and send a test sends a test message and says whether it got there - over UDP, whether nothing was listening. A server that can't be reached doesn't slow ITFlow down: the failure goes in the app logs under Log Forwarding, nothing is sent for a minute, and the settings page shows it with a link to it. What an entry looks like in each format is shown from the latest one. + - Reports > Audit Activity, under Activity: what was done in ITFlow in a period - changes, deletions, passwords looked at, exports and, for an admin, sign-ins and the ones that failed - against the period before, over time, and by the hour of the week, with how much was done out of the business hours set for SLAs. Then by person (people since deleted together, and ITFlow itself last), by client and by kind of record, each linking to those entries in the audit log, and for an admin the security events. One client or one agent can be picked, and each table exported. Anyone else sees only the kinds of entry their role covers, for the clients they can open. + - Admin > Audit Logs' Export has a menu with the Activity report, the log settings and the App Logs. - **Emails** (rebuilt) - Ticket emails to clients now look like the billing emails: your logo or name, a coloured heading saying what it is, the ticket's number, subject, status and priority in a box, the reply or the details quoted as they were written with who wrote them, a button to open the ticket, and your details at the foot. This covers a ticket opened by an agent, on a schedule or from an email, a reply, resolved, closed, being made a watcher, replying to a closed ticket, a reply through the API and a task needing their approval. - Each says under it why they get it - the contact on the ticket, watching it, or asking for every update on their company's tickets - in place of the YOU ARE A COLLABORATOR block. @@ -1094,6 +1113,9 @@ If you are coming from 26.08 or earlier, the database update still has to be run - All Client Docs listed Credentials to agents who can't open it. - Notifications: shared-link notifications reached agents without Support, with a link they couldn't open. - License templates: the type picker showed a stray "<" at the top. +- Vendor templates: Delete was only offered to the built-in Administrator role, not to other admin roles. +- App Logs: the category filter did nothing - it read a misspelt field - and the date filter was slow on a large log. +- App logs: an entry over 1,000 characters with an accented letter or a symbol where it was cut could fail to save, stopping what was logging it. - Admin > Settings > Defaults: Payment Method and Expense Payment Method listed the payment method categories that moved to Admin > Payment Methods in 2.3.1, so a new install had nothing to pick. They list Admin > Payment Methods now. - Messages, hints and window titles were reworded where they were unclear, and bulk windows say "1 asset" or "3 assets" correctly. @@ -1615,6 +1637,19 @@ If you are coming from 26.08 or earlier, the database update still has to be run - `auditLogWho()` marks a failed sign in Their account; Security also takes Login Alert and API Failed; Alert is a danger action. Admin > Audit Logs, the client's Audit Log, Your account > Activity and the quick look badge `signInNewIpIds()`. - New `admin/sign_ins.php` (`view` - '' People or `ips` - `q`, the date filter, `sort` - People `user_name`, `sign_ins`, `failures`, `ips` or `last_seen`, IP addresses `log_ip`, `sign_ins`, `failures`, `accounts`, `first_seen` or `last_seen` - `order`, `page`), in the admin nav under Access. Settings > Security takes `config_login_alert_failures` (one of `signInAlertOptions()`, else 10). - API keys, in `functions/api_keys.php`: `apiKeyUsageEndpoint($uri)`, `apiKeyUsageRecord($id, $endpoint, $ip)` (an upsert per call; nothing before the update), `apiKeyUsageSummary($ids, $days)` (`calls`, `endpoints`, `last_at`, `last_ip`), `apiKeyUsageDaily()`, `apiKeyUsageEndpoints()`, `apiKeyUsageIps()`, `apiKeyTurnedAway()` and `apiKeyWatch()`. `api/v1/validate_api_key.php` records each call, and logs a call turned away as "Expired API key NAME used - it ended DATE" with `log_entity_id` the key, or "Unknown API key used". New `admin/modals/api/api_key_usage.php` (`id`); deleting a key deletes its use. + - Database 3.3.3: `settings.config_app_log_retention` (int, 90; copied from `config_log_retention` when it is added), `config_log_forward_enabled` (0), `config_log_forward_host` (varchar 200, NULL), `config_log_forward_port` (514), `config_log_forward_protocol` (`udp`, `tcp` or `tls`), `config_log_forward_format` (`json`, `cef` or `plain`), `config_log_forward_audit` (1), `config_log_forward_app_level` (`warning`; `error`, `info`, `debug` or '' for none) and `config_log_forward_tls_verify` (1), each added only when missing; an `app_log_created_at` key on `app_logs`. Nightly deletes `logs` and `api_key_usage` older than `config_log_retention` days and `app_logs` older than `config_app_log_retention`, each only when it is above 0. Settings > Security no longer takes `config_log_retention`, and nightly no longer reads it with the other settings. + - `logApp()` takes plain text, as before, and now checks the level: lowercased, `critical`, `fatal`, `alert` and `emergency` to `error`, `warn` to `warning`, anything else `info`, as the column takes only `info`, `warning`, `error` and `debug`. The category and details are cut by character (`mb_substr()`). + - New `functions/app_log.php`: `appLogLevels()` (`label`, `one`, `icon`, `tone`, `title`), `appLogLevel()`, `appLogLevelPill()`, `appLogCategoryIcon()` (by name, or the part before a dash), `appLogViews()`, `appLogFiltersSql($src, &$summary)` (`q`, `category`, `view` - a level - `dtf`, `dtt`; a half-open range on `app_log_created_at`), `appLogFiltersText($summary)`, `appLogDate()`, `appLogCategories()` (name => count), `appLogRetention()` (`audit`, `app`; read apart from the page settings, falling back before the update), `appLogRetentionText()` and `appLogSize($table)` (`count`, `oldest`). + - Admin: `admin/app_logs.php` is rebuilt (`q`, `category`, `view` - `error`, `warning`, `info` or `debug` - the date filter, `sort` - `app_log_created_at`, `app_log_type` (worst first), `app_log_category` or `app_log_id` - `order`, `page` and `open`, an entry's id to open its quick look). New `admin/modals/app_log/app_log.php` (`id`), `app_log_export.php` and `app_log_clear.php`, sharing `app_log_filters.php`, and `admin/post/app_log.php` with `export_app_logs` (the same filters, `columns[]` from the new `app_logs` export columns) and `clear_app_logs` (`scope` - `shown`, with the filters, or `all`), audited as App Log Export and Delete. `auditLogKinds()` has App Log, the Security view takes App Log Delete, and Stripe's icon is one the free icons have. + - Forwarding, in the new `functions/log_forward.php`: `LOG_FORWARD_PROTOCOLS`, `LOG_FORWARD_FORMATS`, `LOG_FORWARD_TIMEOUT` (2 seconds), `LOG_FORWARD_BACKOFF` (60), `LOG_FORWARD_BATCH` (50), `LOG_FORWARD_FACILITY` (16, local0) and `LOG_FORWARD_CATEGORY` (Log Forwarding); `logForwardSettings($fresh)` (read apart from the page settings - off before the update), `logForwardTidy()`, `logForwardOn()`, `logForwardTarget()`, `logForwardAppLevels()` and `logForwardAppLevelSent()`. `logAudit()` and `logApp()` call `logForwardQueue($log, $id, $level, $category)` with the new row's id, as do the three places that insert into `logs` themselves (`api/v1/validate_api_key.php`, `client/login_reset.php` and `guest/guest_pay_invoice_stripe.php`) - nothing during an email preview, or for Log Forwarding's own entries. `logForwardFlush()` runs on shutdown and at every 50 queued: it reads the entries back by id, so one written in a transaction that was rolled back isn't sent, and sends them in the order they happened over one connection for the request (`logForwardSend()`, `logForwardConnect()` - TLS 1.2 or later, the peer and its name checked unless `tls_verify` is off - and `logForwardWrite()`). TCP and TLS end each message with a newline (RFC 6587 non-transparent framing); UDP sends one datagram each. On a failure, `logForwardFailed()` writes an app log error under Log Forwarding, with the reason in words (`logForwardReason()`), and a file in the temp directory (`logForwardBackoffFile()`) stops sending for 60 seconds; the entries in between are not sent. + - Each message is RFC 5424 - `1 TIMESTAMP HOST itflow - MSGID - MSG`, the time with ITFlow's timezone offset, the host from `$config_base_url` (else the server's name), MSGID `audit`, `app` or `test`. Severity: an audit entry that failed, was turned away, denied or alerted on is warning (4), a deletion notice (5) and the rest info (6); an app log entry error 3, warning 4, info 6 and debug 7. As JSON, an audit entry has `log`, `id`, `time`, `type`, `action`, `description`, `severity`, `who` (agent, portal or system), `user_id`, `user`, `user_email`, `client_id`, `client`, `record_id`, `ip`, `user_agent` and `host`, and an app log entry `log`, `id`, `time`, `level`, `category`, `details` and `host`. As CEF: `CEF:0|ITFlow|ITFlow|version|type:action|type action|severity|` with `rt`, `externalId`, `cat`, `act`, `msg`, `suid`, `suser`, `src`, `requestClientApplication`, `cs1` (client), `cn1` (client id), `cn2` (record id), `cs2` (who) and `dvchost`, or for an app log entry `app:category` with `cat`, `msg` and `cs1` (level). `logForwardAuditMessage()`, `logForwardAppMessage()` and `logForwardTest($settings, $by)` (`ok`, `error`; over UDP a port that answers it is closed fails). + - New `admin/settings_logs.php`, in the Settings menu after Security, and `admin/post/settings_logs.php`: `edit_log_settings` and `test_log_forward` (saves, then sends a test) take `config_log_retention` and `config_app_log_retention` (1 to 36500), `audit_forever` and `app_forever` (0), and `config_log_forward_enabled`, `_host` (a name or address - one typed with its port, `logs.example.com:1514`, is taken as the two; anything else is dropped and sending stays off), `_port`, `_protocol`, `_format`, `_audit`, `_app_level` and `_tls_verify` (kept as it was unless TLS). Settings Edit in the audit log says what changed. + - Report: `agent/reports/audit_activity.php` (`client_id`, `agent` and the period; `export=csv` by person, `table=clients`, `kinds`, or for an admin `security`), `audit_activity` in `reportCatalog()` in a new Activity group, needing `module_client`; entries scoped by `auditLogRoleSql()` and `clientScopeSql()`. Out of hours uses the SLA business days and hours (`getSlaSettings()`), else 9 to 5 on weekdays. +- Vendor templates: + - Database 3.3.4: `vendor_templates.vendor_template_created_by` and `vendor_template_updated_by` (0), taken from the first Vendor Template Create and the latest Edit in the audit log with `log_entity_id` the template, where they are 0, without touching `vendor_template_updated_at`. + - New `functions/vendor_template.php`: `vendorTemplateFieldLabels()` (key => `label`, `icon`), `vendorTemplateColumns()` (key => [template column, vendor column] pairs - the support line is three), `vendorTemplateInfo()` (archived or not, with `created_by_name` and `updated_by_name`; `vendorTemplateRow()` in `functions/vendor.php` stays the current-only one used to make vendors), `vendorTemplateUsage($ids)` (`clients`, `vendors`, `ours`, `last` - current vendors, current clients), `vendorTemplateFields($post, &$error)` (as `vendorFormSet()` reads a vendor's, and the support email checked), `vendorTemplateRowFields()`, `vendorTemplateSave()`, `vendorTemplateChanged()`, `vendorTemplateCopy()`, `vendorTemplateDelete()` (vendors and license templates unlinked, their updated dates kept), `vendorTemplatePush($id, $keys)`, `vendorTemplateVendors()`, `vendorTemplateLicenseTemplates()`, `vendorTemplateTile()` and `vendorTemplatePhoneHtml()`. + - Admin: `admin/vendor_templates.php` is rebuilt (`q` - a support line searched by its digits - `sort` - `vendor_template_name`, `used_count`, `last_used` or `changed_at` - `order`, `page` and `show=archived`, `archived=1` still taken). New `admin/vendor_template.php` (`vendor_template_id`). `admin/modals/vendor_template/vendor_template_form.php` is shared by `vendor_template_add.php` (`from_vendor`) and `vendor_template_edit.php` (`id`); both post to `/admin/post.php`. `admin/post/vendor_template.php`: `add_vendor_template` (`from_vendor` links that vendor; redirects to its page) and `edit_vendor_template` (`push[]` - keys of `vendorTemplateFieldLabels()`, replacing the `global_update_vendor_*` and `update_base_vendors` fields), GET `copy_vendor_template`, `archive_vendor_template`, `restore_vendor_template` and `delete_vendor_template` (`from_page`). Audit entries are Vendor Template Create (with where a copy or one made from a vendor came from), Edit (with what changed), Archive, Restore and Delete, on the template, and Vendor Edit on each vendor a push changed. It no longer includes `agent/post/vendor.php`, so the agent vendor handlers don't run on `admin/post.php`. + - `auditLogKinds()` links Vendor Template entries to the template's page. `agent/vendor.php` links its template for an admin, and has Make a template from it for a vendor with none. - Empty states: every list page sets `$empty_state` before including `includes/filter_footer.php`, and `includes/inc_empty_state.php` draws it with `emptyStateHtml()` (`functions/format.php`) when the list has nothing - `thing`, `icon`, `title`, `text`, `actions` (`label`, `modal` or `href`, `size`, `icon`; the first is the main one), `archived`, `view`, `filtered`, `views`, `match_title`, `match_text` and `clear_url`. `emptyStateFiltered($views)` reads the GET string for a search or filter, leaving out paging, sorting, `client_id`, `archived` and the page's views, and `emptyStateClearUrl($views)` is the page without them. `emptyStatePill($pills, $key, $default, $good)` builds a pill's own state from the page's pill array (`label`, `icon`, `title`, `alarm`), with a tick for `alarm` pills and the keys in `$good`. Files, notifications and racks call `emptyStateHtml()` in their own layout; files and notifications set `$empty_state_skip`. A page that sets nothing still gets one, worded from `$page_title`. The pages' own empty blocks (`.itflow-asset-empty` under a list) are gone. CSS: `.itflow-empty-state` with `-icon` (a grey round tile), `-title`, `-text` and `-actions`. - Removed unused helpers and files: - `credentialLink()`, `projectLink()`, `linkCounts()`, `assetContactAvatar()`, `clientOverviewPersonInitials()`, and `getMonthlyTax()` and `getQuarterlyTax()`, which looked a tax up by name diff --git a/admin/app_logs.php b/admin/app_logs.php index 1c8fb49bd..9395d9baf 100644 --- a/admin/app_logs.php +++ b/admin/app_logs.php @@ -1,177 +1,222 @@ App Logs - what ITFlow itself does and runs into: cron runs, mail + * going out and coming in, payment providers, the API, errors. Listed by + * level - errors, warnings, info and debug, each counted for the filters + * picked - searched, and filtered by category and date. Each entry opens a + * quick look with all of it and what happened around then + * (modals/app_log/app_log.php). What is shown can be exported + * (export_app_logs) or cleared (clear_app_logs). The work is in + * functions/app_log.php. + */ + // Default Column Sortby Filter $sort = "app_log_id"; $order = "DESC"; require_once "includes/inc_all_admin.php"; -// Log Type Filter -if (isset($_GET['type']) & !empty($_GET['type'])) { - $log_type_query = "AND (app_log_type = '" . escapeSql($_GET['type']) . "')"; - $type_filter = escapeHtml($_GET['type']); -} else { - // Default - any - $log_type_query = ''; - $type_filter = ''; +// Tab title - the file is named for the table, the page for what it holds +$page_title = 'App Logs'; + +// Only these sort - an unknown column would take the page down +$sortable_columns = [ + 'app_log_id' => "app_log_id $order", + 'app_log_created_at' => "app_log_created_at $order, app_log_id $order", + 'app_log_type' => "FIELD(app_log_type, 'error', 'warning', 'info', 'debug') " . ($order === 'ASC' ? 'DESC' : 'ASC') . ", app_log_id DESC", + 'app_log_category' => "app_log_category IS NULL, app_log_category $order, app_log_id DESC", +]; +if (!isset($sortable_columns[$sort])) { + $sort = 'app_log_id'; } -// Log Category Filter -if (isset($_GET['category']) & !empty($_GET['catergory'])) { - $log_category_query = "AND (app_log_category = '" . escapeSql($_GET['category']) . "')"; - $category_filter = escapeHtml($_GET['category']); -} else { - // Default - any - $log_category_query = ''; - $category_filter = ''; -} - -$sql = mysqli_query( - $mysqli, - "SELECT SQL_CALC_FOUND_ROWS app_log_category, app_log_created_at, app_log_details, app_log_id, app_log_type FROM app_logs - WHERE (app_log_type LIKE '%$q%' OR app_log_category LIKE '%$q%' OR app_log_details LIKE '%$q%') - AND DATE(app_log_created_at) BETWEEN '$dtf' AND '$dtt' - $log_type_query - $log_category_query - ORDER BY $sort $order LIMIT $record_from, $record_to" -); +$views = appLogViews(); +$view = isset($_GET['view']) && is_string($_GET['view']) && isset($views[$_GET['view']]) ? $_GET['view'] : ''; +$category_filter = is_string($_GET['category'] ?? null) ? trim($_GET['category']) : ''; +$filter_src = array_merge($_GET, ['dtf' => $dtf, 'dtt' => $dtt, 'view' => $view, 'category' => $category_filter]); +$sql = mysqli_query($mysqli, "SELECT SQL_CALC_FOUND_ROWS app_log_id, app_log_category, app_log_type, app_log_details, app_log_created_at + FROM app_logs + WHERE " . appLogFiltersSql($filter_src) . " + ORDER BY {$sortable_columns[$sort]} + LIMIT $record_from, $record_to"); $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); +// Each level counted for the rest of the filters +$count_sql = []; +foreach ($views as $definition) { + $count_sql[] = 'COALESCE(SUM(' . ($definition['where'] === '' ? '1' : $definition['where']) . '), 0)'; +} +$counts = array_combine(array_keys($views), array_map('intval', mysqli_fetch_row(mysqli_query($mysqli, "SELECT " . implode(', ', $count_sql) . " + FROM app_logs WHERE " . appLogFiltersSql(array_merge($filter_src, ['view' => ''])))))); +// Nothing logs at debug as it comes - its view shows once something does +if (!$counts['debug'] && $view !== 'debug') { + unset($views['debug']); +} + +$categories = appLogCategories(); +$any_filter = $q !== '' || $category_filter !== '' || $dtf !== '1970-01-01' || $dtt !== '2099-12-31'; +$retention = appLogRetention(); + +$sort_link = function ($column, $label) use ($url_query_strings_sort, $sort, $disp, $order_icon) { + return '' + . $label . ($sort === $column ? " $order_icon" : '') . ''; +}; +// A view keeps the filters, and starts from its first page +$view_url = function ($key) { + $query = $_GET; + unset($query['page'], $query['open']); + $query['view'] = $key; + return '?' . http_build_query(array_filter($query, function ($value) { + return $value !== '' && $value !== null && $value !== []; + })); +}; +// The page with just this category - from a category in the list +$category_url = function ($category) { + $query = $_GET; + unset($query['page'], $query['open']); + $query['category'] = $category; + return '?' . http_build_query($query); +}; +$filters_url = function ($modal) use ($dtf, $dtt, $view, $category_filter) { + return buildExportModalUrl($modal, ['q'], ['dtf' => $dtf, 'dtt' => $dtt, 'view' => $view, 'category' => $category_filter]); +}; + ?> -
-
-

App Logs

-
-
-
-
-
-
-
- - - -
-
-
- -
-
- -
-
- -
-
- -
-
+
+
+

App Logs

+
+
+ + + -
" id="advancedFilter"> -
-
-
- - - - - -
-
-
-
- +
-
- - "> - - - - - - - - - - - - - - - - - - - - -
- - Timestamp - - - - Type - - - - Category - - - - Details - -
-
- 'app logs', - 'icon' => 'fa-history', - 'text' => 'What ITFlow itself does - cron runs, mail, errors - shows here.', - ]; - ?> -
+
+
+ +
+
+
+ + +
+
+
+ + + + +
+
+ +
+
+
+ +
+
+ + "> + + + + + + + + + + 240 ? rtrim(mb_substr($details, 0, 240)) . '…' : $details; + ?> + + + + + + + + + +
Details
+
+
+
+ + + + - + + +
+
+ +
+
+ 'app log entries', + 'icon' => 'fa-clipboard-list', + 'filtered' => $any_filter, + 'views' => ['view'], + 'view' => $view !== '' ? ['icon' => $views[$view]['icon'], 'title' => 'No ' . ($view === 'info' ? 'info entries' : strtolower($views[$view]['label'])) . ' yet', 'text' => escapeHtml($views[$view]['title']) . ' shows here.'] : null, + 'text' => 'What ITFlow itself does - cron runs, mail, payment providers, the API - and the errors it runs into show here. ' . ($retention['app'] ? 'They are kept for ' . appLogRetentionText($retention['app']) . '.' : 'They are kept until they are cleared.'), + ]; + require_once "../includes/filter_footer.php"; + ?> +
+ + + +

Audit Logs

diff --git a/admin/database_updates/3.3.3.php b/admin/database_updates/3.3.3.php new file mode 100644 index 000000000..3dca77a63 --- /dev/null +++ b/admin/database_updates/3.3.3.php @@ -0,0 +1,56 @@ + 0; + }; + + // 1. The app logs are kept for their own number of days, starting from + // what the audit log keeps, as both were kept for that until now. For + // both, 0 is now keep forever. + if (!$itflow_has_column('config_app_log_retention')) { + mysqli_query($mysqli, "ALTER TABLE `settings` ADD COLUMN `config_app_log_retention` int(11) NOT NULL DEFAULT 90"); + mysqli_query($mysqli, "UPDATE `settings` SET `config_app_log_retention` = `config_log_retention`"); + } + + // 2. Sending log entries on to a syslog server or SIEM as they are + // written: on or off, where to (host, port, and UDP, TCP or TLS), how + // (JSON, CEF or plain text), what (the audit log, and the app logs from + // a level up - '' for none) and, over TLS, whether the server's + // certificate is checked. + $itflow_columns = [ + 'config_log_forward_enabled' => "tinyint(1) NOT NULL DEFAULT 0", + 'config_log_forward_host' => "varchar(200) DEFAULT NULL", + 'config_log_forward_port' => "int(11) NOT NULL DEFAULT 514", + 'config_log_forward_protocol' => "varchar(10) NOT NULL DEFAULT 'udp'", + 'config_log_forward_format' => "varchar(10) NOT NULL DEFAULT 'json'", + 'config_log_forward_audit' => "tinyint(1) NOT NULL DEFAULT 1", + 'config_log_forward_app_level' => "varchar(10) NOT NULL DEFAULT 'warning'", + 'config_log_forward_tls_verify' => "tinyint(1) NOT NULL DEFAULT 1", + ]; + foreach ($itflow_columns as $itflow_column => $itflow_definition) { + if (!$itflow_has_column($itflow_column)) { + mysqli_query($mysqli, "ALTER TABLE `settings` ADD COLUMN `$itflow_column` $itflow_definition"); + } + } + + // 3. The app logs are listed and cleared by date, and nightly deletes the + // old ones by date - a key on it. + $itflow_index = mysqli_query($mysqli, "SELECT 1 FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'app_logs' AND INDEX_NAME = 'app_log_created_at' LIMIT 1"); + if (!$itflow_index || mysqli_num_rows($itflow_index) === 0) { + mysqli_query($mysqli, "ALTER TABLE `app_logs` ADD KEY `app_log_created_at` (`app_log_created_at`)"); + } + + unset($itflow_has_column, $itflow_columns, $itflow_column, $itflow_definition, $itflow_index); diff --git a/admin/database_updates/3.3.4.php b/admin/database_updates/3.3.4.php new file mode 100644 index 000000000..1ce8e465a --- /dev/null +++ b/admin/database_updates/3.3.4.php @@ -0,0 +1,37 @@ + 0 AND `log_user_id` > 0 GROUP BY `log_entity_id` + ) AS `made` ON `made`.`log_entity_id` = `vendor_templates`.`vendor_template_id` + SET `vendor_template_created_by` = `made`.`user_id`, `vendor_template_updated_at` = `vendor_template_updated_at` + WHERE `vendor_template_created_by` = 0"); + mysqli_query($mysqli, "UPDATE `vendor_templates` JOIN ( + SELECT `log_entity_id`, SUBSTRING_INDEX(GROUP_CONCAT(`log_user_id` ORDER BY `log_id` DESC), ',', 1) AS `user_id` + FROM `logs` WHERE `log_type` = 'Vendor Template' AND `log_action` = 'Edit' AND `log_entity_id` > 0 AND `log_user_id` > 0 GROUP BY `log_entity_id` + ) AS `changed` ON `changed`.`log_entity_id` = `vendor_templates`.`vendor_template_id` + SET `vendor_template_updated_by` = `changed`.`user_id`, `vendor_template_updated_at` = `vendor_template_updated_at` + WHERE `vendor_template_updated_by` = 0 AND `vendor_template_updated_at` IS NOT NULL"); + + unset($itflow_column); diff --git a/admin/includes/side_nav.php b/admin/includes/side_nav.php index e053ade01..2fcbf98c5 100644 --- a/admin/includes/side_nav.php +++ b/admin/includes/side_nav.php @@ -150,7 +150,7 @@ --> - +
+ + + + '']) ?> +
+ + + + 'Nothing was done for a client in this period.', 'totals' => $client_totals]) ?> +
+ + + 'Nothing in this period.', 'totals' => $kind_totals]) ?> + + + + + + + + diff --git a/agent/vendor.php b/agent/vendor.php index cf4df5b9f..1082654a2 100644 --- a/agent/vendor.php +++ b/agent/vendor.php @@ -249,7 +249,13 @@ if (!$vendor || (isset($_GET['client_id']) && $vendor_client_id !== intval($clie
- + + + + + + +
@@ -316,6 +322,12 @@ if (!$vendor || (isset($_GET['client_id']) && $vendor_client_id !== intval($clie