У меня есть требование провести аудит всех наших хранимых процедур, тысячи из них и определить, какие из них только для чтения или чтения-записи. Мне было интересно, знает ли кто-нибудь хороший способ сделать это точно.Сценарий, чтобы определить, является ли хранимая процедура только для чтения или чтения-записи.
Я написал свой собственный сценарий до сих пор, но я получаю только точность ~ 85%. Я запускаю хранимые процедуры, которые действительно только для чтения, но они создают несколько временных таблиц. Для моих целей это только для чтения. Я не могу просто игнорировать их, потому что есть много процедур чтения и записи, работающих с временными таблицами.
[EDIT] я с точностью примерно ~ 85%, смотря на 20 процедур, я знаю, что это довольно сложный и сравнивая их с результатами, которые я получил от запроса.
Вот запрос я в настоящее время с помощью:
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 символов. На самом деле я мог бы предложить вам ряд предложений. Получите список procs, возвращаемый этим запросом, а затем попробуйте предложение Arrons и посмотрите, могу ли я вырезать еще больше. Я был бы доволен точностью 95%.
Я дам этому еще один день или около того, чтобы узнать, могу ли я получить какие-либо дополнительные предложения, но большое вам спасибо.
[FINAL EDIT] Хорошо, вот что я в конечном итоге делает, и это выглядит как я получаю точность по крайней мере, 95%, может быть выше. Я попытался удовлетворить любой сценарий, который я мог бы придумать.
Я записал хранимые процедуры в файлы и написал приложение winform C# для анализа файлов и поиска тех, у которых есть законные «записи» в реальную базу данных.
Я счастлив опубликовать этот код для двигателя состояния, который я использовал здесь, но не имеет никаких гарантий. Я испытываю давление, чтобы доставить, и на самом деле не успел украсить код, и рефакторинг с красивыми именами переменных и т. Д., И добавил в него хорошие комментарии, у меня было 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 есть какие-то ярлыки. –
@AaronBertrand Точно, поэтому я разместил этот вопрос – Ryk
К сожалению, ответ будет отрицательным (и я не предлагаю вам не задавать вопрос). Как вы уже знали, вы можете искать определенные вещи в тексте хранимой процедуры, чтобы сделать некоторые обоснованные предположения о том, что она делает. Однако это сценарий «доверять, но проверять». RegEx и сопоставление образцов будут указывать только количество, а не качество. Также есть все виды дополнительных переменных, которые еще не были подняты, например, обнаружено ли обнаруженное слово в комментарии, имя параметра или переменной, строковый литерал и т. Д. –