An audit table that the application can update is an audit table that a compromised application can rewrite.
GRANT INSERT, SELECT ON shop.audit_log TO 'app'@'%';
-- no UPDATE, no DELETE
-- and the retention, which needs a separate identity:
GRANT DELETE ON shop.audit_log TO 'audit_pruner'@'localhost';
-- plus the trigger, for the version that must resist a
-- compromised pruner as well:
CREATE TRIGGER audit_log_no_update BEFORE UPDATE ON audit_log
FOR EACH ROW SIGNAL SQLSTATE '45000';
The grant is the practical control and the trigger is the belt-and-braces version for anything with a compliance requirement. Both are easy to add at table creation and awkward to retrofit, because the retrofit has to establish that nothing currently updates the table — which on a table that has existed for two years is not obvious.