我有一个要求来审计我们的所有存储过程,其中成千上万个存储过程,并确定哪些是只读或读写。我想知道是否有人知道准确地做到这一点的好方法。用于确定存储过程是只读还是读写的脚本
我已经写了我自己的脚本到目前为止,但我只有〜85%的准确性。我绊倒了真正只读的存储过程,但他们创建了一些临时表。为了我的目的,这些是只读的。我也不能忽略这些,因为有很多读写过程也适用于临时表。
[编辑] 我在20名的程序,我知道找那是相当复杂的,他们比较我从查询得到的结果有大致〜85%的准确性。
这是我目前使用的查询:
CREATE TABLE tempdb.dbo.[_tempProcs]
(objectname varchar(150), dbname varchar(150), ROUTINE_DEFINITION varchar(4000))
GO
EXEC sp_MSforeachdb
'USE [?]
DECLARE @dbname VARCHAR(200)
SET @dbname = DB_NAME()
IF 1 = 1 AND (@dbname NOT IN (''master'',''model'',''msdb'',''tempdb'',''distribution'')
BEGIN
EXEC(''
INSERT INTO tempdb.dbo.[_tempProcs](objectname, dbname, ROUTINE_DEFINITION)
SELECT ROUTINE_NAME AS ObjectName, ''''?'''' AS dbname, ROUTINE_DEFINITION
FROM [?].INFORMATION_SCHEMA.ROUTINES WITH(NOLOCK)
WHERE ROUTINE_DEFINITION LIKE ''''%INSERT [^]%''''
OR ROUTINE_DEFINITION LIKE ''''%UPDATE [^]%''''
OR ROUTINE_DEFINITION LIKE ''''%INTO [^]%''''
OR ROUTINE_DEFINITION LIKE ''''%DELETE [^]%''''
OR ROUTINE_DEFINITION LIKE ''''%CREATE TABLE[^]%''''
OR ROUTINE_DEFINITION LIKE ''''%DROP [^]%''''
OR ROUTINE_DEFINITION LIKE ''''%ALTER [^]%''''
OR ROUTINE_DEFINITION LIKE ''''%TRUNCATE [^]%''''
AND ROUTINE_TYPE=''''PROCEDURE''''
'')
END
'
GO
SELECT * FROM tempdb.dbo.[_tempProcs] WITH(NOLOCK)
我还没有细化它,此刻我只想把重点放在可写的查询,看看我能得到它准确。还有一个问题是ROUTINE_DEFINITION只给出了前4000个字符,所以我可能会错过任何正在写入4000个字符长度之后的字符。我可能会最终提出一些建议。获取这个查询返回的特效列表,然后进一步尝试Arrons的建议,看看我是否可以清除更多。我会很高兴95%的准确性。
我将在这一天左右再来看看我能否得到任何进一步的建议,但非常感谢。好吧,这是我最终做的,看起来我至少有95%的准确性,可能会更高。我试图迎合任何我可以想到的场景。
我将存储过程脚本化为文件,并编写了一个C#winform应用程序来解析这些文件并找到合法写入真实数据库的应用程序。
我很高兴发布此代码的状态引擎我用在这里,但没有保证。我承受着压力,实际上没有时间来美化代码,并用很好的变量名称等进行重构,并在其中添加了很好的评论,我有3个小时的时间来完成它,并且我只是挤压它,因此对于那些谁在乎,并且可能在未来帮助,那就是:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
namespace SQLParser
{
public class StateEngine
{
public static class CurrentState
{
public static bool IsInComment;
public static bool IsInCommentBlock;
public static bool IsInInsert;
public static bool IsInUpdate;
public static bool IsInDelete;
public static bool IsInCreate;
public static bool IsInDrop;
public static bool IsInAlter;
public static bool IsInTruncate;
public static bool IsInInto;
}
public class ReturnState
{
public int LineNumber { get; set; }
public bool Value { get; set; }
public string Line { get; set; }
}
private static int _tripLine = 0;
private static string[] _lines;
public ReturnState ParseFile(string fileName)
{
var retVal = false;
_tripLine = 0;
ResetCurrentState();
_lines = File.ReadAllLines(fileName);
for (int i = 0; i < _lines.Length; i++)
{
retVal = ParseLine(_lines[i], i);
//return true the moment we have a valid case
if (retVal)
{
ResetCurrentState();
return new ReturnState() { LineNumber = _tripLine, Value = retVal, Line = _lines[_tripLine] };
}
}
if (CurrentState.IsInInsert ||
CurrentState.IsInDelete ||
CurrentState.IsInUpdate ||
CurrentState.IsInDrop ||
CurrentState.IsInAlter ||
CurrentState.IsInTruncate)
{
retVal = true;
ResetCurrentState();
return new ReturnState() { LineNumber = _tripLine, Value = retVal, Line = _lines[_tripLine] };
}
return new ReturnState() { LineNumber = -1, Value = retVal };
}
private static void ResetCurrentState()
{
CurrentState.IsInAlter = false;
CurrentState.IsInCreate = false;
CurrentState.IsInDelete = false;
CurrentState.IsInDrop = false;
CurrentState.IsInInsert = false;
CurrentState.IsInTruncate = false;
CurrentState.IsInUpdate = false;
CurrentState.IsInInto = false;
CurrentState.IsInComment = false;
CurrentState.IsInCommentBlock = false;
}
private static bool ParseLine(string sqlLine, int lineNo)
{
var retVal = false;
var _currentWord = 0;
var _tripWord = 0;
var _offsetTollerance = 4;
sqlLine = sqlLine.Replace("\t", " ");
//This would have been set in previous line, so reset it
if (CurrentState.IsInComment)
CurrentState.IsInComment = false;
var words = sqlLine.Split(char.Parse(" ")).Where(x => x.Length > 0).ToArray();
for (int i = 0; i < words.Length; i++)
{
if (string.IsNullOrWhiteSpace(words[i]))
continue;
_currentWord += 1;
if (CurrentState.IsInCommentBlock && words[i].EndsWith("*/") || words[i] == "*/") { CurrentState.IsInCommentBlock = false; }
if (words[i].StartsWith("/*")) { CurrentState.IsInCommentBlock = true; }
if (words[i].StartsWith("--") && !CurrentState.IsInCommentBlock) { CurrentState.IsInComment = true; }
if (words[i].Length == 1 && CurrentState.IsInUpdate)
{
//find the alias table name, find 'FROM' and then next word
var tempAlias = words[i];
var tempLine = lineNo;
for (int l = lineNo; l < _lines.Length; l++)
{
var nextWord = "";
var found = false;
var tempWords = _lines[l].Replace("\t", " ").Split(char.Parse(" ")).Where(x => x.Length > 0).ToArray();
for (int m = 0; m < tempWords.Length; m++)
{
if (found) { break; }
if (tempWords[m].ToLower() == tempAlias && tempWords[m - m == 0 ? m : 1].ToLower() != "update")
{
nextWord = m == tempWords.Length - 1 ? "" : tempWords[m + 1].ToString();
var prevWord = m == 0 ? "" : tempWords[m - 1].ToString();
var testWord = "";
if (nextWord.ToLower() == "on" || nextWord == "")
{
testWord = prevWord;
}
if (prevWord.ToLower() == "from")
{
testWord = nextWord;
}
found = true;
if (testWord.StartsWith("#") || testWord.StartsWith("@"))
{
ResetCurrentState();
}
break;
}
}
if (found) { break; }
}
}
if (!CurrentState.IsInComment && !CurrentState.IsInCommentBlock)
{
#region SWITCH
if (words[i].EndsWith(";"))
{
retVal = SetStateReturnValue(retVal);
ResetCurrentState();
return retVal;
}
if ((CurrentState.IsInCreate || CurrentState.IsInDrop && (words[i].ToLower() == "procedure" || words[i].ToLower() == "proc")) && (lineNo > _tripLine ? 1000 : _currentWord - _tripWord) < _offsetTollerance)
ResetCurrentState();
switch (words[i].ToLower())
{
case "insert":
//assume that we have parsed all lines/words and got to next keyword, so return previous state
retVal = SetStateReturnValue(retVal);
if (retVal)
return retVal;
CurrentState.IsInInsert = true;
_tripLine = lineNo;
_tripWord = _currentWord;
continue;
case "update":
//assume that we have parsed all lines/words and got to next keyword, so return previous state
retVal = SetStateReturnValue(retVal);
if (retVal)
return retVal;
CurrentState.IsInUpdate = true;
_tripLine = lineNo;
_tripWord = _currentWord;
continue;
case "delete":
//assume that we have parsed all lines/words and got to next keyword, so return previous state
retVal = SetStateReturnValue(retVal);
if (retVal)
return retVal;
CurrentState.IsInDelete = true;
_tripLine = lineNo;
_tripWord = _currentWord;
continue;
case "into":
//assume that we have parsed all lines/words and got to next keyword, so return previous state
//retVal = SetStateReturnValue(retVal, lineNo);
//if (retVal)
// return retVal;
CurrentState.IsInInto = true;
_tripLine = lineNo;
_tripWord = _currentWord;
continue;
case "create":
//assume that we have parsed all lines/words and got to next keyword, so return previous state
retVal = SetStateReturnValue(retVal);
if (retVal)
return retVal;
CurrentState.IsInCreate = true;
_tripLine = lineNo;
_tripWord = _currentWord;
continue;
case "drop":
//assume that we have parsed all lines/words and got to next keyword, so return previous state
retVal = SetStateReturnValue(retVal);
if (retVal)
return retVal;
CurrentState.IsInDrop = true;
_tripLine = lineNo;
continue;
case "alter":
//assume that we have parsed all lines/words and got to next keyword, so return previous state
retVal = SetStateReturnValue(retVal);
if (retVal)
return retVal;
CurrentState.IsInAlter = true;
_tripLine = lineNo;
_tripWord = _currentWord;
continue;
case "truncate":
//assume that we have parsed all lines/words and got to next keyword, so return previous state
retVal = SetStateReturnValue(retVal);
if (retVal)
return retVal;
CurrentState.IsInTruncate = true;
_tripLine = lineNo;
_tripWord = _currentWord;
break;
default:
break;
}
#endregion
if (CurrentState.IsInInsert || CurrentState.IsInDelete || CurrentState.IsInUpdate || CurrentState.IsInDrop || CurrentState.IsInAlter || CurrentState.IsInTruncate || CurrentState.IsInInto)
{
if ((words[i].StartsWith("#") || words[i].StartsWith("@") || words[i].StartsWith("dbo.#") || words[i].StartsWith("[email protected]")) && (lineNo > _tripLine ? 1000 : _currentWord - _tripWord) < _offsetTollerance)
{
ResetCurrentState();
continue;
}
}
if ((CurrentState.IsInInsert || CurrentState.IsInInto || CurrentState.IsInUpdate) && (((_currentWord != _tripWord) && (lineNo > _tripLine ? 1000 : _currentWord - _tripWord) < _offsetTollerance) || (lineNo > _tripLine)))
{
retVal = SetStateReturnValue(retVal);
if (retVal)
return retVal;
}
}
}
return retVal;
}
private static bool SetStateReturnValue(bool retVal)
{
if (CurrentState.IsInInsert ||
CurrentState.IsInDelete ||
CurrentState.IsInUpdate ||
CurrentState.IsInDrop ||
CurrentState.IsInAlter ||
CurrentState.IsInTruncate)
{
retVal = (CurrentState.IsInInsert ||
CurrentState.IsInDelete ||
CurrentState.IsInUpdate ||
CurrentState.IsInDrop ||
CurrentState.IsInAlter ||
CurrentState.IsInTruncate);
}
return retVal;
}
}
}
用法
var fileResult = new StateEngine().ParseFile(*path and filename*);
如果一个存储过程写入到另一个数据库,或者使用'RAISERROR WITH LOG'或XP_REGWRITE,或写入文件或发送电子邮件?基本上你正在试图建立一个非常宽的网络,我不认为在SQL Server中有这个捷径。 – 2012-02-23 21:54:16
@AaronBertrand这就是为什么我发布这个问题的原因 – Ryk 2012-02-23 22:04:40
不幸的是答案是否定的(我不是说你不应该问这个问题)。正如您已经意识到的那样,您可以在存储过程的文本中搜索某些内容,以便进行一些有教育的猜测。然而,这是“信任但验证”的情况。 RegEx和模式匹配只会显示数量,而不是质量。此外,还有各种其他变量尚未提出,例如发现的单词是否在评论,参数或变量名称,字符串文字等中找到。 – 2012-02-23 22:14:16