test(configdb): M2.10 review fix — catch bracketed AuditLog identifiers; document EF/multi-line scan limits (#18)
Extends ContainsAuditLogMutation regex to match T-SQL bracketed forms ([AuditLog], [dbo].[AuditLog]) that SSMS-generated SQL produces; the prior optional-schema pattern only matched bare/dbo-prefixed names, silently missing these real violation forms. Changes: - Schema sub-pattern (?:dbo\.)? → (?:\[?dbo\]?\.)? (matches dbo. and [dbo].) - Table sub-pattern AuditLog\b → \[?AuditLog\]?\b (matches AuditLog and [AuditLog]) - Pattern compiled as static readonly Regex field for clarity/performance - Adds 4 new planted-positive cases: UPDATE [dbo].[AuditLog], UPDATE [AuditLog], DELETE FROM [dbo].[AuditLog], DELETE FROM [AuditLog] - Retains all existing negatives; adds DELETE FROM [dbo].[Notifications] negative - Fixes misleading "reverse order" comment on the comment-prefix positive case - Documents scan limitations (EF Core bulk methods; multi-line DML) in class XML doc
This commit is contained in:
+57
-19
@@ -13,8 +13,8 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests;
|
||||
///
|
||||
/// <b>Matching rule (see <c>ContainsAuditLogMutation</c> for full detail)</b>
|
||||
/// A line is flagged as a violation iff it matches the DML-syntax pattern:
|
||||
/// • <c>UPDATE\s+(?:dbo\.)?AuditLog\b</c> — UPDATE targeting AuditLog
|
||||
/// • <c>DELETE\s+(?:FROM\s+)?(?:dbo\.)?AuditLog\b</c> — DELETE targeting AuditLog
|
||||
/// • <c>UPDATE\s+(?:\[?dbo\]?\.)?(?:\[?AuditLog\]?)\b</c> — UPDATE targeting AuditLog
|
||||
/// • <c>DELETE\s+(?:FROM\s+)?(?:\[?dbo\]?\.)?(?:\[?AuditLog\]?)\b</c> — DELETE targeting AuditLog
|
||||
///
|
||||
/// These tight DML-syntax patterns naturally exclude false positives:
|
||||
/// - DENY UPDATE ON dbo.AuditLog … → "DENY" comes before UPDATE; the regex
|
||||
@@ -25,6 +25,12 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests;
|
||||
/// - Comments like "// AuditLog … UPDATE …" → UPDATE is not immediately followed
|
||||
/// by AuditLog (there are intervening words).
|
||||
/// - DELETE FROM Notifications … → AuditLog not present.
|
||||
///
|
||||
/// <b>Known limitations:</b> This guard scans only raw SQL strings — EF Core methods
|
||||
/// such as <c>ExecuteDeleteAsync</c>, <c>ExecuteUpdateAsync</c>, and <c>RemoveRange</c>
|
||||
/// targeting the AuditLog entity are NOT covered and must never be introduced.
|
||||
/// Additionally, the scan is line-oriented: DML where the keyword and table name appear
|
||||
/// on separate lines is an accepted, undetected edge case.
|
||||
/// </summary>
|
||||
public class AuditLogAppendOnlyGuardTests
|
||||
{
|
||||
@@ -72,10 +78,11 @@ public class AuditLogAppendOnlyGuardTests
|
||||
///
|
||||
/// <b>Matching rule.</b> The regex requires the DML keyword to be
|
||||
/// immediately followed (possibly via FROM) by the optional schema prefix
|
||||
/// <c>dbo.</c> and then the table name <c>AuditLog</c> as a whole word:
|
||||
/// (<c>dbo.</c> or <c>[dbo].</c>) and then the table name <c>AuditLog</c>
|
||||
/// or <c>[AuditLog]</c> as a whole word:
|
||||
/// <code>
|
||||
/// UPDATE\s+(?:dbo\.)?AuditLog\b
|
||||
/// DELETE\s+(?:FROM\s+)?(?:dbo\.)?AuditLog\b
|
||||
/// UPDATE\s+(?:\[?dbo\]?\.)?(?:\[?AuditLog\]?)\b
|
||||
/// DELETE\s+(?:FROM\s+)?(?:\[?dbo\]?\.)?(?:\[?AuditLog\]?)\b
|
||||
/// </code>
|
||||
/// This tight DML-syntax pattern naturally excludes false positives without
|
||||
/// any additional keyword checks:
|
||||
@@ -106,23 +113,31 @@ public class AuditLogAppendOnlyGuardTests
|
||||
}
|
||||
|
||||
// DML-syntax pattern: the UPDATE or DELETE keyword must be directly followed
|
||||
// (optionally via FROM) by the optional "dbo." schema qualifier and then the
|
||||
// table name "AuditLog" as a whole word.
|
||||
// (optionally via FROM) by the optional schema qualifier and then the table name.
|
||||
//
|
||||
// UPDATE\s+(?:dbo\.)?AuditLog\b
|
||||
// matches: "UPDATE AuditLog …", "UPDATE dbo.AuditLog …"
|
||||
// does NOT match: "DENY UPDATE ON dbo.AuditLog" (UPDATE is followed by ON, not AuditLog)
|
||||
// Schema sub-pattern : (?:\[?dbo\]?\.)?
|
||||
// matches: nothing, "dbo.", "[dbo]."
|
||||
//
|
||||
// DELETE\s+(?:FROM\s+)?(?:dbo\.)?AuditLog\b
|
||||
// matches: "DELETE FROM AuditLog …", "DELETE FROM dbo.AuditLog …", "DELETE dbo.AuditLog …"
|
||||
// does NOT match: "DENY DELETE ON dbo.AuditLog" (DELETE is followed by ON, not FROM/AuditLog)
|
||||
const string dmlPattern =
|
||||
@"\bUPDATE\s+(?:dbo\.)?AuditLog\b" +
|
||||
@"|\bDELETE\s+(?:FROM\s+)?(?:dbo\.)?AuditLog\b";
|
||||
|
||||
return Regex.IsMatch(text, dmlPattern, RegexOptions.IgnoreCase);
|
||||
// Table sub-pattern : \[?AuditLog\]?
|
||||
// matches: "AuditLog", "[AuditLog]"
|
||||
//
|
||||
// UPDATE\s+(?:\[?dbo\]?\.)?(?:\[?AuditLog\]?)\b
|
||||
// matches: "UPDATE AuditLog", "UPDATE dbo.AuditLog",
|
||||
// "UPDATE [AuditLog]", "UPDATE [dbo].[AuditLog]"
|
||||
// does NOT match: "DENY UPDATE ON dbo.AuditLog" (UPDATE is followed by ON)
|
||||
//
|
||||
// DELETE\s+(?:FROM\s+)?(?:\[?dbo\]?\.)?(?:\[?AuditLog\]?)\b
|
||||
// matches: "DELETE FROM AuditLog", "DELETE FROM dbo.AuditLog",
|
||||
// "DELETE FROM [AuditLog]", "DELETE FROM [dbo].[AuditLog]"
|
||||
// does NOT match: "DENY DELETE ON dbo.AuditLog" (DELETE is followed by ON)
|
||||
return AuditLogMutationPattern.IsMatch(text);
|
||||
}
|
||||
|
||||
private static readonly Regex AuditLogMutationPattern = new(
|
||||
@"\bUPDATE\s+(?:\[?dbo\]?\.)?(?:\[?AuditLog\]?)\b" +
|
||||
@"|\bDELETE\s+(?:FROM\s+)?(?:\[?dbo\]?\.)?(?:\[?AuditLog\]?)\b",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Guard test: scan every *.cs file in ConfigurationDatabase (excluding
|
||||
// Designer/Snapshot EF artefacts and the obj/ directory).
|
||||
@@ -248,9 +263,28 @@ public class AuditLogAppendOnlyGuardTests
|
||||
Assert.True(ContainsAuditLogMutation(
|
||||
"update dbo.AuditLog set Actor = 'system' where Actor is null;"));
|
||||
|
||||
// AuditLog first, UPDATE second (reverse order — still a violation).
|
||||
// AuditLog mentioned earlier in the line (e.g. in a comment prefix), with a real
|
||||
// UPDATE dbo.AuditLog DML following — the DML occurrence must still be caught.
|
||||
Assert.True(ContainsAuditLogMutation(
|
||||
"-- AuditLog: UPDATE dbo.AuditLog SET x = 1"));
|
||||
|
||||
// ---- Bracketed identifier forms (SSMS-generated SQL) ----
|
||||
|
||||
// UPDATE [dbo].[AuditLog] — bracketed schema and bracketed table.
|
||||
Assert.True(ContainsAuditLogMutation(
|
||||
"UPDATE [dbo].[AuditLog] SET DetailsJson = @json WHERE EventId = @id;"));
|
||||
|
||||
// UPDATE [AuditLog] — bracketed table, no schema prefix.
|
||||
Assert.True(ContainsAuditLogMutation(
|
||||
"UPDATE [AuditLog] SET Status = 'Corrected' WHERE EventId = @id;"));
|
||||
|
||||
// DELETE FROM [dbo].[AuditLog] — bracketed schema and bracketed table.
|
||||
Assert.True(ContainsAuditLogMutation(
|
||||
"DELETE FROM [dbo].[AuditLog] WHERE OccurredAtUtc < @threshold;"));
|
||||
|
||||
// DELETE FROM [AuditLog] — bracketed table, no schema prefix.
|
||||
Assert.True(ContainsAuditLogMutation(
|
||||
"DELETE FROM [AuditLog] WHERE OccurredAtUtc < @threshold;"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -273,6 +307,10 @@ public class AuditLogAppendOnlyGuardTests
|
||||
Assert.False(ContainsAuditLogMutation(
|
||||
"DELETE FROM dbo.Notifications WHERE CompletedAtUtc < @cutoff;"));
|
||||
|
||||
// Notifications DELETE using bracketed identifiers — AuditLog not present:
|
||||
Assert.False(ContainsAuditLogMutation(
|
||||
"DELETE FROM [dbo].[Notifications] WHERE CompletedAtUtc < @cutoff;"));
|
||||
|
||||
// SiteCalls DELETE (legitimate; AuditLog not present on the line):
|
||||
Assert.False(ContainsAuditLogMutation(
|
||||
"DELETE FROM dbo.SiteCalls WHERE TerminalAtUtc < @cutoff;"));
|
||||
|
||||
Reference in New Issue
Block a user